I am unable to extract user info from the access token generated by keycloak. I have a protected route where I am expecting Principal or Authentication objects to be populated correctly.
#Configuration
#EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
private final String SIGNING_KEY = "MIIBCgKCAQ...AB";
#Override
public void configure(HttpSecurity http) throws Exception {
http
.requestMatcher(new RequestHeaderRequestMatcher("Authorization"))
.authorizeRequests()
.antMatchers("/api/register").anonymous()
.antMatchers("/api/token").anonymous()
.anyRequest().authenticated();
}
#Override
public void configure(ResourceServerSecurityConfigurer config) {
config.tokenServices(createTokenServices());
}
#Bean
#Primary
public DefaultTokenServices createTokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(createTokenStore());
return defaultTokenServices;
}
#Bean
public TokenStore createTokenStore() {
return new JwtTokenStore(createJwtAccessTokenConverter());
}
#Bean
public JwtAccessTokenConverter createJwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setAccessTokenConverter(new JwtConverter());
// converter.setSigningKey(SIGNING_KEY);
return converter;
}
public static class JwtConverter extends DefaultAccessTokenConverter implements JwtAccessTokenConverterConfigurer {
#Override
public void configure(JwtAccessTokenConverter converter) {
converter.setAccessTokenConverter(this);
}
#Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
OAuth2Authentication auth = super.extractAuthentication(map);
auth.setDetails(map); //this will get spring to copy JWT content into Authentication
return auth;
}
}
}
In Profile controller, I would like to display the details from the token passed in.
#GetMapping("/api/profile/details")
public Object details(final Principal authentication) {
// Object details = authentication.getDetails();
// if (details instanceof OAuth2AuthenticationDetails) {
// OAuth2AuthenticationDetails oAuth2AuthenticationDetails = (OAuth2AuthenticationDetails) details;
// Map<String, Object> decodedDetails = (Map<String, Object>) oAuth2AuthenticationDetails.getDecodedDetails();
//
// return decodedDetails;
// }
return authentication.getName();
}
I get the following error when I call the above endpooint.
{
"error": "invalid_token",
"error_description": "Cannot convert access token to JSON"
}
Whether I pass in the signing key or not it doesn't make a difference.
Thanks for helping.
Related
I`m developing a microservice structure using spring-boot, this structure has an external oauth2 Authorization Server and multiples Resource Servers.
My problem is, each http request to my resources, calls a url to my Authorization Server, to validate the token ( .../oauth/check_token/). This way there is a lot of requests. There is a way to validate/check this token only when it is expired?
My Resource servers:
#Configuration
#EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
#Value("${security.oauth2.client.client-id}")
private String clientId;
#Value("${security.oauth2.client.client-secret}")
private String clientSecret;
#Value("${security.oauth2.resource.id}")
private String resourceId;
#Value("${security.oauth2.resource.token-info-uri}")
private String tokenInfoUri;
#Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(ADMIN_ANT_MATCHER).hasRole("ADMIN")
.antMatchers(PROTECTED_ANT_MATCHER).hasRole("USER")
.and()
.csrf().disable();
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenServices(tokenService()).resourceId(resourceId).stateless(true);
}
#Bean
#Primary
public RemoteTokenServices tokenService() {
RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setCheckTokenEndpointUrl(tokenInfoUri);
tokenService.setClientId(clientId);
tokenService.setClientSecret(clientSecret);
return tokenService;
}
}
Authorization Server:
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Value("${security.oauth2.client.client-id}")
private String clientId;
#Value("${security.oauth2.client.client-secret}")
private String clientSecret;
#Value("${security.oauth2.resource.id}")
private String resourceId;
#Value("${security.oauth2.client.access-token-validity-seconds}")
private Integer tokenValidateSeconds;
#Value("${security.oauth2.client.token-secret}")
private String tokenSecret;
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private TokenStore tokenStore;
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private OauthAccessTokenRepository oauthAccessTokenRepository;
#Autowired
private OauthRefreshTokenRepository oauthRefreshTokenRepository;
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.tokenStore(this.tokenStore)
.tokenEnhancer(tokenEnhancer())
.authenticationManager(this.authenticationManager)
.userDetailsService(userDetailsService);
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient(clientId)
.authorizedGrantTypes("client_credentials", "password", "refresh_token")
.authorities("ROLE_USER","ROLE_ADMIN")
.scopes("read","write","trust")
.resourceIds(resourceId)
.accessTokenValiditySeconds(tokenValidateSeconds)
.secret(bCryptPasswordEncoder().encode(clientSecret));
}
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.checkTokenAccess("isAuthenticated()");
}
#Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setSupportRefreshToken(true);
tokenServices.setTokenStore(this.tokenStore);
return tokenServices;
}
#Bean
public TokenEnhancer tokenEnhancer() {
return new TokenEnhancer() {
#Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
CustomUser user = (CustomUser) authentication.getPrincipal();
String token = JwtTokenHelper.generateToken(
user.getIdUser(),
user.getTenantID(),
user.getIdEntity(),
user.getIdBusinessUnit(),
user.getProfile(),
tokenValidateSeconds,
tokenSecret
);
((DefaultOAuth2AccessToken) accessToken).setValue(token);
return accessToken;
}
};
}
}
You can remove the need to use the check_token endpoint, by using signed JWT tokens.
When the resource server receive a JWT token, it verify it's signature by using a public key, and the expiration date by checking the corresponding field in the JSON object.
For this you can use the JwtAccessTokenConverter, JwtTokenStore and the nimbus-jose-jwt library.
The downside of this approach is that you cannot revoke a token. It's then preferable to have short lived tokens.
I am using oauth2 security in spring boot with 2.1.5 version. When I send a request to issue token, I am receiving an only the same token that got before. I cannot get a token until the token is expired. After that, I can get a new token, but again the same situation.
I tried to change the token store from JdbcTokenStore to InMemoryTOkenStore. Besides that, I changed the authentication mechanism AuthenticationProvider to UserDetailsService.But nothing happened.
#Configuration
public class OAuth2SecurityConfiguration {
#Configuration
#EnableResourceServer
public static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
private final TokenStore tokenStore;
public ResourceServerConfiguration(TokenStore tokenStore) {
this.tokenStore = tokenStore;
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.tokenStore(tokenStore).resourceId(Constants.SERVER_RESOURCE_ID);
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(Constants.API_BASE_PATH + "/**").authenticated()
.and().csrf().disable();
}
}
#Configuration
#EnableAuthorizationServer
public static class OAuth2AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
private final AuthenticationManager authenticationManager;
private final DataSource dataSource;
private final PasswordEncoder passwordEncoder;
#Value("${jwt.key.password:my_pay_jwt_password}")
private String keyStorePassword;
public OAuth2AuthorizationServerConfiguration(AuthenticationManager authenticationManager, DataSource dataSource, PasswordEncoder passwordEncoder) {
this.authenticationManager = authenticationManager;
this.dataSource = dataSource;
this.passwordEncoder = passwordEncoder;
}
#Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
#Bean
public JwtAccessTokenConverter tokenEnhancer() {
// For getting user information in getPrincipal()
DefaultUserAuthenticationConverter duac = new DefaultUserAuthenticationConverter();
DefaultAccessTokenConverter datc = new DefaultAccessTokenConverter();
datc.setUserTokenConverter(duac);
JwtAccessTokenConverter converter = new CustomAccessTokenConverter();
converter.setAccessTokenConverter(datc); // IMPORTANT
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("static/jwt.jks"), keyStorePassword.toCharArray());
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("jwt"));
PublicKey publicKey = keyStoreKeyFactory.getKeyPair("jwt").getPublic();
String publicKeyString = Base64.encodeBase64String(publicKey
.getEncoded());
converter.setVerifierKey(publicKeyString);
return converter;
}
#Bean
#Primary
public AuthorizationServerTokenServices defaultAuthorizationServerTokenServices() {
DefaultTokenServices tokenServices = new LockingTokenServices();
tokenServices.setAuthenticationManager(this.authenticationManager);
tokenServices.setTokenStore(tokenStore());
tokenServices.setTokenEnhancer(tokenEnhancer());
tokenServices.setSupportRefreshToken(true);
tokenServices.setReuseRefreshToken(false);
return tokenServices;
}
#Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security
.checkTokenAccess("permitAll()")
.tokenKeyAccess("isAuthenticated()")
.allowFormAuthenticationForClients()
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource)
.passwordEncoder(passwordEncoder);
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.reuseRefreshTokens(false)
.accessTokenConverter(tokenEnhancer())
.authenticationManager(authenticationManager)
.tokenStore(tokenStore()).approvalStoreDisabled();
endpoints.exceptionTranslator(exception -> {
if (exception instanceof OAuth2Exception) {
OAuth2Exception oAuth2Exception = (OAuth2Exception) exception;
if (OAuth2Exception.INVALID_GRANT.equals(oAuth2Exception.getOAuth2ErrorCode())) {
oAuth2Exception.addAdditionalInformation("error_description", "Invalid username or password");
}
return ResponseEntity
.status(oAuth2Exception.getHttpErrorCode())
.body(oAuth2Exception);
} else {
throw exception;
}
});
}
}
}
I have multiple clients registered for my oauth2 auth server. I want to get the user authorities based on the clientId. Let's say USER-1 has authorities ADMIN for CLIENT-1 and for CLIENT-2 the USER-1 has USER authority.
I have tried this issue. But I always get a null request.
final HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
I have also added a WebListner, but with no luck.
#Configuration
#WebListener
public class MyRequestContextListener extends RequestContextListener {
}
#Service
public class DomainUserDetailsService implements UserDetailsService {
#Autowired
private UserRepository userRepository;
#Autowired
private AuthorityRepository authorityRepository;
#Override
#Transactional
public UserDetails loadUserByUsername(final String email) throws UsernameNotFoundException {
User user = userRepository.findUserByUserName(email);
if (user == null) {
new UsernameNotFoundException("Username not found");
}
String clientId = "?"; // How to get clientId here?
List<String> roles = authorityRepository.getUserAuthorities(email, clientId);
return new DomainUser(email, user.getCredential(), user.getId(), fillUserAuthorities(roles));
}
public Collection<SimpleGrantedAuthority> fillUserAuthorities(Collection<String> roles) {
Collection<SimpleGrantedAuthority> authorties = new ArrayList<SimpleGrantedAuthority>();
for (String role : roles) {
authorties.add(new SimpleGrantedAuthority(role.toUpperCase()));
}
return authorties;
}
}
If I am going in the wrong direction, any suggestions are acceptable.
Currently, I am using a Custom JWT Token Enhancer to achieve the requirement. But I am not sure if this is the right way of doing this. I am not sure why I'm thinking it is the wrong way. But you can achieve this using the below solution.
public class CustomTokenEnhancer implements TokenEnhancer {
#Autowired
private AuthorityRepository authRepository;
#Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Map<String, Object> additionalInfo = new HashMap<>();
DomainUser authPrincipal = (DomainUser) authentication.getPrincipal();
List<String> clientBasedRoles = authRepository.getUserAuthorities(authPrincipal.getId(),
authentication.getOAuth2Request().getClientId());
additionalInfo.put("authorities", clientBasedRoles);
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
}
}
Then, in your AuthorizationServerConfigurerAdapter.
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));
endpoints.authenticationManager(this.authenticationManager).accessTokenConverter(accessTokenConverter())
.tokenEnhancer(tokenEnhancerChain);
}
}
I'm going through this tutorial on how to setup spring boot oauth with jwt. It covers decoding the JWT token using Angular, but how do we decode it and get access to custom claims inside the Resource Server controller?
For example with JJWT it can be done like this (Based on this article):
String subject = "HACKER";
try {
Jws jwtClaims =
Jwts.parser().setSigningKey(key).parseClaimsJws(jwt);
subject = claims.getBody().getSubject();
//OK, we can trust this JWT
} catch (SignatureException e) {
//don't trust the JWT!
}
And Spring has a JWTAccessTokenConverter.decode() method, but the javadoc is lacking, and it is protected.
Here is how I am accessing custom JWT claims in Spring Boot:
1) Get Spring to copy JWT content into Authentication:
#Configuration
#EnableResourceServer
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends ResourceServerConfigurerAdapter{
#Override
public void configure(ResourceServerSecurityConfigurer config) {
config.tokenServices( createTokenServices() );
}
#Bean
public DefaultTokenServices createTokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore( createTokenStore() );
return defaultTokenServices;
}
#Bean
public TokenStore createTokenStore() {
return new JwtTokenStore( createJwtAccessTokenConverter() );
}
#Bean
public JwtAccessTokenConverter createJwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setAccessTokenConverter( new JwtConverter() );
return converter;
}
public static class JwtConverter extends DefaultAccessTokenConverter implements JwtAccessTokenConverterConfigurer {
#Override
public void configure(JwtAccessTokenConverter converter) {
converter.setAccessTokenConverter(this);
}
#Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
OAuth2Authentication auth = super.extractAuthentication(map);
auth.setDetails(map); //this will get spring to copy JWT content into Authentication
return auth;
}
}
}
2) Access token content anywhere in your code:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object details = authentication.getDetails();
if ( details instanceof OAuth2AuthenticationDetails ){
OAuth2AuthenticationDetails oAuth2AuthenticationDetails = (OAuth2AuthenticationDetails)details;
Map<String, Object> decodedDetails = (Map<String, Object>)oAuth2AuthenticationDetails.getDecodedDetails();
System.out.println( "My custom claim value: " + decodedDetails.get("MyClaim") );
}
In my Spring boot application I'm trying to configure Oauth2 & JWT, it works fine but I would like hide additionnal informations from the oauth2 token because they are in plain text and the same informations are duplicated in the JWT token.
This is my Oauth2ServerConfig :
#Configuration
public class OAuth2ServerConfiguration {
#Configuration
#EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
private final AuthenticationManager authenticationManager;
private final OAuth2ApprovalRepository oAuth2ApprovalRepository;
private final OAuth2CodeRepository oAuth2CodeRepository;
private final OAuth2ClientDetailsRepository oAuth2ClientDetailsRepository;
public AuthorizationServerConfiguration(#Qualifier("authenticationManagerBean") AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#Bean
public ApprovalStore approvalStore() {
return new MyDBApprovalStore(oAuth2ApprovalRepository);
}
#Bean
protected AuthorizationCodeServices authorizationCodeServices() {
return new MyDBAuthorizationCodeServices(oAuth2CodeRepository);
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));
endpoints.authorizationCodeServices(authorizationCodeServices())
.approvalStore(approvalStore())
.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancerChain)
.authenticationManager(authenticationManager);
}
#Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("123");
return converter;
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(new MyClientDetailsService(oAuth2ClientDetailsRepository));
}
}
}
And my custom information adding :
public class CustomTokenEnhancer implements TokenEnhancer {
#Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Map<String, Object> additionalInfo = new HashMap<>();
additionalInfo.put("organizationId", "123");
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
}
}
This is an example of the response of my authenticating WS call :
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdhbml6YXRpb25JZCI6IjEyMyIsImF1ZCI6WyJyZXNfYmh1YiJdLCJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdLCJleHAiOjE0OTc4NjkyNDMsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiXSwianRpIjoiOGNhYTZjN2YtNTU0Yy00OTZmLTkwYTUtZTA4MjAyM2I3ZTFlIiwiY2xpZW50X2lkIjoiYmh1YmFwcCJ9.B58c2_tmfuV_L1py8ZzOPuTK3OZAhVFviL9W1gxRoec",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdhbml6YXRpb25JZCI6IjEyMyIsImF1ZCI6WyJyZXNfYmh1YiJdLCJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdLCJhdGkiOiI4Y2FhNmM3Zi01NTRjLTQ5NmYtOTBhNS1lMDgyMDIzYjdlMWUiLCJleHAiOjE0OTc4Njk0NDMsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiXSwianRpIjoiMGJjNWJhYzctMWI3Ny00OGFiLWI1N2MtNDM4ZjMyN2JmNGM2IiwiY2xpZW50X2lkIjoiYmh1YmFwcCJ9.DkQoCEX47PmmxOEj0n9kb2L5Yu6DqFgmUh7HBSTO_z4",
"expires_in": 1799,
"scope": "read write",
"organizationId": "123",
"jti": "8caa6c7f-554c-496f-90a5-e082023b7e1e"
}
I don't want to expose the organizationId of this token to external world and would like to encode this information in only the JWT token (access_token) .
How it can be implemented with Spring Boot, OAuth2, JWT ?
If the connection is over HTTPS (as it should be) then the information won't be exposed to the external world (just the client which is requesting it).
In any case, the access token you have is only a JWS (it's not encrypted) so the information isn't hidden if you put it in there (it's just Base64 encoded).
I found a solution here:
Spring OAuth 2 + JWT Inlcuding additional info JUST in access token
I also changed the configure method...
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer()));
endpoints
.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancerChain)
.reuseRefreshTokens(false)
.userDetailsService(userDetailsService)
.authenticationManager(authenticationManager);
}