DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Securing Verifiable Credentials With DPoP: A Spring Boot Implementation
  • MuleSoft OAuth 2.0 Provider: Password Grant Type
  • Spring OAuth Server: Token Claim Customization
  • How To Implement OAuth User Authentication in Next.js

Trending

  • Run Gemma 4 on Your Laptop: A Hands-On Guide to Google's Latest Open Multimodal LLM
  • Agentic Testing: Moving Quality From Checkpoint to Control Layer
  • S3 Vectors: How to Build a RAG Without a Vector Database
  • Securing Everything: Mapping the Right Identity and Access Protocol (OIDC, OAuth2, and SAML) to the Right Identity
  1. DZone
  2. Software Design and Architecture
  3. Security
  4. Spring OAuth Server: Authenticate User With user-details Service

Spring OAuth Server: Authenticate User With user-details Service

Explore the integration of spring-oauth-server with user-details service, authenticate, and create User. Customize token claims with user-details.

By 
Naveen Maanju user avatar
Naveen Maanju
·
Nov. 14, 23 · Tutorial
Likes (1)
Comment
Save
Tweet
Share
6.5K Views

Join the DZone community and get the full member experience.

Join For Free

In this article, we will see how we can customize the authentication where user details are fetched from another component/service over HTTP. Store user details as Principal and use them later while creating tokens to customize the claims in JWT (the scope of this article covers two flows only: client-credentials and code flow).

The code is available on GitHub.

To achieve this, the changes below would be required.

  1. Password encoder
  2. Service/Client to fetch user details from a service
  3. UserDetails entity
  4. Token customizers

Password Encoder

A password encoder is required to encode the password provided while authentication/login to verify/validate the secret against the one stored in the DB (while registering or changing the password) as encoded.

Refer to D3PasswordEncoder for more.

Service/Client To Fetch UserDetails

A bean/service is required to provide the custom UserDetails. This service can provide user details as hard-coded, from in-memory storage, or by calling another service. In this example, we will focus on invoking another service (user-detail-service).

The user detail service bean in oauth-server implements the UserDetailsService provided by spring-security (as oauth-server is built on top of spring-security).

Java
 
@Service
public class D3UserDetailsService implements UserDetailsService {

  private final WebClient webClient;

  public D3UserDetailsService(@Value("${user.details.service.base.url}") String userServiceBaseUrl) {
    webClient = WebClient.builder().baseUrl(userServiceBaseUrl).build();

  }

  public UserDetails loadUserByUsername(String username) {
    D3User user = webClient.get()
        .uri(uriBuilder -> uriBuilder.path("/users").path("/{username}").build(username))
        .retrieve()
        .onStatus(httpStatusCode -> httpStatusCode.isSameCodeAs(HttpStatus.NOT_FOUND),
            clientResponse -> Mono.error(new D3Exception("Bad credentials")))
        .bodyToMono(D3User.class).block(
            Duration.ofSeconds(2));

    return new D3UserDetails(user.userId(), user.username(), user.password(), getAuthorities(user.roles()), user.ssn(),
        user.email(), user.isPasswordChangeRequired(), user.roles());
  }


  private List<GrantedAuthority> getAuthorities(List<String> roles) {
    List<GrantedAuthority> authorities = new ArrayList<>(roles.size());
    for (String role : roles) {
      authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
    }
    return authorities;
  }

  @JsonIgnoreProperties(ignoreUnknown = true)
  @Builder
  public record D3User(@JsonProperty("id") Integer userId, @JsonProperty("userName") String username,
                       String password, List<String> roles, String ssn, String email,
                       boolean isPasswordChangeRequired) {

  }
  
}


UserDetails Entity

A UserDetails entity can (not must, unless you want to add a few more details to the authenticated user's context) be defined as:

Java
 
@Getter
public class D3UserDetails extends User {

  private final Integer userId;
  private final boolean isPasswordChangeRequired;
  private final List<String> roles;
  private final String ssn;
  private final String email;

  public D3UserDetails(Integer userId, String username, String password, List<GrantedAuthority> authorities,
      String ssn, String email, boolean isPasswordChangeRequired, List<String> roles) {
    super(username, password, authorities);
    this.userId = userId;
    this.ssn = ssn;
    this.email = email;
    this.isPasswordChangeRequired = isPasswordChangeRequired;
    this.roles = roles;
  }
}


This D3UserDetails entity extends the Spring Security User entity and provides additional attributes as well.

Token Customizers

Token customizers are required to provide additional attributes/claims for access_token:

Self-Contained JWT

If the access_token format is self-contained, then a customizer implementing Auth2TokenCustomizer<JwtEncodingContext> is required.

Java
 
public class OAuth2JWTTokenCustomizer implements OAuth2TokenCustomizer<JwtEncodingContext> {

  private static final Consumer<JwtEncodingContext> AUTHORIZE_CODE_FLOW_CUSTOMIZER = (jwtContext) -> {
    if (AUTHORIZATION_CODE.equals(jwtContext.getAuthorizationGrantType()) && ACCESS_TOKEN.equals(
        jwtContext.getTokenType())) {
      UsernamePasswordAuthenticationToken authenticatedUserToken = jwtContext.getPrincipal();
      D3UserDetails userDetails = (D3UserDetails) authenticatedUserToken.getPrincipal();
      Map.of("userId", userDetails.getUserId(),
              "username", userDetails.getUsername(),
              "isPasswordChangeRequired", userDetails.isPasswordChangeRequired(),
              "roles", userDetails.getRoles(),
              "ssn", userDetails.getSsn(),
              "email", userDetails.getEmail())
          .forEach((key, value) -> jwtContext.getClaims().claim(key, value));
    }
  };

  private static final Consumer<JwtEncodingContext> CLIENT_CREDENTIALS_FLOW_CUSTOMIZER = (jwtContext) -> {
    if (CLIENT_CREDENTIALS.equals(jwtContext.getAuthorizationGrantType()) && ACCESS_TOKEN.equals(
        jwtContext.getTokenType())) {
      OAuth2ClientCredentialsAuthenticationToken clientCredentialsAuthentication = jwtContext.getAuthorizationGrant();
      Map<String, Object> additionalParameters = clientCredentialsAuthentication.getAdditionalParameters();
      additionalParameters.forEach((key, value) -> jwtContext.getClaims().claim(key, value));
    }
  };

  private final Consumer<JwtEncodingContext> jwtEncodingContextCustomizers = AUTHORIZE_CODE_FLOW_CUSTOMIZER.andThen(
      CLIENT_CREDENTIALS_FLOW_CUSTOMIZER);

  @Override
  public void customize(JwtEncodingContext context) {
    jwtEncodingContextCustomizers.accept(context);
  }
}


As the client-credential flow is always self-contained, we have to add support for it in JWTToken along with code flow. In the case of code flow, we authenticate the user and use the user details fetched from UserService as additional claims in JWT. Whereas in the case of client-credentials flow, additional parameters are provided as request parameters.

Opaque Token

If the access_token format is reference, then a customizer implementing OAuth2TokenCustomizer<OAuth2TokenClaimsContext> is required.

Java
 
@Component
public class OAuth2OpaqueTokenIntrospectionResponseCustomizer implements
    OAuth2TokenCustomizer<OAuth2TokenClaimsContext> {

  private static final Consumer<OAuth2TokenClaimsContext> INTROSPECTION_TOKEN_CLAIMS_CUSTOMIZER = (claimsContext) -> {
    if (AUTHORIZATION_CODE.equals(claimsContext.getAuthorizationGrantType()) && ACCESS_TOKEN.equals(
        claimsContext.getTokenType())) {
      UsernamePasswordAuthenticationToken authenticatedUserToken = claimsContext.getPrincipal();
      D3UserDetails userDetails = (D3UserDetails) authenticatedUserToken.getPrincipal();
      Map.of("userId", userDetails.getUserId(),
              "username", userDetails.getUsername(),
              "isPasswordChangeRequired", userDetails.isPasswordChangeRequired(),
              "roles", userDetails.getRoles(),
              "ssn", userDetails.getSsn(),
              "email", userDetails.getEmail())
          .forEach((key, value) -> claimsContext.getClaims().claim(key, value));
    }
  };

  private final Consumer<OAuth2TokenClaimsContext> claimsContextCustomizer = INTROSPECTION_TOKEN_CLAIMS_CUSTOMIZER;

  @Override
  public void customize(OAuth2TokenClaimsContext jwtContext) {
    claimsContextCustomizer.accept(jwtContext);
  }
}


As the reference token is associated with code flow and after successful authentication when code is exchanged for the token, the access_token so issued by the authorization server will not be JWT, but a reference. This reference should be exchanged for access_token with user details claims and other claims using the introspection endpoint. A working function test can be referred to here.

A working example is available on GitHub here.

In the case of self-contained, at the end of code flow, the access_token will be in the form JWT with all additional claims including UserDetails added through customizer. Whereas in the case of opaque tokens (reference), an introspection call is required to fetch the UserDetails in the form of claims in the response.

What Does the Response Look Like?

You can verify it through the test added on GitHub: it has two test methods covering both scenarios.

Self-Contained JWT

Code Flow Token Response

JSON
 
{
   "access_token":"eyJraWQiOiIxNzdjMzA1MC1lMGY2LTQ4NDctYjJiNy02NTY2ZDVlZGZiMWUiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJkM3VzZXIiLCJyb2xlcyI6WyJhZG1pbiIsInVzZXIiXSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo2MDYwIiwiaXNQYXNzd29yZENoYW5nZVJlcXVpcmVkIjp0cnVlLCJ1c2VySWQiOjEyMywic3NuIjoiMTk3NjExMTE5ODc3IiwiYXVkIjoic3ByaW5nLXRlc3QiLCJuYmYiOjE2OTkzNDcyODMsInNjb3BlIjpbIm9wZW5pZCIsInByb2ZpbGUiLCJlbWFpbCJdLCJleHAiOjE2OTkzNDc1ODMsImlhdCI6MTY5OTM0NzI4MywiZW1haWwiOiJ0ZXN0LXVzZXJAZDNzb2Z0dGVjaC5jb20iLCJ1c2VybmFtZSI6ImQzdXNlciJ9.RQiLWmGf9_rV4UfKzKomEhuJrncG08a2F34mN-gPDw7vK2csRPGMMDRYh2Gm0Eh-n3JRTaJ9_twdPQG9BgQifKiubPsM_etxpxKLLfQHoTfqzguiP8D53FyXLB9xwhvAgKH0KWLOSRxl-bdZsctpVZpqrMTPZtfdlt7tqcl71tGDY-7Nri76Kod39kyVcKEAuLNNZKt4fhn8tCLUA64jKfmKPM3afmAdvf0PlEwgwqhGhojxtCLnYNtzuO_VQheTaQvZxrzcXw3gNRnO4vppedAyG1gmUV44l4u7cXdhG-vGc1ItU45PSg3EaG7BtHU1axKu3qHB8C7mHAhk3zVuUA",
   "refresh_token":"t9U3CDejVC2k_eNtyvM23RTN3ePpS9x8b8_pVrD-U-ivLij0dWt9NZVO9wn-kIsyr89Yj-fBFpH8BFZoMUIqGI_wZSmKgYqpO0SmNE-C1_hW8DVLqT8zQ7PkhF_Gil7N",
   "scope":"openid profile email",
   "token_type":"Bearer",
   "expires_in":299
}


AccessToken JWT claims will look like:

JSON
 
{
  "sub": "d3user",
  "roles": [
    "admin",
    "user"
  ],
  "iss": "http://localhost:6060",
  "isPasswordChangeRequired": true,
  "userId": 123,
  "ssn": "197611119877",
  "aud": "spring-test",
  "nbf": 1699347283,
  "scope": [
    "openid",
    "profile",
    "email"
  ],
  "exp": 1699347583,
  "iat": 1699347283,
  "email": "[email protected]",
  "username": "d3user"
}


We can see that the JWT body contains additional claims such as:

  1. roles
  2. isPasswordChangeRequired
  3. userId
  4. ssn
  5. email
  6. username

We provided these in Customizer for the token. Similarly, you can add as many claims as you want.

Introspection Response Using access_token

JSON
 
{
   "active":true,
   "client_id":"spring-test",
   "iat":1698757155,
   "exp":1698760755
}


The default response for /oauth2/introspect will just return the status of access_token. And it can be customized as well if required.  

Opaque Token

Code Flow: Code-Exchange Response

JSON
 
{
   "access_token":"vbHFMLGQPmqAWWOzjLoYNu_RG1jBHc7oifI9Hl9N1eCyG3jdzTgAoN8YXAAK-GfEy1CUhokTAnM2aC4GsDe07OgPBpI_sAGHP60pQgbTDTyBUJj2jO1inIi0FoCpmPcj",
   "refresh_token":"Rj8CpnQexjtFJzCPFJUmhKGVmgdFAJ6RLMB_h6SwYgDItPLwSu6AR7CZ3WpIEQthm7pGEpis7NlrarvIHX5YjwBX6wGwWpwfnIKVSa0OJYJqhFsZfFvOmn8sypi4DS4b",
   "scope":"openid profile email",
   "token_type":"Bearer",
   "expires_in":299
}


At the end of the code flow, you will have the JSON response encapsulating access_token, refresh_token, scope, token_type and expires_in. 

To pull the claims of the authenticated user, we have to invoke the /oauth2/introspect endpoint against spring-oauth-server.

Introspection Response Using access_token Without Customizer

JSON
 
{
   "active":true,
   "sub":"d3user",
   "aud":[
      "spring-reference"
   ],
   "nbf":1698755697,
   "scope":"openid profile email",
   "iss":"http://localhost:6060",
   "exp":1698755997,
   "iat":1698755697,
   "jti":"2b4165c0-68f3-4e3d-b67e-d50c3f7b6110",
   "client_id":"spring-reference",
   "token_type":"Bearer"
}


Without a customizer, it has all default claims like status "active" and subject (sub) for the user authenticated in code flow. 

Introspection Response Using access_token With Customizer

JSON
 
{
   "active":true,
   "sub":"d3user",
   "roles":[
      "admin",
      "user"
   ],
   "iss":"http://localhost:6060",
   "isPasswordChangeRequired":true,
   "userId":123,
   "ssn":"197611119877",
   "aud":[
      "spring-reference"
   ],
   "nbf":1698755588,
   "scope":"openid profile email",
   "exp":1698755888,
   "iat":1698755588,
   "operatorId":"197611119877",
   "jti":"c0560938-c413-44f7-a01b-9cbc119eae58",
   "email":"[email protected]",
   "username":"d3user",
   "client_id":"spring-reference",
   "token_type":"Bearer"
}


With customizer, the access_token will have additional claims like:

  1. roles
  2. isPasswordChangeRequired
  3. userId
  4. ssn
  5. operatorId
  6. email
  7. username

Note: If you are using Spring Security in your service, then introspection will be taken care of by the security layer. I will cover Spring Security with oauth2-resource-server in detail in a separate article.

Spring Security authentication Data Types

Opinions expressed by DZone contributors are their own.

Related

  • Securing Verifiable Credentials With DPoP: A Spring Boot Implementation
  • MuleSoft OAuth 2.0 Provider: Password Grant Type
  • Spring OAuth Server: Token Claim Customization
  • How To Implement OAuth User Authentication in Next.js

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook