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, 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
:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
}
See Spring Security OAuth 2.0 Resource Server and Spring Security Architecture for more details.
All tokens are issued by the issuer, and signed with JWK (See JWS 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').href}
, 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.
{`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.
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<Jwt> {
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(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();
}
}
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.
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.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.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.web.DefaultSecurityFilterChain;
@Configuration
@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)
// 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<SecurityContext>(new JOSEObjectType("at+jwt"))))
.build();
jwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
new AudienceValidator(audience),
new JwtIssuerValidator(issuer),
new JwtTimestampValidator()));
return jwtDecoder;
}
@Bean
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();
}
}
Add a controller to provide the protected and public APIs:
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("/api/profile")
public String protectedProfile() {
return "Protected profile.";
}
@GetMapping("/api/.wellknown/config.json")
public String publicConfig() {
return "Public config.";
}
}
Build and run your Spring Boot web application, e.g. execute the bootRun Gradle task.
./gradlew bootRun
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, '/api/profile')}' \\
--header 'Authorization: Bearer '`}
If successful, you will get a response with 200 status:
HTTP/1.1 200
...
Otherwise, you will get a response with 401 status like this:
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"
...