Reputation: 5251
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"
}
Upvotes: 7
Views: 2892
Reputation: 12659
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).Upvotes: 4
Reputation: 16469
You have to do little bit changes to make it working.
I will explain by step - by - step :
I have created an account and have registered a regular web application with the name test-app in the Auth0 portal.
Now, I took help of the resource links provided for Auth0 client and Resource Server by Auth0 to setup Spring boot app and these are given below.
Now, I will explain through use cases.
I have created a Auth0 spring boot client (separate project).
application.properties :
server:
port: 3000
spring:
security:
oauth2:
client:
registration:
auth0:
client-id: <<id>>
client-secret: <<secret>>
scope:
- openid
- profile
- email
provider:
auth0:
issuer-uri: https://<<name>>.us.auth0.com/
Note: You can find client id, client secret and issuer uri from Applications -> Open the app -> Settings.
Now, I need to extract ID token which contains the user info, so I have created a sample controller and have used OidcUser
to get that token:
@RestController
public class Resource {
@GetMapping("/token")
public void profile(@AuthenticationPrincipal OidcUser oidcUser) {
System.out.println(oidcUser.getIdToken().getTokenValue());
}
}
As soon as I run the server and send the request to the /token
, it will first redirect to the Auth0 login page. I have used my Google account to login and after successful login, it prints the ID JWT token.
Note: This client project is just show how I got the ID token. Not to confuse that resource server is also a client.
Now, coming to resource server, I have created a resource server Spring Boot App (separate project).
application.properties :
server:
port: 3010
auth0:
audience: https://<<name>>.auth0.com/api/v2/
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://<<name>>.us.auth0.com/
audiences:
- https://<<name>>.us.auth0.com/api/v2/
SecurityConfig (You don't need to add any extra validator, i.e remove the AudienceValidator):
@EnableWebSecurity
public class SecurityConfig {
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuer;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll().and().oauth2ResourceServer().jwt(jwt -> jwtDecoder());
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) JwtDecoders.fromOidcIssuerLocation(issuer);
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
jwtDecoder.setJwtValidator(withIssuer);
return jwtDecoder;
}
}
A sample Controller to show my case :
@RestController
public class ProfileController {
@GetMapping("/profile")
public void profile() {
Jwt user = (Jwt) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
System.out.println(user.getClaimAsString("email") + " " + user.getClaimAsString("name"));
}
}
As soon as I have run the server, hit the uri /profile
with Bearer token as ID token like this :
The Spring Security is consuming this Jwt
automatically and setting the Principal
at this Jwt
Object and you can extract this via SecurityContextHolder
.
Note: In the claims map, you have all of your user info.
Output :
Note: ID token are not safe to be used in any cases. It can be used only in the case where you want to show/get user profile data but it should be avoided for all other use cases.
Upvotes: 1
Reputation: 462
I would like to have this informations in SecurityContextHolder or at least user's email and user's name.
Have you seen what's inside your jwt token? Did you add openid scope in your authentication process? if so there should be an IdToken in your auth server response json body, inside IdToken jwt token claim there are various information about the user's data such user's name and email. Other user attributes can also be added by adding custom claim to your jwt token, after adding those claims then you can try to access it via SecurityContextHolder.
Upvotes: 1