Denis Stephanov
Denis Stephanov

Reputation: 5251

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

Upvotes: 7

Views: 2892

Answers (3)

ch4mp
ch4mp

Reputation: 12659

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

Upvotes: 4

Anish B.
Anish B.

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.

  1. Auth0 Client Spring Boot App Quick Start
  2. Auth0 Resource Server Spring Boot App Quick Start

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.

enter image description here

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 :

enter image description here

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.

enter image description here

Output :

enter image description here

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

Aleson
Aleson

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.

Reference link

Upvotes: 1

Related Questions