Spring REST API Quick Start: Difference between revisions

From bibbleWiki
Jump to navigation Jump to search
Line 271: Line 271:
</syntaxhighlight>
</syntaxhighlight>


==Configure Spring Security==
==Configure Spring Security WebSecurityConfigurerAdapter==
===http configure===
===http configure===
Restrict the requests to be authenticated and add the oauth2ResourceServer where we define the jwtAuthenticationConverter
Restrict the requests to be authenticated and add the oauth2ResourceServer where we define the jwtAuthenticationConverter

Revision as of 01:10, 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 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.