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?
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
.
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 :
spring.security.oauth2.resourceserver.jwt.jwk-set-uri
property.kid
as the kid
in the signed JWT bearer token.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 expects the following when receiving signed JWT as bearer in the Authorization
HTTP header :
Authorization
HTTP header request to the Resource Server with a grant type of urn:ietf:params:oauth:grant-type:jwt-bearer
.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. )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 )
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
)
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.Authorization
HTTP header request to the Resource Server ( e.g. Spring's OAuth2 resource server ).spring.security.oauth2.resourceserver.opaque-token.introspection-uri
), which is an endpoint exposed by the AM server to validate the access token.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] ------------------------------------------------------------------------