From 0995b4257cb70b32e5ba378288d5e7e1e5149d06 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Fri, 15 Sep 2023 18:32:36 +0800 Subject: [PATCH] feat(console): add api guide for spring boot (#4517) --- .../docs/guides/api-spring-boot/README.mdx | 309 ++++++++++++++++++ .../docs/guides/api-spring-boot/index.ts | 9 + .../docs/guides/api-spring-boot/logo.svg | 10 + .../console/src/assets/docs/guides/index.ts | 8 + 4 files changed, 336 insertions(+) create mode 100644 packages/console/src/assets/docs/guides/api-spring-boot/README.mdx create mode 100644 packages/console/src/assets/docs/guides/api-spring-boot/index.ts create mode 100644 packages/console/src/assets/docs/guides/api-spring-boot/logo.svg 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 new file mode 100644 index 000000000..161da637a --- /dev/null +++ b/packages/console/src/assets/docs/guides/api-spring-boot/README.mdx @@ -0,0 +1,309 @@ +import Tabs from '@mdx/components/Tabs'; +import TabItem from '@mdx/components/TabItem'; +import InlineNotification from '@/ds-components/InlineNotification'; +import Steps from '@/mdx-components/Steps'; +import Step from '@/mdx-components/Step'; +import { appendPath } from '@silverhand/essentials'; + + + + + +With [Spring Initializr](https://start.spring.io/), you can quickly start a Spring Boot project. +Use the following options: + +- Gradle Project +- Language: Java +- Spring Boot: 2.7.2 or above + +Generate and open the project. + + + + + +Add the dependencies to your Gradle project build file `build.gradle`: + +```groovy +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' +} +``` + + + 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. + + + + + + +All tokens are issued by the [issuer](https://openid.net/specs/openid-connect-core-1_0.html#IssuerIdentifier), +and signed with [JWK](https://datatracker.ietf.org/doc/html/rfc7517) +(See [JWS](https://datatracker.ietf.org/doc/html/rfc7515) for more details). + +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. +

+ +An example of the response: + +
+  
+{`{
+  // ...
+  "issuer": "${appendPath(props.endpoint, '/oidc')}",
+  "jwks_uri": "${appendPath(props.endpoint, '/oidc/jwks')}"
+  // ...
+}`}
+  
+
+ +
+ + + +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
+server:
+  port: 3000
+
+logto:
+  audience: ${props.audience}
+
+spring:
+  security:
+    oauth2:
+      resourceserver:
+        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. +- `spring.security.oauth2.resourceserver.jwt.issuer-uri`: The iss claim value and the issuer URI in the JWT issued by Logto. +- `spring.security.oauth2.resourceserver.jwt.jwk-set-uri`: Spring Security uses this URI to get the authorization server's public keys to validate JWT signatures. + +
+ + + +Provide your own `AudienceValidator` class that implements the `OAuth2TokenValidator` interface to validate whether the required audience is present in the JWT. + +```java +// path/to/project/src/main/java/io/logto/springboot/sample/validator/AudienceValidator.java +package io.logto.springboot.sample.validator; + +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; + +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) { + this.audience = audience; + } + + @Override + public OAuth2TokenValidatorResult validate(Jwt jwt) { + if (!jwt.getAudience().contains(audience)) { + return OAuth2TokenValidatorResult.failure(oAuth2Error); + } + + return OAuth2TokenValidatorResult.success(); + } +} +``` + + + For 🔐 RBAC, scope validation is also required. + + + + + + +Spring Security makes it easy to configure your application as a resource server and validate the JWT from the Bearer Token in the request header. + +You need to provide instances of `JwtDecoder` and `SecurityFilterChain` (as Spring beans), and add the `@EnableWebSecurity` annotation. + +```java +// path/to/project/src/main/java/io/logto/springboot/sample/configuration/SecurityConfiguration.java +package io.logto.springboot.sample.configuration; + +import com.nimbusds.jose.JOSEObjectType; +import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier; +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.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; + +@EnableWebSecurity +public class SecurityConfiguration { + + @Value("${logto.audience}") + private String audience; + + @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") + private String issuer; + + @Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") + private String jwksUri; + + @Bean + public JwtDecoder jwtDecoder() { + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwksUri) + // The decoder should support the token type: Access Token + JWT. + .jwtProcessorCustomizer(customizer -> customizer.setJWSTypeVerifier( + new DefaultJOSEObjectTypeVerifier(new JOSEObjectType("at+jwt")))) + .build(); + + jwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>( + new AudienceValidator(audience), + new JwtIssuerValidator(issuer), + new JwtTimestampValidator())); + + return jwtDecoder; + } + + @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() + ); + return http.build(); + } +} +``` + + + + + +Add a controller to provide the protected and public APIs: + +```java +// path/to/project/src/main/java/io/logto/springboot/sample/controller/ProtectedController.java +package io.logto.springboot.sample.controller; + +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +// Only allow all origins for the sample. +// (Production applications should configure CORS carefully.) +@CrossOrigin(origins = "*") +@RestController +public class ProtectedController { + + @GetMapping("/") + public String protectedRoot() { + return "Protected root."; + } + + @GetMapping("/secret") + public String protectedSecret() { + return "Protected secret."; + } + + @GetMapping("/profile") + public String publicProfile() { + return "Public profile."; + } +} +``` + + + + + +Build and run your Spring Boot web application, e.g. execute the bootRun Gradle task. + + + + + +```bash +./gradlew bootRun +``` + + + + + +```bash +gradlew.bat bootRun +``` + + + + + +Request your protected API with the Access Token as the Bearer token in the Authorization header, e.g. execute the `curl` command. + +
+  
+  {`curl --include '${appendPath(props.endpoint, '/secret')}' \\
+--header 'Authorization: Bearer '`}
+  
+
+ +If successful, you will get a response with 200 status: + +```bash +HTTP/1.1 200 +... + +``` + +Otherwise, you will get a response with 401 status like this: + +```bash +HTTP/1.1 401 +... +WWW-Authenticate: Bearer error="invalid_token", error_description="An error occurred while attempting to decode the Jwt: Signed JWT rejected: Invalid signature", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1" +... +``` + +
+ + + +- [Protect your API](https://docs.logto.io/docs/recipes/protect-your-api/) +- [Spring Security OAuth 2.0 Resource Server](https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/index.html) +- [Spring Security Architecture](https://spring.io/guides/topicals/spring-security-architecture) + + + +
diff --git a/packages/console/src/assets/docs/guides/api-spring-boot/index.ts b/packages/console/src/assets/docs/guides/api-spring-boot/index.ts new file mode 100644 index 000000000..5871f5afa --- /dev/null +++ b/packages/console/src/assets/docs/guides/api-spring-boot/index.ts @@ -0,0 +1,9 @@ +import { type GuideMetadata } from '../types'; + +const metadata: Readonly = Object.freeze({ + name: 'Spring Boot', + description: 'Spring Boot is an open source Java-based framework used to create a micro Service.', + target: 'API', +}); + +export default metadata; diff --git a/packages/console/src/assets/docs/guides/api-spring-boot/logo.svg b/packages/console/src/assets/docs/guides/api-spring-boot/logo.svg new file mode 100644 index 000000000..55b425680 --- /dev/null +++ b/packages/console/src/assets/docs/guides/api-spring-boot/logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/console/src/assets/docs/guides/index.ts b/packages/console/src/assets/docs/guides/index.ts index e350a4f4c..354a1eb6a 100644 --- a/packages/console/src/assets/docs/guides/index.ts +++ b/packages/console/src/assets/docs/guides/index.ts @@ -3,6 +3,7 @@ import { lazy } from 'react'; import apiExpress from './api-express/index'; +import apiSpringBoot from './api-spring-boot/index'; import m2mGeneral from './m2m-general/index'; import nativeAndroidJava from './native-android-java/index'; import nativeAndroidKt from './native-android-kt/index'; @@ -173,6 +174,13 @@ const guides: Readonly = Object.freeze([ Component: lazy(async () => import('./api-express/README.mdx')), metadata: apiExpress, }, + { + order: Number.POSITIVE_INFINITY, + id: 'api-spring-boot', + Logo: lazy(async () => import('./api-spring-boot/logo.svg')), + Component: lazy(async () => import('./api-spring-boot/README.mdx')), + metadata: apiSpringBoot, + }, ]); export default guides;