Search code examples
javaspringoauth-2.0

How can OAuth2 resource server use endpoint to access public key when I add custom jwt details to the authorization server?


I customized the Authorization server to add custom details to the JSON Web token and intended that the resource server should access the verifier public key on the authorization server using an endpoint. But the OAuth2AuthenticationDetails.getDecodedDetails() returns null. My code structure is a shown below:

Custom token enhancer class:

public class CustomTokenEnhancer implements TokenEnhancer {
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken oauth2AccessToken,
            OAuth2Authentication oauth2Authentication) {
      var customToken = new DefaultOAuth2AccessToken(oauth2AccessToken);
   Map<String, Object> customInfo =  Map.of("generatedIn", "Year "+LocalDateTime.now().getYear());

customToken.setAdditionalInformation(customInfo);

return customToken;
}
}

The Authorization server class:

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter{
@Value("${password}")
    private String password;
    
@Value("${privateKey}")
    private String privateKey;
    
@Value("${alias}")
    private String alias;
    
//autowire the authentication manager here
    @Autowired
    private AuthenticationManager authenticationManager;
//provide clients' details
@Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.inMemory()
               .withClient("client")
               .secret("secret")
               .authorizedGrantTypes("password", "refresh_token")
               .scopes("read")
               .and()
               .withClient("resourceserver")
               .secret("resourceserversecret");
    }

 @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
     //Define a token enhancer chain here
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
         
        //Add the two token enhancer objects to a list
        var tokenEnhancers =
                List.of(new CustomTokenEnhancer(), jwtAccessTokenConverter());
        
        //Add the token enhancer list to the chain of token enhancer
        tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);
        
endpoints.authenticationManager(authenticationManager)
              .tokenStore(tokenStore())
              .tokenEnhancer(tokenEnhancerChain);
     
     }
@Override
        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
            
         /*
          * Configures the authorization server to expose and endpoint for 
           * public key for any authenticated 
          * request with valid client credentials
          */
         security.tokenKeyAccess("isAuthenticated()");
         
        }
       @Bean
        public TokenStore tokenStore() {
            return new JwtTokenStore(jwtAccessTokenConverter());
        }
        @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        
        var converter = new JwtAccessTokenConverter();
        
        
        KeyStoreKeyFactory keyStoreKeyFactory =
                new KeyStoreKeyFactory(
                        new ClassPathResource(privateKey),
                        password.toCharArray()
                        );
        
        converter.setKeyPair(keyStoreKeyFactory.getKeyPair(alias));
        return converter;
    }
}

Application.properties file:

password = somepassword
privateKey =key.jks
alias = somekey

The Resource server:


@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
}

Application.properties file:

server.port = 9090
security.oauth2.resource.jwt.key-uri=http://localhost:8080/oauth/token_key

security.oauth2.client.client-id=resourceserver
security.oauth2.client.client-secret=resourceserversecret

Protected endpoint on resource server:

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello(OAuth2Authentication authentication) {
        OAuth2AuthenticationDetails details =
                (OAuth2AuthenticationDetails) authentication.getDetails();

        return details.getDecodedDetails().toString();
    }
}

The result to the call details.getDecodedDetails().toString() printed null to the console when I make the curl request: curl -H "Authorization:Bearer e1yhrjkkkfk....." http://localhost:9090/hello.

However, the code behaves as I expected if I implement the Resource server like so:

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter{

    
    @Value("${publicKey}") //from the properties file
    private String publicKey;
    
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        
    }

    @Bean
    public TokenStore tokenStore() {
        
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    
    
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
    
    var converter = new OtherAccessTokenConverter(); //Handles the claims in our custom token. 
    converter.setVerifierKey(publicKey);
    
    return converter;
}

    
}

OtherAccessTokenConverter class:

public class OtherAccessTokenConverter extends JwtAccessTokenConverter {

    @Override
    public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
//Get the initial authenticated object
        var  authentication = super.extractAuthentication(map);
        
        //Add the custom details to the authentication object
        authentication.setDetails(map);
        
        //Return the authentication object
        return authentication;
        
    }

But I never wanted to have the public verifier key on the resource server, instead to be accessed through an endpoint. How do I go about it?


Solution

    • I presume based on your sample that you already have the spring-boot-starter-oauth2-resource-server dependency :
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    </dependency>
    

    What version of spring boot + spring security are you using ? Reason I ask is that because once you have the dependency above, you don't need to have explicitly include @EnableResourceServer.

    • All then you have to do is add the property, something like this:
    spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://openam.localtest.me:8080/openam/oauth2/realms/subrealm/connect/jwk_uri
    

    NOTE: I also noticed you are missing the spring. prefix in the properties file ?

    Those are the only two things that you have to do. No need to manually override the default provided by the Spring Boot's startup modules.

    What will happen then is that, once your Web / REST endpoint receives an HTTP Authorization header with a Bearer token ... AND ... the bearer token is a signed JWT, the resource server will then :

    • Query the URL specified by spring.security.oauth2.resourceserver.jwt.jwk-set-uri property.
    • From the set of keys in that URL, the resource server will then find a public key with the same kid as the kid in the signed JWT bearer token.
    • Theoretically then, this URL could be anywhere, it does not have to be served up by an authorisation server.
    • Once found, the resource server then verifies the signature of the signed JWT bearer token, either returning an HTTP 401 back to the client if the signed JWT could not be verified, or an HTTP 200 if it is successful ( assuming you don't have other enforcements in place ).

    I am sort of in the same journey, where I have to make the public key visible somehow listed into that URL defined in the resource server's application.properties file, pointing to ForgeRock AM's connect/jwk_uri ... but the public key I added into JWK Set of ForgeRock AM's trusted JWT issuer does not appear in the connect/jwk_uri URL when queried from a browser / curl.

    It seems like that Spring Security's Resource Server support for JWT bearer token authorisation, as highlighted in the Spring Security documentation URL at:

    https://docs.spring.io/spring-security/reference/5.7/servlet/oauth2/resource-server/jwt.html

    ... is not compatible with what ForgeRock AM expects.

    e.g.:

    SpringBoot OAuth2 Resource Server JWT token support

    SpringBoot OAuth2 Resource Server expects the following when receiving signed JWT as bearer in the Authorization HTTP header :

    • Client creates and signs a JWT, and send the signed JWT as bearer token in the Authorization HTTP header request to the Resource Server with a grant type of urn:ietf:params:oauth:grant-type:jwt-bearer.
    • Spring's OAuth2 Resource Server sees that the bearer token is a signed JWT, so looks up a matching public key from the spring.security.oauth2.resourceserver.jwt.jwk-set-uri property. ( You can also try first loading them locally from spring.security.oauth2.resourceserver.jwt.public-key-location - this one expected to be in PEM format. )
    • Spring's OAuth2 Resource Server verifies the signed JWT bearer token using a matching public key found from the previous step.
    • Spring's OAuth2 Resource Server then allows the HTTP request through. Otherwise, returns an HTTP 401.

    ForgeRock AM's expected flow with JWT profile for OAuth 2.0 authorization grant

    Whereas ForgeRock AM's model expects a different flow ( documented here : https://backstage.forgerock.com/docs/am/7.2/oauth2-guide/oauth2-jwt-bearer-grant.html )

    • It is expected that the resource server will be setup for opaque bearer token, not JWT bearer token, as defined here :

    https://docs.spring.io/spring-security/reference/5.7/servlet/oauth2/resource-server/opaque-token.html

    So you change the following in your resource server's application.properties, like so:

    spring.security.oauth2.resourceserver.opaque-token.introspection-uri=http://openam.localtest.me:8080/openam/oauth2/realms/subrealm/introspect
    spring.security.oauth2.resourceserver.opaque-token.client-id=someclient
    spring.security.oauth2.resourceserver.opaque-token.client-secret=somesecret
    

    ... and then remove / comment out the other properties related to jwk / jwt. ( e.g. Comment out spring.security.oauth2.resourceserver.jwt.jwk-set-uri )

    • Client creates and signs a JWT, and send the signed JWT as bearer token in the Authorization HTTP header request to the ForgeRock Authorization Server with a grant type of urn:ietf:params:oauth:grant-type:jwt-bearer. It is therefore expected that ForgeRock AM's trusted JWT issuer agent has been configured with the public key JWKS of the signer in the previous step.
    • AM server then validates the signed JWT and generates and returns an access token ( an opaque token in Spring's vocabulary ) back to the client.
    • Client then sends and uses this opaque access token as bearer token in the Authorization HTTP header request to the Resource Server ( e.g. Spring's OAuth2 resource server ).
    • Because it is an opaque token, Spring'2 OAuth2 resource server then calls the introspection URI ( spring.security.oauth2.resourceserver.opaque-token.introspection-uri ), which is an endpoint exposed by the AM server to validate the access token.
    • Based on the result of the call, resource server than either rejects ( with an HTTP 401 ) or accepts the HTTP call from the client.

    Because of this ( not being able to include the public key that I added into the HTTP output of ForgeRock AM's connect/jwk_uri, I am now looking to follow that grant flow documented by ForgeRock AM.


    Also, the documentation at :

    https://docs.spring.io/spring-security/reference/5.7/servlet/oauth2/resource-server/jwt.html

    .. says that you need to explicitly include both spring-security-oauth2-resource-server and spring-security-oauth2-jose to support signed JWT. But the maven dependency tree suggests that it is automatically included as a transitive dependency :

    $ mvn dependency:tree -Dincludes=org.springframework.security:spring-security-oauth2-jose:jar
    [INFO] Scanning for projects...
    [INFO] 
    [INFO] ---------------< org.example.oauth2-resource-server:jwt >---------------
    [INFO] Building oauth2-resource-server 0.0.1-SNAPSHOT
    [INFO] --------------------------------[ jar ]---------------------------------
    [INFO] 
    [INFO] --- maven-dependency-plugin:3.3.0:tree (default-cli) @ jwt ---
    [INFO] org.example.oauth2-resource-server:jwt:jar:0.0.1-SNAPSHOT
    [INFO] \- org.springframework.boot:spring-boot-starter-oauth2-resource-server:jar:2.7.7:compile
    [INFO]    \- org.springframework.security:spring-security-oauth2-jose:jar:5.7.6:compile
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time:  0.629 s
    [INFO] Finished at: 2023-01-01T14:33:49+11:00
    [INFO] ------------------------------------------------------------------------