Spring REST API Quick Start

From bibbleWiki
Jump to navigation Jump to search

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

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

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.