From a02c20a6640a06abae86c91936b26ae93c4ea3ae Mon Sep 17 00:00:00 2001 From: simeng-li Date: Mon, 17 Jun 2024 10:14:04 +0800 Subject: [PATCH] refactor(console): update spring boot api protection guide (#6018) update spring boot api protection guide --- .../docs/guides/api-spring-boot/README.mdx | 81 ++++++++++--------- 1 file changed, 43 insertions(+), 38 deletions(-) diff --git a/packages/console/src/assets/docs/guides/api-spring-boot/README.mdx b/packages/console/src/assets/docs/guides/api-spring-boot/README.mdx index 161da637a..609441c5e 100644 --- a/packages/console/src/assets/docs/guides/api-spring-boot/README.mdx +++ b/packages/console/src/assets/docs/guides/api-spring-boot/README.mdx @@ -35,9 +35,10 @@ dependencies { Since Spring Boot and Spring Security have built-in support for both OAuth2 resource server and JWT validation, you DO NOT need to add additional libraries from Logto to integrate. - See [Spring Security OAuth 2.0 Resource Server](https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/index.html) - and [Spring Security Architecture](https://spring.io/guides/topicals/spring-security-architecture) - for more details. +See [Spring Security OAuth 2.0 Resource Server](https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/index.html) +and [Spring Security Architecture](https://spring.io/guides/topicals/spring-security-architecture) +for more details. + @@ -51,14 +52,16 @@ and signed with [JWK](https://datatracker.ietf.org/doc/html/rfc7517) Before moving on, you will need to get an issuer and a JWKS URI to verify the issuer and the signature of the Bearer Token (`access_token`).

-All the Logto Authorization server configurations can be found by requesting {appendPath(props.endpoint, '/oidc/.well-known/openid-configuration')}, including the issuer, jwks_uri and other authorization configs. + All the Logto Authorization server configurations can be found by requesting{' '} + {appendPath(props.endpoint, '/oidc/.well-known/openid-configuration')}, including the{' '} + issuer, jwks_uri and other authorization configs.

An example of the response:
   
-{`{
+    {`{
   // ...
   "issuer": "${appendPath(props.endpoint, '/oidc')}",
   "jwks_uri": "${appendPath(props.endpoint, '/oidc/jwks')}"
@@ -74,8 +77,8 @@ An example of the response:
 Use an `application.yml` file (instead of the default `application.properties`) to configure the server port, audience, and OAuth2 resource server.
 
 
-
-{`# path/to/project/src/main/resources/application.yaml
+  
+    {`# path/to/project/src/main/resources/application.yaml
 server:
   port: 3000
 
@@ -89,7 +92,7 @@ spring:
         jwt:
           issuer-uri: ${appendPath(props.endpoint, '/oidc')}
           jwk-set-uri: ${appendPath(props.endpoint, '/oidc/jwks')}`}
-
+  
 
- `audience`: The unique API identifier of your protected API resource. @@ -113,8 +116,6 @@ import org.springframework.security.oauth2.jwt.Jwt; public class AudienceValidator implements OAuth2TokenValidator { - private final OAuth2Error oAuth2Error = new OAuth2Error("invalid_token", "Required audience not found", null); - private final String audience; public AudienceValidator(String audience) { @@ -124,18 +125,21 @@ public class AudienceValidator implements OAuth2TokenValidator { @Override public OAuth2TokenValidatorResult validate(Jwt jwt) { if (!jwt.getAudience().contains(audience)) { - return OAuth2TokenValidatorResult.failure(oAuth2Error); + return OAuth2TokenValidatorResult.failure(new OAuth2Error("invalid_token", "Required audience not found", null)); } + // Optional: For RBAC validate the scopes of the JWT. + String scopes = jwt.getClaimAsString("scope"); + if (scopes == null || !scopes.contains("read:profile")) { + return OAuth2TokenValidatorResult.failure(new OAuth2Error("invalid_token", "Insufficient permission", null)); + } + + return OAuth2TokenValidatorResult.success(); } } ``` - - For 🔐 RBAC, scope validation is also required. - - @@ -154,17 +158,19 @@ import com.nimbusds.jose.proc.SecurityContext; import io.logto.springboot.sample.validator.AudienceValidator; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; 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.JwtValidators; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; -import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.web.DefaultSecurityFilterChain; +@Configuration @EnableWebSecurity public class SecurityConfiguration { @@ -180,6 +186,8 @@ public class SecurityConfiguration { @Bean public JwtDecoder jwtDecoder() { NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwksUri) + // Logto uses the ES384 algorithm to sign the JWTs by default. + .jwsAlgorithm(ES384) // The decoder should support the token type: Access Token + JWT. .jwtProcessorCustomizer(customizer -> customizer.setJWSTypeVerifier( new DefaultJOSEObjectTypeVerifier(new JOSEObjectType("at+jwt")))) @@ -194,14 +202,17 @@ public class SecurityConfiguration { } @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt).cors().and() - .authorizeRequests(customizer -> customizer - // Only authenticated requests can access your protected APIs - .mvcMatchers("/", "/secret").authenticated() - // Anyone can access the public profile. - .mvcMatchers("/profile").permitAll() - ); + public DefaultSecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .securityMatcher("/api/**") + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(Customizer.withDefaults())) + .authorizeHttpRequests(requests -> requests + // Allow all requests to the public APIs. + .requestMatchers("/api/.wellknown/**").permitAll() + // Require jwt token validation for the protected APIs. + .anyRequest().authenticated()); + return http.build(); } } @@ -226,20 +237,14 @@ import org.springframework.web.bind.annotation.RestController; @CrossOrigin(origins = "*") @RestController public class ProtectedController { - - @GetMapping("/") - public String protectedRoot() { - return "Protected root."; + @GetMapping("/api/profile") + public String protectedProfile() { + return "Protected profile."; } - @GetMapping("/secret") - public String protectedSecret() { - return "Protected secret."; - } - - @GetMapping("/profile") - public String publicProfile() { - return "Public profile."; + @GetMapping("/api/.wellknown/config.json") + public String publicConfig() { + return "Public config."; } } ``` @@ -274,7 +279,7 @@ Request your protected API with the Access Token as the Bearer token in the Auth
   
-  {`curl --include '${appendPath(props.endpoint, '/secret')}' \\
+    {`curl --include '${appendPath(props.endpoint, '/api/profile')}' \\
 --header 'Authorization: Bearer '`}