Basit-Mahmood Ahmed has provided a nice example of adding a custom grant to Spring Authorization Server, providing a replacement for the "resource owner" grant removed from the OAuth 2.1 standard. I was able to leverage that for providing our own resource owner implementation. At work we've needed to create several types of custom grants, thankfully what started off as perplexing to implement, due to repetition became rather routine. Best starting advice I can to examine the out-of-the-box provided grant types and follow along with them. The Reference Guide of course and YouTube videos are also valuable, for example Getting Started with Spring Authorization Server and Configuring and Extending Spring Authorization Server.
For each custom grant type to support under Spring Auth Server, I've normally found five extra source files needed, as well as adjusting a couple of others. Most classes are limited in responsibilities helping keep their creation straightforward. Providing links to Basit-Mahmood's example where applicable, as well as some unrelated additional code samples:
The custom grant Token class (example): Extending OAuth2AuthorizationGrantAuthenticationToken, this class holds the properties used by the Provider (discussed below) to authenticate the client. For the resource owner grant, it would have username and password. For a grant based on incoming IP Address, it would be an IP address string. This class is also a good place to define the custom token grant_type parameter used in the OAuth2 token request.
The custom grant Converter class (example): This class takes the incoming HttpServletRequest and, by reading its properties, creates an instance of the Token class. Parameter validation for obvious shortcomings (missing param values, etc.) are good to do here, to help keep the Provider uncluttered.
The Provider class (example): This class takes the Token created by the Converter and authenticates and authorizes the grant. In general, there are two parts to this: authentication of the token, frequently handled by two other classes, discussed below, followed by construction of the JWT, partly in the Provider and partly in the OAuth2TokenCustomizer discussed below.
A token to represent the resource owner. This class will extend from AbstractAuthenticationToken, and will be used both to authenticate a user and to represent the user after authentication.
package ...; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; ... public class MyInnerAuthenticationToken extends AbstractAuthenticationToken { private final MyAccessUser myAccessUser; // constructor for user-to-validate public MyInnerAuthenticationToken(String propertyToCheck) { super(null); this.myAccessUser = new MyAccessUser(propertyToCheck); } // constructor for validated user public MyInnerAuthenticationToken(MyAccessUser myAccessUser, Collection extends GrantedAuthority> authorities) { super(authorities); this.myAccessUser = myAccessUser; super.setAuthenticated(true); // must use super, as we override } @Override public Object getPrincipal() { return this.myAccessUser; } @Override public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); super.setAuthenticated(false); } @Override public String getName() { return this.myAccessUser.getName(); } }
Another Provider to authenticate the above token. This would be called by the OAuth2 grant Provider during authentication. This Provider implements the standard authenticate(Authentication) method, returning a new Token populated with the principal and its authorities.
@Service public class MyInnerAuthenticationProvider implements AuthenticationProvider { private static final Logger LOGGER = LoggerFactory.getLogger(MyInnerAuthenticationProvider.class); @Autowired private MyAuthenticator authenticator; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { MyAccessUser unvalidatedUser = (MyAccessUser) authentication.getPrincipal(); String ip = unvalidatedUser.getIpAddress(); MyAccessUser validatedAccessUser = authenticator.checkIpAddress(ip); if (validatedIPAccessUser != null) { Collection<GrantedAuthority> authorities = authenticator.toGrantedAuthorities( validatedAccessUser.getPermissions()); return new MyInnerAuthenticationToken(validatedAccessUser, authorities); } else { LOGGER.warn("Could not validate user {}", unvalidatedUser); return null; } } @Override public boolean supports(Class<?> authentication) { return MyInnerAuthenticationToken.class.isAssignableFrom(authentication); } }
Spring Authorization Server allows for creating an OAuth2TokenCustomizer implementation for adding claims to a JWT common to multiple grant types. It should get picked up automatically by the framework's JwtGenerator. If you've created one, good to review at this stage any adjustments or additions that can be made to it as a result of the new custom grant.
@Component public class MyTokenCustomizer implements OAuth2TokenCustomizer{ public void customize(JwtEncodingContext context) { JwtClaimsSet.Builder claimsBuilder = context.getClaims(); claimsBuilder.claim(ENVIRONMENT_ID, environment); // Spring Auth Server's JwtGenerator does not provide JTI by default claimsBuilder.claim(JwtClaimNames.JTI, UUID.randomUUID().toString()); Authentication token = context.getPrincipal(); // can add principal-specific claims: if (token.getPrincipal() instanceof MySubjectClass chiefJwtSubject) { ... } } }
Once completed, now time to wire new grant support within the authorization server. To wire up the grant-level Converter and Provider, within a WebSecurityConfigurerAdapter subclass:
Listconverters = new ArrayList<>(); converters.add(resourceOwnerPasswordAuthenticationConverter); authorizationServerConfigurer .tokenEndpoint(tokenEndpoint -> { tokenEndpoint.accessTokenRequestConverter(new DelegatingAuthenticationConverter( converters)) // lots more providers .authenticationProvider(resourceOwnerPasswordAuthenticationProvider) } );
The mini-level Provider, used for the actual authentication of the User, can be configured separately as a @Bean:
@Bean public MyInnerAuthenticationProvider myInnerAuthenticationProvider() { return new MyInnerAuthenticationProvider(); }
Once developed, easy to test with Postman. Spring Auth Server uses an oauth2_registered_client table where the client_id and client_secret for clients are defined. Within Postman, Authorization tab, choose Basic Auth type and enter the client ID and secret as the credentials:
Then the new grant type can be tested with a POST call to the standard oauth/token endpoint using that grant_type:
Posted by Glen Mazza in Programming at 03:00AM May 05, 2023 | Comments[0]