I'm trying to get a new access token using a refresh token in Spring Boot with OAuth2. It should be done as following: POST: url/oauth/token?grant_type=refresh_token&refresh_token=....
It works fine if I'm using InMemoryTokenStore because the token is tiny and contains only digits/letters but right now I'm using a JWT token and as you probably know it has 3 different parts which probably are breaking the code.
I'm using the official migration guide to 2.4.
When I try to access the URL above, I'm getting the following message:
{
"error": "invalid_token",
"error_description": "Cannot convert access token to JSON"
}
How do I pass a JWT token in the params? I tried to set a breakpoint on that message, so I could see what the actual argument was, but it didn't get to it for some reason.
/**
* The Authorization Server is responsible for generating tokens specific to a client.
* Additional information can be found here: https://www.devglan.com/spring-security/spring-boot-security-oauth2-example.
*/
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Value("${user.oauth2.client-id}")
private String clientId;
#Value("${user.oauth2.client-secret}")
private String clientSecret;
#Value("${user.oauth2.accessTokenValidity}")
private int accessTokenValidity;
#Value("${user.oauth2.refreshTokenValidity}")
private int refreshTokenValidity;
#Autowired
private ClientDetailsService clientDetailsService;
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient(clientId)
.secret(bCryptPasswordEncoder.encode(clientSecret))
.authorizedGrantTypes("password", "authorization_code", "refresh_token")
.scopes("read", "write", "trust")
.resourceIds("api")
.accessTokenValiditySeconds(accessTokenValidity)
.refreshTokenValiditySeconds(refreshTokenValidity);
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.tokenStore(tokenStore())
.userApprovalHandler(userApprovalHandler())
.accessTokenConverter(accessTokenConverter());
}
#Bean
public UserApprovalHandler userApprovalHandler() {
ApprovalStoreUserApprovalHandler userApprovalHandler = new ApprovalStoreUserApprovalHandler();
userApprovalHandler.setApprovalStore(approvalStore());
userApprovalHandler.setClientDetailsService(clientDetailsService);
userApprovalHandler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));
return userApprovalHandler;
}
#Bean
public TokenStore tokenStore() {
JwtTokenStore tokenStore = new JwtTokenStore(accessTokenConverter());
tokenStore.setApprovalStore(approvalStore());
return tokenStore;
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
final RsaSigner signer = new RsaSigner(KeyConfig.getSignerKey());
JwtAccessTokenConverter converter = new JwtAccessTokenConverter() {
private JsonParser objectMapper = JsonParserFactory.create();
#Override
protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
String content;
try {
content = this.objectMapper.formatMap(getAccessTokenConverter().convertAccessToken(accessToken, authentication));
} catch (Exception ex) {
throw new IllegalStateException("Cannot convert access token to JSON", ex);
}
Map<String, String> headers = new HashMap<>();
headers.put("kid", KeyConfig.VERIFIER_KEY_ID);
return JwtHelper.encode(content, signer, headers).getEncoded();
}
};
converter.setSigner(signer);
converter.setVerifier(new RsaVerifier(KeyConfig.getVerifierKey()));
return converter;
}
#Bean
public ApprovalStore approvalStore() {
return new InMemoryApprovalStore();
}
#Bean
public JWKSet jwkSet() {
RSAKey.Builder builder = new RSAKey.Builder(KeyConfig.getVerifierKey())
.keyUse(KeyUse.SIGNATURE)
.algorithm(JWSAlgorithm.RS256)
.keyID(KeyConfig.VERIFIER_KEY_ID);
return new JWKSet(builder.build());
}
}
I assume that the Cannot convert access token to JSON might have been due to incorrectly pasted token.
As for Invalid refresh token, it occurs because when JwtTokenStore reads the refresh token, it validates the scopes and revocation with InMemoryApprovalStore. However, for this implementation, the approvals are registered only during authorization through /oauth/authorize URL (Authorisation Code Grant) by the ApprovalStoreUserApprovalHandler.
Especially for the Authorisation Code Grant (authorization_code), you want to have this validation, so that the refresh token request will not be called with an extended scope without the user knowledge. Moreover, it's optional to store approvals for future revocation.
The solution is to fill the ApprovalStore with the Approval list for all resource owners either statically or dynamically. Additionally, you might be missing setting the user details service endpoints.userDetailsService(userDetailsService) which is used during the refresh process.
Update:
You can verify this by creating pre-filled InMemoryApprovalStore:
#Bean
public ApprovalStore approvalStore() {
InMemoryApprovalStore approvalStore = new InMemoryApprovalStore();
Date expirationDate = Date.from(Instant.now().plusSeconds(3600));
List<Approval> approvals = Stream.of("read", "write", "trust")
.map(scope -> new Approval("admin", "trusted", scope, expirationDate,
ApprovalStatus.APPROVED))
.collect(Collectors.toList());
approvalStore.addApprovals(approvals);
return approvalStore;
}
I would also take a look at implementing it in the storeRefreshToken()/storeAccessToken() methods of JwtTokenStore, as they have an empty implementation, and the method parameters contain all the necessary data.
Related
For some reason, the input field to write the bearer access token is not showing as an implicit param on swagger UI.
This makes it impossible to access all endpoints because they require authentication.
What is showing :
What should show :
These images are from different projects but with the same configuration classes. This is the annotation I'm using to mark the endpoints that require the bearer access token.
#ApiImplicitParam(name = "Authorization", value = "Bearer Token", required = true,
allowEmptyValue = false, paramType = "header", example = "Bearer access_token")
And lastly, this is the token configuration class :
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("user")
.secret("senha")
.scopes("read", "write")
.authorizedGrantTypes("password")
.accessTokenValiditySeconds(1800);
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore()).authenticationManager(authenticationManager);
}
#Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
}
Would appreciate if someone could help with this problem I'm having for quite a few hours. Thank you.
i'm trying to get the refresh token from the user logged in my system, and store it in a database. So a different system in my ecosystem can access the stored refresh token, generate an access token with it and use the google calendar api with the user credentials.
So far i have managed to do the login with
#Configuration
public class AppConfig extends WebSecurityConfigurerAdapter {
#Autowired
private ClientRegistrationRepository clientRegistrationRepository;
#Override
public void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.antMatchers("/**").authenticated()
.anyRequest().permitAll()
.and()
.oauth2Login()
.authorizationEndpoint()
.authorizationRequestResolver(new CustomAuthorizationRequestResolver(
this.clientRegistrationRepository))
.and()
.and()
.rememberMe();
}
}
And
public class CustomAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
private final OAuth2AuthorizationRequestResolver defaultAuthorizationRequestResolver;
public CustomAuthorizationRequestResolver(
ClientRegistrationRepository clientRegistrationRepository) {
this.defaultAuthorizationRequestResolver =
new DefaultOAuth2AuthorizationRequestResolver(
clientRegistrationRepository, "/oauth2/authorization");
}
#Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
OAuth2AuthorizationRequest authorizationRequest =
this.defaultAuthorizationRequestResolver.resolve(request);
return authorizationRequest != null ?
customAuthorizationRequest(authorizationRequest) :
null;
}
#Override
public OAuth2AuthorizationRequest resolve(
HttpServletRequest request, String clientRegistrationId) {
OAuth2AuthorizationRequest authorizationRequest =
this.defaultAuthorizationRequestResolver.resolve(
request, clientRegistrationId);
return authorizationRequest != null ?
customAuthorizationRequest(authorizationRequest) :
null;
}
private OAuth2AuthorizationRequest customAuthorizationRequest(
OAuth2AuthorizationRequest authorizationRequest) {
Map<String, Object> additionalParameters = new LinkedHashMap<>(authorizationRequest.getAdditionalParameters());
additionalParameters.put("access_type", "offline");
return OAuth2AuthorizationRequest.from(authorizationRequest)
.additionalParameters(additionalParameters)
.build();
}
}
how and where can i access the refresh token of the logged user?
I answered a similar question here, but it is in kotlin, so I'll add a java version for you.
These are two approaches to get the refresh token (or rather OAuth2AuthorizedClient, from which you can get the refresh token). Which one you use depend on your needs.
Inject and OAuth2AuthorizedClient representing the requesting user into an endpoint method:
#GetMapping("/foo")
void foo(#RegisteredOAuth2AuthorizedClient("google") OAuth2AuthorizedClient user) {
OAuth2RefreshToken refreshToken = user.getRefreshToken();
}
Outside the context of a request, you can inject OAuth2AuthorizedClientService into a managed component, and get the needed OAuth2AuthorizedClient instance with the client registration id and principal name:
#Autowired
private OAuth2AuthorizedClientService clientService;
public void foo() {
OAuth2AuthorizedClient user = clientService.loadAuthorizedClient("google", "principal-name");
OAuth2RefreshToken refreshToken = user.getRefreshToken();
}
I have configured my Oauth2 Authorization project with JWT. When I authorize using client credentials I gets the access token like as shown below. The expiry time is 43199
{
"access_token":"eyJhbGci........................",
"token_type": "bearer",
"expires_in": 43199,
"scope": "resource-access",
"jti": "45507f3e-2d8c-4dc8-95ce-295bb690cf3a"
}
I am not storing the tokens anywhere like DB or session etc, still If I invoke the same authorize token endpoint I gets the same access token with decreasing expiry time.
Not sure where this token is been stored, what I expect is to get a new token every time I invokes the token authorize endpoint.
Can anyone please help me on this
My custom AuthorizationServerConfigurerAdapter class is given below
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Value("${scopes}")
private Boolean checkUserScopes;
#Autowired
private DataSource dataSource;
#Autowired
private PasswordEncoder passwordEncoder;
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private ClientDetailsService clientDetailsService;
#Autowired
#Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
#Bean
public OAuth2RequestFactory requestFactory() {
CustomOauth2RequestFactory requestFactory = new CustomOauth2RequestFactory(clientDetailsService);
requestFactory.setCheckUserScopes(true);
return requestFactory;
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource).passwordEncoder(passwordEncoder);
}
#Bean
public TokenEndpointAuthenticationFilter tokenEndpointAuthenticationFilter() {
return new TokenEndpointAuthenticationFilter(authenticationManager, requestFactory());
}
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.tokenEnhancer(jwtAccessTokenConverter())
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
if (checkUserScopes) {
endpoints.requestFactory(requestFactory());
}
}
#Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
CustomJwtTokenEnhancerConfig tokenEnhancer = new CustomJwtTokenEnhancerConfig();
tokenEnhancer.setKeyPair(new KeyStoreKeyFactory(new ClassPathResource("myjwt.jks"), "password".toCharArray()).getKeyPair("jwt"));
return tokenEnhancer;
}
}
JSON Web Token (JWT) authentication technique doesn't require database tables or data stores to keep the generated token. The token is generated by using cryptography algorithms.
When you call your login service, the token is generated by using a payload and a secret value. If your payload is the same, you won't get a new token value with the same secret value.
You can refer to this link for a little more information on how JWT works.
https://medium.com/vandium-software/5-easy-steps-to-understanding-json-web-tokens-jwt-1164c0adfcec
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);
}