diff --git a/articles/quickstart/backend/java-spring-security5/01-authorization.md b/articles/quickstart/backend/java-spring-security5/01-authorization.md index 2b6fb145ea..fa3cf07bb4 100644 --- a/articles/quickstart/backend/java-spring-security5/01-authorization.md +++ b/articles/quickstart/backend/java-spring-security5/01-authorization.md @@ -1,7 +1,7 @@ --- title: Authorization -name: How to secure your API using Spring Security 5 and Auth0 -description: This tutorial demonstrates how to add authorization to an API using Spring Security 5. +name: How to secure your API with Spring Boot +description: This tutorial demonstrates how to add authorization to an API using the Okta Spring Boot Starter. budicon: 500 topics: - quickstart @@ -30,24 +30,20 @@ This Quickstart uses Spring MVC. If you are using Spring WebFlux, the steps to s The sample project uses a `/src/main/resources/application.yml` file, which configures it to use the correct Auth0 **Domain** and **API Identifier** for your API. If you download the code from this page it will be automatically configured. If you clone the example from GitHub, you will need to fill it in yourself. ```yaml -auth0: - audience: ${apiIdentifier} -spring: - security: - oauth2: - resourceserver: - jwt: - issuer-uri: https://${account.namespace}/ +okta: + oauth2: + # Replace with the domain of your Auth0 tenant. + issuer: https://${account.namespace}/ + # Replace with the API Identifier for your Auth0 API. + audience: ${apiIdentifier} ``` | Attribute | Description| | --- | --- | -| `auth0.audience` | The unique identifier for your API. If you are following the steps in this tutorial it would be `https://quickstarts/api`. | -| `spring.security.oauth2.resourceserver.jwt.issuer-uri` | The issuer URI of the resource server, which will be the value of the `iss` claim in the JWT issued by Auth0. Spring Security will use this property to discover the authorization server's public keys and validate the JWT signature. The value will be your Auth0 domain with an `https://` prefix and a `/` suffix (the trailing slash is important). | +| `okta.oauth2.audience` | The unique identifier for your API. If you are following the steps in this tutorial it would be `https://quickstarts/api`. | +| `okta.oauth2.issuer` | The issuer URI of the resource server, which will be the value of the `iss` claim in the JWT issued by Auth0. Spring Security will use this property to discover the authorization server's public keys and validate the JWT signature. The value will be your Auth0 domain with an `https://` prefix and a `/` suffix (the trailing slash is important). | -## Validate Access Tokens - -### Install dependencies +## Install dependencies If you are using Gradle, you can add the required dependencies using the [Spring Boot Gradle Plugin](https://docs.spring.io/spring-boot/docs/current/gradle-plugin/reference/html/) and the [Dependency Management Plugin](https://docs.spring.io/dependency-management-plugin/docs/current/reference/html/) to resolve dependency versions: @@ -55,13 +51,14 @@ If you are using Gradle, you can add the required dependencies using the [Spring // build.gradle plugins { - id 'org.springframework.boot' version '2.5.12' - id 'io.spring.dependency-management' version '1.0.9.RELEASE' + id 'java' + id 'org.springframework.boot' version '3.1.5' + id 'io.spring.dependency-management' version '1.1.3' } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' + implementation 'com.okta.spring:okta-spring-boot-starter:3.0.5' } ``` @@ -73,7 +70,7 @@ If you are using Maven, add the Spring dependencies to your `pom.xml` file: org.springframework.boot spring-boot-starter-parent - 2.5.12 + 3.1.5 @@ -83,113 +80,19 @@ If you are using Maven, add the Spring dependencies to your `pom.xml` file: spring-boot-starter-web - org.springframework.boot - spring-boot-starter-oauth2-resource-server + com.okta + okta-spring-boot-starter + 3.0.5 ``` -### Configure the resource server - -To configure the application as a Resource Server and validate the JWTs, create a class that will provide an instance of `SecurityFilterChain`, and add the `@EnableWebSecurity` annotation: - -```java -// src/main/java/com/auth0/example/security/SecurityConfig.java - -import org.springframework.context.annotation.Bean; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.web.SecurityFilterChain; - -@EnableWebSecurity -public class SecurityConfig { - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http.oauth2Login(); - return http.build(); - } -} -``` - -### Validate the audience - -> Note: If you are using Spring Boot version 2.7 or higher, audience validation is supported out of the box by Spring Security. Instead of customizing beans as indicated below, you should just include the Auth0 API identifier as a value in the `spring.security.oauth2.resourceserver.jwt.audiences` array. -> Example: `spring.security.oauth2.resourceserver.jwt.audiences=http://api-identifier` - -In addition to validating the JWT, you also need to validate that the JWT is intended for your API by checking the `aud` claim of the JWT. Create a new class named `AudienceValidator` that implements the `OAuth2TokenValidator` interface: - -```java -// src/main/java/com/auth0/example/security/AudienceValidator.java - -import org.springframework.security.oauth2.core.OAuth2Error; -import org.springframework.security.oauth2.core.OAuth2TokenValidator; -import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; -import org.springframework.security.oauth2.jwt.Jwt; - -class AudienceValidator implements OAuth2TokenValidator { - private final String audience; - - AudienceValidator(String audience) { - this.audience = audience; - } - - public OAuth2TokenValidatorResult validate(Jwt jwt) { - OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null); - - if (jwt.getAudience().contains(audience)) { - return OAuth2TokenValidatorResult.success(); - } - return OAuth2TokenValidatorResult.failure(error); - } -} -``` - -Update the `SecurityConfig` class to configure a `JwtDecoder` bean that uses the `AudienceValidator`: - -```java -// src/main/java/com/auth0/example/security/SecurityConfig.java - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; -import org.springframework.security.oauth2.core.OAuth2TokenValidator; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.jwt.JwtDecoder; -import org.springframework.security.oauth2.jwt.JwtDecoders; -import org.springframework.security.oauth2.jwt.JwtValidators; -import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; - -@EnableWebSecurity -public class SecurityConfig { - - @Value("<%= "${auth0.audience}" %>") - private String audience; - - @Value("<%= "${spring.security.oauth2.resourceserver.jwt.issuer-uri}" %>") - private String issuer; - - @Bean - JwtDecoder jwtDecoder() { - NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) - JwtDecoders.fromOidcIssuerLocation(issuer); - - OAuth2TokenValidator audienceValidator = new AudienceValidator(audience); - OAuth2TokenValidator withIssuer = JwtValidators.createDefaultWithIssuer(issuer); - OAuth2TokenValidator withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator); - - jwtDecoder.setJwtValidator(withAudience); - - return jwtDecoder; - } -} -``` - -## Protect API Endpoints +## Protect API endpoints <%= include('../_includes/_api_endpoints') %> +To configure the application as a Resource Server and validate the JWTs, create a class that will register a [SecurityFilterChain](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/SecurityFilterChain.html), an instance of `SecurityFilterChain`, and add the `@Configuration` annotation. + The example below shows how to secure API methods using the `HttpSecurity` object provided in the `filterChain()` method of the `SecurityConfig` class. Route matchers are used to restrict access based on the level of authorization required: ```java @@ -218,28 +121,16 @@ public class SecurityConfig { ::: note By default, Spring Security will create a `GrantedAuthority` for each scope in the `scope` claim of the JWT. This is what enables using the `hasAuthority("SCOPE_read:messages")` method to restrict access to a valid JWT that contains the `read:messages` scope. - -If your use case requires different claims to make authorization decisions, see the [Spring Security Reference Documentation](https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#oauth2resourceserver-authorization-extraction) to learn how to customize the extracted authorities. ::: ### Create the API controller -Create a new class named `Message`, which is the domain object the API will return: +Create a new record named `Message`, which will be the domain object the API will return: ```java // src/main/java/com/auth0/example/model/Message.java -public class Message { - private final String message; - - public Message(String message) { - this.message = message; - } - - public String getMessage() { - return this.message; - } -} +public record Message(String message) {} ``` Create a new class named `APIController` to handle requests to the endpoints: @@ -254,6 +145,10 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +/** + * Handles requests to "/api" endpoints. + * @see com.auth0.example.security.SecurityConfig to see how these endpoints are protected. + */ @RestController @RequestMapping(path = "api", produces = MediaType.APPLICATION_JSON_VALUE) // For simplicity of this sample, allow all origins. Real applications should configure CORS for their use case. diff --git a/articles/quickstart/backend/java-spring-security5/files/application.md b/articles/quickstart/backend/java-spring-security5/files/application.md index 8b101d0381..f12a521ce0 100644 --- a/articles/quickstart/backend/java-spring-security5/files/application.md +++ b/articles/quickstart/backend/java-spring-security5/files/application.md @@ -3,12 +3,10 @@ name: application.yml language: yaml --- ```yaml -auth0: - audience: ${apiIdentifier} -spring: - security: - oauth2: - resourceserver: - jwt: - issuer-uri: https://${account.namespace}/ +okta: + oauth2: + # Replace with the domain of your Auth0 tenant. + issuer: https://${account.namespace}/ + # Replace with the API Identifier for your Auth0 API. + audience: ${apiIdentifier} ``` \ No newline at end of file diff --git a/articles/quickstart/backend/java-spring-security5/files/audience-validator.md b/articles/quickstart/backend/java-spring-security5/files/audience-validator.md deleted file mode 100644 index 4dee42194d..0000000000 --- a/articles/quickstart/backend/java-spring-security5/files/audience-validator.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: AudienceValidator.java -language: java ---- -```java -import org.springframework.security.oauth2.core.OAuth2Error; -import org.springframework.security.oauth2.core.OAuth2TokenValidator; -import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; -import org.springframework.security.oauth2.jwt.Jwt; - -class AudienceValidator implements OAuth2TokenValidator { - private final String audience; - - AudienceValidator(String audience) { - this.audience = audience; - } - - public OAuth2TokenValidatorResult validate(Jwt jwt) { - OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null); - - if (jwt.getAudience().contains(audience)) { - return OAuth2TokenValidatorResult.success(); - } - return OAuth2TokenValidatorResult.failure(error); - } -} -``` \ No newline at end of file diff --git a/articles/quickstart/backend/java-spring-security5/files/message.md b/articles/quickstart/backend/java-spring-security5/files/message.md index 2b5a51290a..99f4056914 100644 --- a/articles/quickstart/backend/java-spring-security5/files/message.md +++ b/articles/quickstart/backend/java-spring-security5/files/message.md @@ -3,15 +3,9 @@ name: Message.java language: java --- ```java -public class Message { - private final String message; +/** + * Simple domain object for our API to return a message. + */ +public record Message(String message) {} - public Message(String message) { - this.message = message; - } - - public String getMessage() { - return this.message; - } -} ``` \ No newline at end of file diff --git a/articles/quickstart/backend/java-spring-security5/files/security-config.md b/articles/quickstart/backend/java-spring-security5/files/security-config.md index fe9d549af1..257bdbaa32 100644 --- a/articles/quickstart/backend/java-spring-security5/files/security-config.md +++ b/articles/quickstart/backend/java-spring-security5/files/security-config.md @@ -3,59 +3,40 @@ name: SecurityConfig.java language: java --- ```java -import org.springframework.beans.factory.annotation.Value; +package com.auth0.example.security; + + import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; -import org.springframework.security.oauth2.core.OAuth2TokenValidator; -import org.springframework.security.oauth2.jwt.*; import org.springframework.security.web.SecurityFilterChain; +import static org.springframework.security.config.Customizer.withDefaults; + /** * Configures our application with Spring Security to restrict access to our API endpoints. */ -@EnableWebSecurity +@Configuration public class SecurityConfig { - @Value("<%= "${auth0.audience}" %>") - private String audience; - - @Value("<%= "${spring.security.oauth2.resourceserver.jwt.issuer-uri}" %>") - private String issuer; - @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { /* This is where we configure the security required for our endpoints and setup our app to serve as an OAuth2 Resource Server, using JWT validation. */ - http.authorizeRequests() - .mvcMatchers("/api/public").permitAll() - .mvcMatchers("/api/private").authenticated() - .mvcMatchers("/api/private-scoped").hasAuthority("SCOPE_read:messages") - .and().cors() - .and().oauth2ResourceServer().jwt(); - return http.build(); - } - - @Bean - JwtDecoder jwtDecoder() { - /* - By default, Spring Security does not validate the "aud" claim of the token, to ensure that this token is - indeed intended for our app. Adding our own validator is easy to do: - */ - - NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) - JwtDecoders.fromOidcIssuerLocation(issuer); - - OAuth2TokenValidator audienceValidator = new AudienceValidator(audience); - OAuth2TokenValidator withIssuer = JwtValidators.createDefaultWithIssuer(issuer); - OAuth2TokenValidator withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator); - - jwtDecoder.setJwtValidator(withAudience); - - return jwtDecoder; + return http + .authorizeHttpRequests((authorize) -> authorize + .requestMatchers("/api/public").permitAll() + .requestMatchers("/api/private").authenticated() + .requestMatchers("/api/private-scoped").hasAuthority("SCOPE_read:messages") + ) + .cors(withDefaults()) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(withDefaults()) + ) + .build(); } } + ``` \ No newline at end of file diff --git a/articles/quickstart/backend/java-spring-security5/index.yml b/articles/quickstart/backend/java-spring-security5/index.yml index 1cac540d5e..9baab5db4c 100644 --- a/articles/quickstart/backend/java-spring-security5/index.yml +++ b/articles/quickstart/backend/java-spring-security5/index.yml @@ -8,8 +8,8 @@ author: email: jim.anderson@auth0.com community: false sdk: - name: auth0-spring-security-api - url: https://github.com/auth0/auth0-spring-security-api + name: Okta Spring Boot Starter + url: https://github.com/okta/okta-spring-boot/ logo: spring alias: - spring security @@ -34,7 +34,7 @@ github: org: auth0-samples repo: auth0-spring-security5-api-sample requirements: - - Java 8 or above + - Java 17 next_steps: - path: 01-authorization list: diff --git a/articles/quickstart/backend/java-spring-security5/interactive.md b/articles/quickstart/backend/java-spring-security5/interactive.md index 4d09273327..bae669d759 100644 --- a/articles/quickstart/backend/java-spring-security5/interactive.md +++ b/articles/quickstart/backend/java-spring-security5/interactive.md @@ -11,7 +11,6 @@ topics: - spring files: - files/application - - files/audience-validator - files/security-config - files/message - files/api-controller @@ -40,8 +39,8 @@ The sample project uses a `/src/main/resources/application.yml` file, which conf | Attribute | Description| | --- | --- | -| `auth0.audience` | The unique identifier for your API. If you are following the steps in this tutorial it would be `https://quickstarts/api`. | -| `spring.security.oauth2.resourceserver.jwt.issuer-uri` | The value of the `iss` claim and the issuer URI of the resource server in the JWT issued by Auth0. Spring Security uses this property to discover the authorization server's public keys and validate the JWT signature. The value is Auth0 domain with an `https://` prefix and a `/` suffix (the trailing slash is important). | +| `okta.oauth2.audience` | The unique identifier for your API. If you are following the steps in this tutorial it would be `https://quickstarts/api`. | +| `okta.oauth2.issuer` | The issuer URI of the resource server, which will be the value of the `iss` claim in the JWT issued by Auth0. Spring Security will use this property to discover the authorization server's public keys and validate the JWT signature. The value will be your Auth0 domain with an `https://` prefix and a `/` suffix (the trailing slash is important). ## Install dependencies {{{ data-action=code data-code="application.yml#1:8" }}} @@ -51,13 +50,14 @@ If you are using Gradle, you can add the required dependencies using the [Spring // build.gradle plugins { - id 'org.springframework.boot' version '2.5.12' - id 'io.spring.dependency-management' version '1.0.9.RELEASE' + id 'java' + id 'org.springframework.boot' version '3.1.5' + id 'io.spring.dependency-management' version '1.1.3' } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' + implementation 'com.okta.spring:okta-spring-boot-starter:3.0.5' } ``` @@ -69,7 +69,7 @@ If you are using Maven, add the Spring dependencies to your `pom.xml` file: org.springframework.boot spring-boot-starter-parent - 2.5.12 + 3.1.5 @@ -79,39 +79,31 @@ If you are using Maven, add the Spring dependencies to your `pom.xml` file: spring-boot-starter-web - org.springframework.boot - spring-boot-starter-oauth2-resource-server + com.okta + okta-spring-boot-starter + 3.0.5 ``` -## Validate the audience {{{ data-action=code data-code="AudienceValidator.java#13:20" }}} - -To validate the JWT, you also need to validate that the JWT is intended for your API by checking the `aud` claim of the JWT. Create a new class named `AudienceValidator` that implements the `OAuth2TokenValidator` interface and override the `validate` method to verify whether the required `audience` is present. ## Configure the resource server {{{ data-action=code data-code="SecurityConfig.java" }}} -To configure the application as a Resource Server and validate the JWTs, create a class that will provide an instance of `SecurityFilterChain`, and add the `@EnableWebSecurity` annotation: +To configure the application as a Resource Server and validate the JWTs, create a class that will provide an instance of `SecurityFilterChain`, and add the `@Configuration` annotation. ### Protect API endpoints <%= include('../_includes/_api_endpoints') %> -The example below shows how to secure API methods using the `HttpSecurity` object provided in the `filterChain()` method of the `SecurityConfig` class. Route matchers restrict access based on the level of authorization required: +The example below shows how to secure API methods using the `HttpSecurity` object provided in the `filterChain()` method of the `SecurityConfig` class. Route matchers restrict access based on the level of authorization required. ::: note By default, Spring Security creates a `GrantedAuthority` for each scope in the `scope` claim of the JWT. This scope enables using the `hasAuthority("SCOPE_read:messages")` method to restrict access to a valid JWT that contains the `read:messages` scope. - -If your use case requires different claims to make authorization decisions, see the [Spring Security Reference Documentation](https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#oauth2resourceserver-authorization-extraction) to learn how to customize the extracted authorities. ::: -### Configure JWT Validator - -Update the `SecurityConfig` class to configure a `JwtDecoder` bean that uses the `AudienceValidator`. The `auth0.audience` value from `application.yml` validates the `aud` claim. The `issuer-uri` value from `application.yml` verifies the `issuer`. - ## Create the Domain Object {{{ data-action=code data-code="Message.java#1:11" }}} -To make your endpoint return a JSON, you can use a [POJO](https://en.wikipedia.org/wiki/Plain_old_Java_object) (Plain Old Java Object). The member variables of this object is serialized into the key value for your JSON. Create a new class named `Message` as an example domain object to return during the API calls. +To make your endpoint return a JSON, you can use a Java record. The member variables of this object is serialized into the key value for your JSON. Create a new record named `Message` as an example domain object to return during the API calls. ## Create the API controller {{{ data-action=code data-code="APIController.java" }}}