Broadleaf Microservices
  • v1.0.0-latest-prod

Loyalty Group Token Enhancer

Table of Contents

Overview

Note
This example was created with AuthenticationServices 1.x. In 2.0, the enhancers have been renamed like *AccessTokenEnhancer. Additionally, the method TokenEnhancerUtil.getEnhanceableToken() has been removed. Instead, use JwtClaimsSet.Builder claims = context.getClaims(); to get the ClaimSet builder and add claims to it. See the other enhancers for details.
Warning
When creating your own TokenEnhancers, be sure to include sufficient debug logging. To avoid the possibility of sensitive data leaking, exceptions thrown by TokenEnhancers are minimally logged, making debugging difficult. For instance, if a TokenEnhancer throws an exception, a stack trace is not logged. Failure to add sufficient logging may make debugging very difficult.

In some cases, there may be a need to add additional claims to a token. This can be achieved via a TokenEnhancer. Achieving this is a two-step process.

Scenario

Let’s say we want to add a custom claim with a key of loyalty_groups and a value of platinum to a specific user. Assume a User may belong to multiple loyalty groups, and we want to add a new loyalty group without overwriting any existing groups.

1. Adding a User Attribute

First, if these are unique attributes on a per-user basis, it is recommended that these values be set on the User itself, via User#getAttributes, which returns a Map<String, Object>. The key in this map should be a unique String because on persistence any values already associated with that key are overwritten.

There are two ways to set attributes on a user:

  1. Via a POST to the endpoint /auth/user-attributes.

  2. Publishing a message via Kafka (or other messaging system) on the userAttributesOutput channel (recommended, if available).

See com.broadleafcommerce.auth.resource.domain.UserAttributesRequest for the payload structure.

2. Enhancing a Token with User Attributes

Next, we need an implementation of org.springframework.security.oauth2.provider.token.TokenEnhancer. When reading users, which most token enhancers will do, it is recommended to extend com.broadleafcommerce.auth.user.session.token.enhancer.AbstractUserTokenEnhancer. This class caches the User in the request scope to avoid reading the current User in every TokenEnhancer. This allows us to retrieve the current user with AbstractUserTokenEnhancer#getUser(OAuth2Authentication).

In addition to the above, for commonly performed operations, there is the utility class com.broadleafcommerce.auth.user.session.token.enhancer.TokenEnhancerUtil which contains several static utility methods for common operations performed when enhancing a token. We will be using these methods in the example below.

For the following example, let’s assume we have persisted a user attribute with the unique key/value pair of mycompany_loyalty_group : "platinum", and we want to supplement any existing loyalty_groups with this new value.

We would implement our token enhancer as follows:

public class LoyaltyGroupTokenEnhancer extends AbstractUserTokenEnhancer {

  public OAuth2AccessToken enhance(OAuth2AccessToken accessToken,
          OAuth2Authentication authentication) {
     if (hasAuthenticatedUser(authentication)) {
            // do nothing if no user authentication
            return accessToken;
     }

     // Call to AbstractUserTokenEnhancer#getUser to retrieve user stored in the current request scope
     User user = super.getUser(authentication);
     Object loyaltyGroup = user.getAttribute("mycompany_loyalty_group");
     if(loyaltyGroup == null) {
         return accessToken;
     }

     // Convenience method. Allows us to add new values to the map returned
     // by DefaultOAuth2AccessToken#getAdditionalInformation, as the default
     // map is read-only.
     DefaultOAuth2AccessToken enhanceableToken = TokenEnhancerUtil.getEnhanceableToken(accessToken);
     Map<String, Object> claims = enhanceableToken.getAdditionalInformation();
     // Enhance our token with the new claim without overwriting existing claims
     Collection<String> loyaltyGroups = claims.getAttribute("loyalty_groups");
     if(loyaltyGroups == null)  {
         loyaltyGroups = new HashSet<>();
     }
     loyaltyGroups.add(loyaltyGroup);

     claims.put("loyalty_groups", loyaltyGroups);
     return enhancedToken;
  }

}

3. Register the Token Enhancer as a Spring bean

@Bean
public LoyaltyGroupTokenEnhancer loyaltyGroupTokenEnhancer() {
  return new LoyaltyGroupTokenEnhancer();
}

Now we should see that our new "platinum" loyalty group is present on the User’s access token. Note that there is no hard requirement that User#attributes be used. This could be an external service call, or even a new first class field on a User. However, if the values are rarely updated, it’s recommended to simply store values as attributes on the User entity to avoid the extra work of adding new database columns. Keep this in mind when evaluating how these values should be stored, and be aware values stored on the User attributes map are not queryable.