I have configured the resource server which verify JWT token against auth server. In code bellow you can see my configuration which has defined issuer-uri (is URI from Auth0). If user is authenticated on my public client against Auth0, this client receive JWT token from Auth0. When I call resource server with token header, user is authorized, and resources are available, but SecurityContextHolder contains only base data parsed from JWT, and not whole information about user. I have available userinfo endpoint from Auth0 which provides user's name, picture, email, etc.
My question is if I can set this user info endpoint in my resource server, to fetch this information automatically or what is the best way to do that? I would like to have this informations in SecurityContextHolder or at least user's email and user's name.
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http.authorizeRequests().anyRequest().permitAll()
.and()
.oauth2ResourceServer().jwt();
return http.build()
}
and JWT decoder bean
@Bean
fun jwtDecoder(): JwtDecoder? {
val jwtDecoder = JwtDecoders.fromOidcIssuerLocation<JwtDecoder>(issuer) as NimbusJwtDecoder
val audienceValidator: OAuth2TokenValidator<Jwt> = AudienceValidator(audience)
val withIssuer = JwtValidators.createDefaultWithIssuer(issuer)
val withAudience: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(withIssuer, audienceValidator)
jwtDecoder.setJwtValidator(withAudience)
return jwtDecoder
}
File application.properties
spring.security.oauth2.resourceserver.jwt.issuer-uri=my-domain.com
spring.security.oauth2.resourceserver.jwt.audience=my-audience
EDIT This is payload of JWT received from Auth0
{
"iss": "https://dev-abcdefgh.us.auth0.com/",
"sub": "google-oauth2|353335637216442227159",
"aud": [
"my-audience",
"https://dev-3ag8q43b.us.auth0.com/userinfo"
],
"iat": 1663100248,
"exp": 1663186648,
"azp": "m01yBdKdQd5erBxriQde24ogfsdAsYvD",
"scope": "openid profile email"
}
Do not call user endpoint when building the security-context for a request on a resource-server with JWT decoder.
Auth0 can issue JWT access-token and JWTs can be decoded / validated on the resource-server without a round trip to the authorization-server.
Calling the user-info endpoint for each and every of your resource-server incoming request would be a drop in latency (and efficiency).
In Auth0 management console, go to Auth Pipeline -> Rules and click Create to add a rule like:
function addUserInfoToAccessToken(user, context, callback) {
context.accessToken['https://stackoverflow.com/user'] = user;
return callback(null, user, context);
}
Et voilà! You now have a https://stackoverflow.com/user private claim in access-tokens. You can (should?) narrow to the user attributes you actually need in your resource-server (what is accessed in your @PreAuthorize expressions for instance).
JwtAuthenticationToken, Spring-security default Authentication implementation for resource-servers with JWT decoder, exposes all of the access-token claims, including the private one we added.
Here is a complete sample using the private claim from above in different ways (security expression, and inside @Controller method), but I recommand you go through this first 3 of those tutorials I wrote, you'll find useful tips to make the best usage of private claims in spring-security:
@SpringBootApplication
public class Auth0DemoApplication {
public static void main(String[] args) {
SpringApplication.run(Auth0DemoApplication.class, args);
}
@RestController
@RequestMapping("/access-token-user-info")
@PreAuthorize("isAuthenticated()")
public static class DemoController {
@GetMapping("/{nickname}")
@PreAuthorize("#nickname eq authentication.tokenAttributes['https://stackoverflow.com/user']['nickname']")
public Map<String, Object> getGreeting(@PathVariable String nickname, JwtAuthenticationToken auth) {
return auth.getToken().getClaimAsMap("https://stackoverflow.com/user");
}
}
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public static class SecurityConf {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.oauth2ResourceServer().jwt();
// Enable and configure CORS
http.cors().configurationSource(corsConfigurationSource());
// State-less session (state in access-token only)
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Disable CSRF because of state-less session-management
http.csrf().disable();
// Return 401 (unauthorized) instead of 403 (redirect to login) when authorization is missing or invalid
http.exceptionHandling().authenticationEntryPoint((request, response, authException) -> {
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"Restricted Content\"");
response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
});
return http.build();
}
private CorsConfigurationSource corsConfigurationSource() {
// Very permissive CORS config...
final var configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("*"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setExposedHeaders(Arrays.asList("*"));
// Limited to API routes (neither actuator nor Swagger-UI)
final var source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/access-token-user-info/**", configuration);
return source;
}
}
}
With just this property:
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://dev-ch4mpy.eu.auth0.com/
And that pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.c4soft</groupId>
<artifactId>auth0-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>auth0-demo</name>
<description>Demo project for Spring Boot and Auth0 with user-data in access-token</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
And now the output of a call to http://localhost:8080/access-token-user-info/ch4mp with Postman (and an access-token for ch4mp):
{
"clientID": "...",
"email_verified": true,
"roles": [
"PLAYER"
],
"created_at": "2021-08-16T21:03:02.086Z",
"picture": "https://s.gravatar.com/avatar/....png",
"global_client_id": "...",
"identities": [
{
"isSocial": false,
"provider": "auth0",
"user_id": "...",
"connection": "Username-Password-Authentication"
}
],
"updated_at": "2022-09-26T20:53:08.957Z",
"user_id": "auth0|...",
"permissions": [
"solutions:manage"
],
"name": "[email protected]",
"nickname": "ch4mp",
"_id": "...",
"persistent": {},
"email": "[email protected]",
"last_password_reset": "2022-09-24T16:39:00.152Z"
}
Do not use ID tokens as access-tokens, this is a worst practice. Authorization-server emmits different kind of tokens for different usages:
Bearer Authorization header in the requests sent to the right audience. In the case you have different audience, your client must maintain different access-tokens (for instance one for "your" API and other ones for Google, Facebook or whatever other API your client consumes directly).If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With