Spring REST API Quick Start: Difference between revisions
Line 166: | Line 166: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
=Setup MVC= | ==Setup MVC== | ||
This section sets up the data to be used with the keycloak server. The important part is the owner. We read the user_id from the Jwt to provide filtering on owner | This section sets up the data to be used with the keycloak server. The important part is the owner. We read the user_id from the Jwt to provide filtering on owner | ||
==Create Entity== | ===Create Entity=== | ||
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
@Entity | @Entity | ||
Line 208: | Line 208: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
==Create Repository== | ===Create Repository=== | ||
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
@RepositoryRestResource | @RepositoryRestResource | ||
Line 216: | Line 216: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
==Create Initializer== | ===Create Initializer=== | ||
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
@Component | @Component | ||
Line 251: | Line 251: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
==Create Controller== | ===Create Controller=== | ||
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
@RestController | @RestController |
Revision as of 01:07, 4 April 2021
Introduction
This is meant to just be a quickstart for me. I use many technologies so this reminds me how to get going.
Setup Spring
Go to start.spring.io and select JPA, Rest Repositories and H2 Database.
Import the resulting project as a maven project
Create Classes
Beer Class (Entity)
@Entity
public class Beer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getApv() {
return apv;
}
public void setApv(Double apv) {
this.apv = apv;
}
private String name;
private Double apv;
}
Beer Repository
package nz.co.bibble.restapi11;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource
public interface RestRepository extends CrudRepository<Beer,Long> {
}
Resources
Set the application properties for H2
spring.datasource.url=jdbc:h2:mem:beers
Create the Data.sql in the resources directory
INSERT INTO beer(name, apv) VALUES('Jai Alai', 7.5);
INSERT INTO beer(name, apv) VALUES('Stella Artois', 5.0);
INSERT INTO beer(name, apv) VALUES('Lagunitas', 6.2);
COMMIT;
Run
Goto http://localhost:8080/beers
Add Spring Security
Add Dependencies
We can add security to a new project with
<dependencies>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Add Web Security to App
@SpringBootApplication
public class Restapi11Application extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(Restapi11Application.class, args);
}
}
Amend WebSecurityConfigurerAdapter
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity (
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true
)
public class Restapi11SecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("iwiseman").password(passwordEncoder().encode("pass")).roles("USER");
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Add Oauth2 Security
Setup Application
I set up a Keycloak Server to do this. Assuming this all works these are the changes to make my /beer work
Application Properties
Starting to see why yaml is nice given the alternative.
# H2 definition
spring.datasource.url=jdbc:h2:mem:beers
# Server Port
server.port=8081
# Path
server.servlet.context-path=/resource-server-jwt
# Keycloak Server with Realm
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://192.168.1.70:9999/auth/realms/bibble
# Debug
logging.level.root=debug
logging.level.org.springframework.security=DEBUG
Add Dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
Define location of Keycloak Server
Add some debug for good measure
# Keycloak Server with Realm
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://192.168.1.70:9999/auth/realms/bibble
# Debug
logging.level.root=debug
logging.level.org.springframework.security=DEBUG
Setup MVC
This section sets up the data to be used with the keycloak server. The important part is the owner. We read the user_id from the Jwt to provide filtering on owner
Create Entity
@Entity
public class TestItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Column
private String description;
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
@Column
private UUID owner;
public UUID getOwner() {
return owner;
}
public void setOwner(UUID owner) {
this.owner = owner;
}
}
Create Repository
@RepositoryRestResource
public interface TestItemsRepository extends CrudRepository<TestItem,Long> {
List<TestItem> findByOwner(UUID owner);
}
Create Initializer
@Component
public class TestItemsInitializer implements SmartInitializingSingleton {
private final TestItemsRepository testItems;
public TestItemsInitializer(TestItemsRepository testItems) {
this.testItems = testItems;
}
@Override
public void afterSingletonsInstantiated() {
UUID iwiseman = UUID.fromString("cae12e7d-9c1c-4c26-a146-15b9f97f2cca");
UUID bwiseman = UUID.fromString("e5991839-9e2f-41c9-b188-02cc112eedf3");
this.testItems.save(create("Iain Read War and Peace", iwiseman));
this.testItems.save(create("Iain Free Solo the Eiffel Tower", iwiseman));
this.testItems.save(create("Iain Hang Christmas Lights", iwiseman));
this.testItems.save(create("Run for President", bwiseman));
this.testItems.save(create("Run a Marathon", bwiseman));
this.testItems.save(create("Run an Errand", bwiseman));
}
public TestItem create(String description, UUID owner) {
TestItem testItem = new TestItem();
testItem.setDescription(description);
testItem.setOwner(owner);
return testItem;
}
}
Create Controller
@RestController
public class TestItemController {
private final TestItemsRepository testItems;
public TestItemController(TestItemsRepository testItems) {
this.testItems = testItems;
}
@GetMapping("/testItems")
public List<TestItem> read(@AuthenticationPrincipal Jwt jwt) {
UUID owner = jwt.getClaim("user_id");
return this.testItems.findByOwner(owner);
}
}
Configure Spring Security
Modify the WebSecurityConfigurerAdapter
http configure
Restrict the requests to be authenticated and add the oauth2ResourceServer where we define the jwtAuthenticationConverter
@Configuration
@EnableWebSecurity(debug = true)
public class Restapi11SecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
private static final Logger logger = LoggerFactory.getLogger(FooController.class);
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(a -> a
.anyRequest().authenticated())
.oauth2ResourceServer(o -> o
.jwt(j -> j.jwtAuthenticationConverter(jwtAuthenticationConverter()))
);
}
...
Auto Wire the JwtDecoder
Define the custom ClaimSetConverter and JwtValidator to use
@Autowired
void jwtDecoder(JwtDecoder jwtDecoder) {
((NimbusJwtDecoder) jwtDecoder).setClaimSetConverter(claimSetConverter());
((NimbusJwtDecoder) jwtDecoder).setJwtValidator(jwtValidator());
}
Jwt Validator
For this example we are just looking at the Audience
private OAuth2TokenValidator<Jwt> jwtValidator() {
OAuth2TokenValidator<Jwt> audience = jwt ->
Optional.ofNullable(jwt.getAudience())
.filter(aud -> aud.contains("account"))
.map(aud -> success())
.orElse(failure(new OAuth2Error(INVALID_TOKEN, "Bad audience", "url")));
OAuth2TokenValidator<Jwt> defaults = JwtValidators.createDefault();
return new DelegatingOAuth2TokenValidator<>(defaults, audience);
}
Claim Set Converter
For this in the Keycloak server I modified the client and the user. For Client I added an attribute.
"id": "cf2a3f05-d29c-44bb-b08d-96a1b1c7b178",
"name": "user_id",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "true"
},
"protocolMappers": [
{
"id": "88e630bf-10e6-470e-9592-981392a906ab",
"name": "user_id",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "user_id",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "user_id",
"jsonType.label": "String"
}
}
]
This is then mapped to the user as an attribute.
From there we simply extract it from the Jwt.
private MappedJwtClaimSetConverter claimSetConverter() {
Converter<Object, UUID> converter = value -> {
if(value == null) {
return UUID.fromString("");
}
return UUID.fromString(value.toString());
};
return MappedJwtClaimSetConverter.withDefaults
(Collections.singletonMap("user_id", converter));
}
Issues
- The iss claim is not valid
This is caused by the wrong address in the issuer-uri where I put localhost and it wanted 192.168.x.x. You see iss in the Jwt Debugger which is what let me to fix this.