Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fetch user information in Resource Server

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"
}
like image 658
Denis Stephanov Avatar asked Aug 04 '26 20:08

Denis Stephanov


1 Answers

Note On Efficiency

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).

Add User Info to Auth0 Access Tokens

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).

Complete Resource Server

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"
}

Note on Tokens

Do not use ID tokens as access-tokens, this is a worst practice. Authorization-server emmits different kind of tokens for different usages:

  • access-token: destined to resource-server. It should be very short lived (minutes) so that if leaked or revoked, the consequences are limited. Clients should:
    • just use it as 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).
    • not try to decode access-tokens, it is a contract between authorization and resource servers and they can decide to change the format at any moment (breaking the client if it expects to "understand" that token)
  • ID token: destined to client. Such tokens aim at communicating signed user data. As it is generally quite long lived, the consequences of it being leaked could be a real problem if used for access-control. Read article linked earlier for more reasons why not to use it for access-control.
  • refresh-token: long lived, to be used by client only and sent to authorization-server only. Authorization-server should carefully control the origin of tokens refreshing requests and clients be very careful with who they send such tokens to (consequences of a leak can be dramatic).
like image 116
ch4mp Avatar answered Aug 07 '26 12:08

ch4mp



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!