Spring oauth2 refresh token - Cannot convert access token to JSON - spring

I'm trying to use a refresh token in a Spring OAuth application without success. The system will issue a refresh token on a password grant:
{
"access_token": "xxxxx",
"token_type": "bearer",
"refresh_token": "xxxxxx",
"expires_in": 21599,
"scope": "read write"
}
But trying to use the refresh token results in the following error:
curl -u acme -d "grant_type=refresh_token&refresh_token=xxxxxx" http://localhost:9999/uaa/oauth/token
{
"error": "invalid_token",
"error_description": "Cannot convert access token to JSON"
}
My auth server config is as follows:
#Controller
#SessionAttributes("authorizationRequest")
#EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
#EnableResourceServer
#ImportResource("classpath:/spring/application-context.xml")
#Configuration
public class ApplicationConfiguration extends WebMvcConfigurerAdapter {
#RequestMapping("/user")
#ResponseBody
public Principal user(Principal user) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
System.out.println(auth.toString());
return user;
}
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
registry.addViewController("/oauth/confirm_access").setViewName("authorize");
}
#Configuration
#Order(-20)
protected static class LoginConfig extends WebSecurityConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
// #formatter:off
http
.formLogin().loginPage("/login").permitAll()
.and()
.requestMatchers().antMatchers("/login", "/oauth/authorize", "/oauth/confirm_access")
.and()
.authorizeRequests().anyRequest().authenticated();
// #formatter:on
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
#Configuration
public static class JwtConfiguration {
#Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyPair keyPair = new KeyStoreKeyFactory(new ClassPathResource("keystore.jks"), "foobar".toCharArray())
.getKeyPair("test");
converter.setKeyPair(keyPair);
return converter;
}
#Bean
public JwtTokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
}
#Configuration
#EnableAuthorizationServer
protected static class OAuth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter implements
EnvironmentAware {
private static final String ENV_OAUTH = "authentication.oauth.";
private static final String PROP_CLIENTID = "clientid";
private static final String PROP_SECRET = "secret";
private static final String PROP_TOKEN_VALIDITY_SECONDS = "tokenValidityInSeconds";
private RelaxedPropertyResolver propertyResolver;
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
#Autowired
private JwtTokenStore jwtTokenStore;
#Autowired
#Qualifier("myUserDetailsService")
private UserDetailsService userDetailsService;
#Autowired
private DataSource dataSource;
#Override
public void setEnvironment(Environment environment) {
this.propertyResolver = new RelaxedPropertyResolver(environment, ENV_OAUTH);
}
#Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setSupportRefreshToken(true);
tokenServices.setTokenStore(jwtTokenStore);
tokenServices.setAuthenticationManager(authenticationManager);
tokenServices.setTokenEnhancer(tokenEnhancer());
return tokenServices;
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
// The order is important here - the custom enhancer must come before the jwtAccessTokenConverter.
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), jwtAccessTokenConverter));
endpoints
.authenticationManager(authenticationManager)
.tokenEnhancer(tokenEnhancerChain)
.tokenStore(jwtTokenStore)
.userDetailsService(userDetailsService);
}
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer)
throws Exception {
oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
/*.withClient(propertyResolver.getProperty(PROP_CLIENTID))
.scopes("read", "write")
.autoApprove(true)
.authorities(ClientAuthoritiesConstants.CLIENT)
.authorizedGrantTypes("authorization_code", "refresh_token", "password")
.secret(propertyResolver.getProperty(PROP_SECRET))
.accessTokenValiditySeconds(propertyResolver.getProperty(PROP_TOKEN_VALIDITY_SECONDS, Integer
.class, 1800));*/
}
}
/**
* Configures the global LDAP authentication
*/
#Configuration
protected static class AuthenticationConfiguration extends GlobalAuthenticationConfigurerAdapter implements EnvironmentAware {
private static final String ENV_LDAP = "authentication.ldap.";
private static final String PROP_SEARCH_BASE = "userSearchBase";
private static final String PROP_SEARCH_FILTER = "userSearchFilter";
private static final String PROP_GROUP_SEARCH_FILTER = "groupSearchFilter";
private static final String PROP_LDAP_URL = "url";
private static final String PROP_LDAP_USER = "userDn";
private static final String PROP_LDAP_PASS = "password";
private RelaxedPropertyResolver propertyResolver;
/**
* Maps the LDAP user to the Principle that we'll be using in the app
*/
public UserDetailsContextMapper userDetailsContextMapper() {
return new UserDetailsContextMapper() {
#Override
public UserDetails mapUserFromContext(DirContextOperations ctx, String username,
Collection<? extends GrantedAuthority> authorities) {
// Get the common name of the user
String commonName = ctx.getStringAttribute("cn");
// Get the users email address
String email = ctx.getStringAttribute("mail");
// Get the domino user UNID
String uId = ctx.getStringAttribute("uid");
return new CustomUserDetails(email, "", commonName, authorities);
}
#Override
public void mapUserToContext(UserDetails user, DirContextAdapter ctx) {
throw new IllegalStateException("Only retrieving data from LDAP is currently supported");
}
};
}
#Override
public void setEnvironment(Environment environment) {
this.propertyResolver = new RelaxedPropertyResolver(environment, ENV_LDAP);
}
#Override
public void init(AuthenticationManagerBuilder auth) throws Exception {
auth
.ldapAuthentication()
.userSearchBase(propertyResolver.getProperty(PROP_SEARCH_BASE))
.groupSearchBase(propertyResolver.getProperty(PROP_SEARCH_BASE))
.userSearchFilter(propertyResolver.getProperty(PROP_SEARCH_FILTER))
.groupSearchFilter(propertyResolver.getProperty(PROP_GROUP_SEARCH_FILTER))
.userDetailsContextMapper(userDetailsContextMapper())
.contextSource()
.url(propertyResolver.getProperty(PROP_LDAP_URL))
.managerDn(propertyResolver.getProperty(PROP_LDAP_USER))
.managerPassword(propertyResolver.getProperty(PROP_LDAP_PASS));
}
}
}
Anyone have any ideas why the auth server isn't issuing a new token when given a valid refresh token?

had this issue. i was sending the "Bearer xxxxxx..." and the TokenEnhancer was expecting just "xxxxx..." without the "Bearer " prefix

I had the same issue. After some debugging it turned out my signature did not match.
In my case i set-up keys a bit differently, and there is a bug where the signing and verifying key miss-match.
https://github.com/spring-projects/spring-security-oauth/issues/1144

Also has same issue with Spring Boot 1.5.4
It is really actual that jwtAccessTokenConverter.setVerifierKey(publicKey);doesn't really set verifier(in debug value is null) that is used in -
JwtAccessTokenConverter
...protected Map<String, Object> decode(String token) {
try {
Jwt jwt = JwtHelper.decodeAndVerify(token, verifier);
as workaround helped:
private JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new CustomTokenEnhancer();
jwtAccessTokenConverter.setSigningKey(jwtSigningKey);
jwtAccessTokenConverter.setVerifier(new RsaVerifier(jwtPublicKey));
log.info("Set JWT signing key to: {}", jwtAccessTokenConverter.getKey());
return jwtAccessTokenConverter;
}

It is been two years I don't if it helps anyone but my same issue was due to I was not using the tokenEnhancer I used in my JwtTokenStore in my token service provider DefaultTokenServices.
<!-- Access token converter -->
<bean id="jwtAccessTokenConverter"
class="org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter">
<property name="signingKey" value="${security.jwt.signing-key}"/>
</bean>
<!-- Token store -->
<bean id="jwtTokenStore"
class="org.springframework.security.oauth2.provider.token.store.JwtTokenStore">
<constructor-arg name="jwtTokenEnhancer" ref="jwtAccessTokenConverter"/>
</bean>
<!-- Creates token store services provider -->
<bean id="tokenServiceProvider"
class="org.springframework.security.oauth2.provider.token.DefaultTokenServices">
<property name="tokenStore"
ref="jwtTokenStore"/>
<!--This must be set according to z docs -->
<property name="tokenEnhancer"
ref="jwtAccessTokenConverter"/>
<property name="supportRefreshToken"
value="true"/>
<property name="accessTokenValiditySeconds"
value="${security.jwt.access-token-validity-seconds}"/>
<property name="refreshTokenValiditySeconds"
value="${security.jwt.refresh-token-validity-seconds}"/>
</bean>

I had the same issue with Spring Boot 2.5.7. Because I missed set verifier for JwtAccessTokenConverter.
My solution:
#Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new CustomJwtAccessTokenConverter(privateKey);
jwtAccessTokenConverter.setVerifier(new RsaVerifier(publicKey));
return jwtAccessTokenConverter;
}

So it looks like the issue was an invalid refresh_token format. Due to my config, what the auth server was expecting was a valid JWT, whereas I was sending it a plain bearer token. Hence the error message 'cannot convert token to JSON'.
Incidentally, I found this document useful in understanding how all the parts of Spring OAuth fit together, which led me to figuring out what was going on here:
https://github.com/spring-projects/spring-security-oauth/blob/master/docs/oauth2.md

Related

Getting Bad credentials with InMemoryUserDetails in spring boot

I am using spring boot oauth, while doing authorization I am not able to validate my credentials even if I used inMemory.
My authorization server:
#Configuration
#EnableAuthorizationServer
#Import(ServerWebSecurityConfig.class)
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
static final String CLIEN_ID = "ipro-client";
static final String CLIENT_SECRET = "ipro-secret";
static final String GRANT_TYPE = "password";
static final String AUTHORIZATION_CODE = "authorization_code";
static final String REFRESH_TOKEN = "refresh_token";
static final String IMPLICIT = "implicit";
static final String SCOPE_READ = "read";
static final String SCOPE_WRITE = "write";
static final String TRUST = "trust";
static final int ACCESS_TOKEN_VALIDITY_SECONDS = 1 * 60 * 60;
static final int FREFRESH_TOKEN_VALIDITY_SECONDS = 6 * 60 * 60;
static final String REDIRECT_URI = "http://localhost:8888/login";
/*#Autowired
#Qualifier("ibrdatasource")
private DataSource dataSource;*/
#Autowired
private UserDetailsService userDetailsService;
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore()).accessTokenConverter(accessTokenConverter())
.authenticationManager(authenticationManager).userDetailsService(userDetailsService);
}
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("permitAll()");
}
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
/*
* #Override public void configure(ClientDetailsServiceConfigurer clients)
* throws Exception { JdbcClientDetailsService jdbcClientDetailsService = new
* JdbcClientDetailsService(dataSource);
* clients.withClientDetails(jdbcClientDetailsService); }
*/
#Override
public void configure(ClientDetailsServiceConfigurer configurer) throws Exception {
configurer.inMemory().withClient(CLIEN_ID).secret(CLIENT_SECRET)
.authorizedGrantTypes(GRANT_TYPE, AUTHORIZATION_CODE, REFRESH_TOKEN, IMPLICIT)
.scopes(SCOPE_READ, SCOPE_WRITE, TRUST).redirectUris(REDIRECT_URI)
.accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS)
.refreshTokenValiditySeconds(FREFRESH_TOKEN_VALIDITY_SECONDS);
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("123");
return converter;
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
#Bean
public PasswordEncoder userPasswordEncoder() {
return new BCryptPasswordEncoder(4);
}
}
And I am adding userdetails with inMemrory data:
#Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("user").password("user").roles("USER").build());
manager.createUser(User.withUsername("admin").password("password").roles("USER", "ADMIN").build());
return manager;
}
But I am not able to validate these details.
It's showing bad credentials. Please help me on this, any help would be great.
This is probably happening because you're not encoding your password.
Use the BCryptPasswordEncoder when setting the password. Also, you'll have to tell the AuthenticationManagerBuilder that you're authenticating in memory.
Create a class that extends from WebSecurityConfigurerAdapter, inject the bean of type BCryptPasswordEncoder and use it when defining the in memory authentication.
SecurityConfiguration.java
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private BCryptPasswordEncoder passwordEncoder;
#Autowired
public void globalUserDetails(final AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password(this.passwordEncoder.encode("user")).roles("USER").and()
.withUser("admin").password(this.passwordEncoder.encode("admin")).roles("USER", "ADMIN").and()
}
}
#Bean
public PasswordEncoder userPasswordEncoder() {
return new BCryptPasswordEncoder(4);
}
#Autowired
PasswordEncoder passwordEncoder;
#Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(
User.withUsername("user").password(this.passwordEncoder.encode("user")).roles("USER").build());
manager.createUser(User.withUsername("admin").password(this.passwordEncoder.encode("password"))
.roles("USER", "ADMIN").build());
return manager;
}`enter code here`

invalid_token:Cannot convert access token to JSON

I get error while refreshing the token(grant_type=refresh_token). It seems that user did not use the application for long time and both access token as well as refresh token expired. When app now tried to refresh token, it gets the error
{
"error": "invalid_token",
"error_description": "Cannot convert access token to JSON"
}
I saw many posts on this issue but I am still facing this error. I tried to use setVerifierKey. But no luck. Here is the code:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${security.signing-key}")
private String signingKey;
#Value("${security.encoding-strength}")
private Integer encodingStrength;
#Value("${security.security-realm}")
private String securityRealm;
#Autowired
private UserDetailsService userDetailsService;
#Bean
#Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
// .passwordEncoder(new ShaPasswordEncoder(encodingStrength));
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic()
.realmName(securityRealm)
.and()
.csrf()
.disable();
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(signingKey);
return converter;
}
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
#Bean
#Primary //Making this primary to avoid any accidental duplication with another token service instance of the same name
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
}
//AuthorizationServerConfig
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Value("${security.jwt.client-id}")
private String clientId;
#Value("${security.jwt.client-secret}")
private String clientSecret;
#Value("${security.jwt.grant-type}")
private String grantType;
#Value("${security.jwt.scope-read}")
private String scopeRead;
#Value("${security.jwt.scope-write}")
private String scopeWrite = "write";
#Value("${security.jwt.resource-ids}")
private String resourceIds;
#Autowired
private TokenStore tokenStore;
#Autowired
private JwtAccessTokenConverter accessTokenConverter;
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private UserDetailsService userDetailsService;
#Override
public void configure(ClientDetailsServiceConfigurer configurer) throws Exception {
configurer
.inMemory()
.withClient(clientId)
.secret(clientSecret)
.authorizedGrantTypes("client_credentials", "password", "refresh_token", "authorization_code")
.scopes(scopeRead, scopeWrite)
// .accessTokenValiditySeconds(60)
// .refreshTokenValiditySeconds(2000)
.resourceIds(resourceIds);
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
enhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
endpoints.tokenStore(tokenStore)
.accessTokenConverter(accessTokenConverter)
.tokenEnhancer(enhancerChain).userDetailsService(userDetailsService)
.authenticationManager(authenticationManager);
}
}
I expect that token will be refreshed but I get the error as mentioned above. my config properties are:
security.oauth2.resource.filter-order=3
security.signing-key=ZMaasazkSjmaasw
security.encoding-strength=256
security.security-realm=Spring Boot JWT Example Realm
security.jwt.grant-type=password security.jwt.scope-read=read
security.jwt.scope-write=write
security.jwt.resource-ids=testjwtresourceid
Any Help appreciated!!

How to catch and handle InvalidGrantException (User is disabled)?

I noticed that my ResponseEntityExceptionHandler does not work with exceptions thrown by Spring Security in my Spring Boot application.
However, I need a way to catch InvalidGrantException which seems to get thrown when a user account is still disabled.
The use case is simple: If a user is currently disabled, I want to throw an appropriate error to my client s.t. it can display a message accordingly.
Right now the response by Spring Security is
{
error: "invalid_grant",
error_description: "User is disabled"
}
I saw this question but for some reason my AuthFailureHandler is not getting invoked:
#Configuration
#EnableResourceServer
public class ResourceServer extends ResourceServerConfigurerAdapter {
#Component
public class AuthFailureHandler implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
// Never reached ..
System.out.println("Hello World!");
}
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(customAuthEntryPoint());;
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.authenticationEntryPoint(customAuthEntryPoint());
}
#Bean
public AuthenticationEntryPoint customAuthEntryPoint(){
return new AuthFailureHandler();
}
}
Any idea what I am missing?
Configuration Code
Here is my OAuth2Configuration which also contains a ResourceServerConfiguraerAdapter which is supposed to handle the exception
#Configuration
#EnableAuthorizationServer
#EnableResourceServer
public class OAuth2Configuration extends AuthorizationServerConfigurerAdapter {
private final TokenStore tokenStore;
private final JwtAccessTokenConverter accessTokenConverter;
private final AuthenticationManager authenticationManager;
#Autowired
public OAuth2Configuration(TokenStore tokenStore, JwtAccessTokenConverter accessTokenConverter, AuthenticationManager authenticationManager) {
this.tokenStore = tokenStore;
this.accessTokenConverter = accessTokenConverter;
this.authenticationManager = authenticationManager;
}
#Value("${security.jwt.client-id}")
private String clientId;
#Value("${security.jwt.client-secret}")
private String clientSecret;
#Value("${security.jwt.scope-read}")
private String scopeRead;
#Value("${security.jwt.scope-write}")
private String scopeWrite;
#Value("${security.jwt.resource-ids}")
private String resourceIds;
private final static String WEBHOOK_ENDPOINTS = "/r/api/*/webhooks/**";
private final static String PUBLIC_ENDPOINTS = "/r/api/*/public/**";
#Override
public void configure(ClientDetailsServiceConfigurer configurer) throws Exception {
configurer
.inMemory()
.withClient(clientId)
.secret(clientSecret)
.scopes(scopeRead, scopeWrite)
.resourceIds(resourceIds)
.accessTokenValiditySeconds(60*60*24*7 * 2) // Access tokens last two weeks
.refreshTokenValiditySeconds(60*60*24*7 * 12) // Refresh tokens last 12 weeks
//.accessTokenValiditySeconds(5)
//.refreshTokenValiditySeconds(10)
.authorizedGrantTypes("password", "refresh_token"); //, "client_credentials");
}
/**
* Since there are currently multiply clients, we map the OAuth2 endpoints under /api
* to avoid conflicts with #{#link ForwardController}
*/
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
enhancerChain.setTokenEnhancers(Collections.singletonList(accessTokenConverter));
endpoints.tokenStore(tokenStore)
// Create path mappings to avoid conflicts with forwarding controller
.pathMapping("/oauth/authorize", "/api/v1/oauth/authorize")
.pathMapping("/oauth/check_token", "/api/v1/oauth/check_token")
.pathMapping("/oauth/confirm_access", "/api/v1/oauth/confirm_access")
.pathMapping("/oauth/error", "/api/v1/oauth/error")
.pathMapping("/oauth/token", "/api/v1/oauth/token")
.accessTokenConverter(accessTokenConverter)
.tokenEnhancer(enhancerChain)
.reuseRefreshTokens(false)
.authenticationManager(authenticationManager);
}
/**
* Forwarding controller.
*
* This controller manages forwarding in particular for the static web clients. Since there are multiple
* clients, this controller will map <i>any</i> GET request to the root /* to one of the clients.
*
* If no match was found, the default redirect goes to /web/index.html
*
*/
#Controller
public class ForwardController {
#RequestMapping(value = "/sitemap.xml", method = RequestMethod.GET)
public String redirectSitemapXml(HttpServletRequest request) {
return "forward:/a/web/assets/sitemap.xml";
}
#RequestMapping(value = "/robots.txt", method = RequestMethod.GET)
public String redirectRobotTxt(HttpServletRequest request) {
return "forward:/a/web/assets/robots.txt";
}
#RequestMapping(value = "/*", method = RequestMethod.GET)
public String redirectRoot(HttpServletRequest request) {
return "forward:/a/web/index.html";
}
#RequestMapping(value = "/a/**/{path:[^.]*}", method = RequestMethod.GET)
public String redirectClients(HttpServletRequest request) {
String requestURI = request.getRequestURI();
if (requestURI.startsWith("/a/admin/")) {
return "forward:/a/admin/index.html";
}
if (requestURI.startsWith("/a/swagger/")) {
return "forward:/a/swagger/swagger-ui.html#/";
}
return "forward:/a/web/index.html";
}
}
#Configuration
#EnableResourceServer
public class ResourceServer extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.requiresChannel()
/* Require HTTPS evereywhere*/
.antMatchers("/**")
.requiresSecure()
.and()
.exceptionHandling()
.and()
/* Permit all requests towards the public api as well as webhook endpoints. */
.authorizeRequests()
.antMatchers(PUBLIC_ENDPOINTS, WEBHOOK_ENDPOINTS)
.permitAll()
/* Required for ForwardController */
.antMatchers(HttpMethod.GET, "/*")
.permitAll()
.antMatchers("/r/api/**")
.authenticated();
// #formatter:on
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
.resourceId(resourceIds)
.authenticationEntryPoint(customAuthEntryPoint());
}
#Component
public class AuthFailureHandler implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
System.out.println("Hello");
// FIXME We need to return HTTP 401 (s.t. the client knows what's going on) but for some reason it's not working as intended!
throw authException;
}
}
#Bean
public AuthenticationEntryPoint customAuthEntryPoint(){
return new AuthFailureHandler();
}
}
}
In addition, here is the WebSecurityConfigurerAdapter although I do not think this plays a role here:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${security.signing-key}")
private String signingKey;
private final UserDetailsService userDetailsService;
private final DataSource dataSource;
#Autowired
public WebSecurityConfig(#Qualifier("appUserDetailsService") UserDetailsService userDetailsService, DataSource dataSource) {
this.userDetailsService = userDetailsService;
this.dataSource = dataSource;
}
#Bean
#Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
#Override
public void configure(WebSecurity web) throws Exception {
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(signingKey);
return converter;
}
/**
* Using {#link JwtTokenStore} for JWT access tokens.
* #return
*/
#Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
/**
* Provide {#link DefaultTokenServices} using the {#link JwtTokenStore}.
* #return
*/
#Bean
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
/**
* We provide the AuthenticationManagerBuilder using our {#link UserDetailsService} and the {#link BCryptPasswordEncoder}.
* #param auth
* #throws Exception
*/
#Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(this.userDetailsService)
.passwordEncoder(passwordEncoder())
.and()
.authenticationProvider(daoAuthenticationProvider());
}
/**
* Using {#link BCryptPasswordEncoder} for user-password encryption.
* #return
*/
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* Provide {#link DaoAuthenticationProvider} for password encoding and set the {#link UserDetailsService}.
* #return
*/
#Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
daoAuthenticationProvider.setUserDetailsService(this.userDetailsService);
return daoAuthenticationProvider;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.requiresChannel()
.requestMatchers(r -> r.getHeader("X-Forwarded-Proto") != null)
.requiresSecure();
}
}
Create a custom class that extends ResponseEntityExceptionHandler, annotate it with #ControllerAdvice and handle OAuth2Exception (base class of InvalidGrantException).
#ExceptionHandler({OAuth2Exception.class})
public ResponseEntity<Object> handleOAuth2Exception(OAuth2Exception exception, WebRequest request) {
LOGGER.debug("OAuth failed on request processing", exception);
return this.handleExceptionInternal(exception, ErrorOutputDto.create(exception.getOAuth2ErrorCode(), exception.getMessage()), new HttpHeaders(), HttpStatus.valueOf(exception.getHttpErrorCode()), request);
}
Use HandlerExceptionResolverComposite to composite all exception resolvers in the system into one exception resolver. This overrides the corresponding bean defined in
WebMvcConfigurationSupport class. Add list of exceptionResolvers, like DefaultErrorAttributes and ExceptionHandlerExceptionResolver (this enables AOP based exceptions handling by involving classes with #ControllerAdvice annotation such as custom class that we created that extends ResponseEntityExceptionHandler.
<bean id="handlerExceptionResolver" class="org.springframework.web.servlet.handler.HandlerExceptionResolverComposite">
<property name="exceptionResolvers">
<list>
<bean class="org.springframework.boot.web.servlet.error.DefaultErrorAttributes"/>
<bean class="org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver">
<property name="messageConverters">
<list>
<ref bean="jackson2HttpMessageConverter" />
</list>
</property>
</bean>
</list>
</property>
Have you tried if defining a #ControllerAdvice specifying your InvalidGrantException works?
#ControllerAdvice
#ResponseBody
public class GlobalExceptionHandler {
#ExceptionHandler(InvalidGrantException.class)
public ResponseEntity<CustomErrorMessageTO> handleInvalidGrant(
InvalidGrantException invalidGrantException) {
CustomErrorMessageTO customErrorMessageTO = new CustomErrorMessageTO("Not granted or whatsoever");
return new ResponseEntity<>(customErrorMessageTO, HttpStatus.UNAUTHORIZED);
}
}
class CustomErrorMessageTO {
private String message;
public CustomErrorMessageTO(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}

Spring Boot OAuth 2 - expiring refresh tokens when password changed

I've created an API using Spring Boot/OAuth. It's currently set so that access_tokens are valid for 30 days, and refresh_tokens are valid for 5 years. It's been requested that OAuth work this way so that a single refresh_token can be used over and over again. What we also need to do is implement some way of expiring refresh tokens when a user changes their password, this is what I'm struggling with as we're not using a token store as we're using JWTs, so there's no need to store the tokens, and even when we were storing that in a database we regularly got 'Invalid refresh token' errors, so removed the token store.
My question is, how to you handle expiring refresh tokens, say, when a user changes their password (as suggested by OAuth).
My client has specifically requested that the returned refresh_token be long-life, but I'm concerned that a long-life refresh token isn't very secure, as if anyone gets hold of that token they can access the users account until that token naturally expires. Personally I'd prefer to set a shorter expiry on refresh_tokens at say 45 days, forcing the client to store a new refresh_token every 45 days at least.
Here's are some of my configuration classes for security to show how I currently have things setup;
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private Environment env;
#Autowired
private CustomUserDetailsService userDetailsService;
#Autowired
private AccountAuthenticationProvider accountAuthenticationProvider;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
auth.authenticationProvider(accountAuthenticationProvider);
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
final JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(env.getProperty("jwt.secret"));
return jwtAccessTokenConverter;
}
}
#Configuration
public class OAuth2ServerConfiguration {
private static final String RESOURCE_ID = "myapi";
#Autowired
DataSource dataSource;
#Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
#Configuration
#EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Autowired
TokenStore tokenStore;
#Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources
.resourceId(RESOURCE_ID)
.tokenStore(tokenStore);
}
#Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/oauth/**", "/view/**").permitAll()
.anyRequest().authenticated();
}
}
#Configuration
#EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
#Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
#Autowired
private DataSource dataSource;
#Autowired
private TokenStore tokenStore;
#Autowired
private CustomUserDetailsService userDetailsService;
#Autowired
#Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
//.tokenStore(tokenStore)
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
.accessTokenConverter(jwtAccessTokenConverter);
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.jdbc(dataSource);
}
}
}
Revoking token is not supported if JWT is used. If you would like to have this functionality implemented, you should consider using JdbcTokenStore instead.
#Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource());
}
#Bean
public DataSource dataSource() {
DriverManagerDataSource jdbcdataSource = new DriverManagerDataSource();
jdbcdataSource.setDriverClassName(env.getProperty("jdbc.driverClassName"));
jdbcdataSource.setUrl(env.getProperty("jdbc.url"));//connection String
jdbcdataSource.setUsername(env.getProperty("jdbc.user"));
jdbcdataSource.setPassword(env.getProperty("jdbc.pass"));
return dataSource;
}
When the user changes password, you should invoke the revokeToken API
#Resource(name="tokenServices")
ConsumerTokenServices tokenServices;
#RequestMapping(method = RequestMethod.POST, value = "/tokens/revoke/{tokenId:.*}")
#ResponseBody
public String revokeToken(#PathVariable String tokenId) {
tokenServices.revokeToken(tokenId);
return tokenId;
}
JDBCTokenStore also exposes a method using which you can invalidate the refresh token
#RequestMapping(method = RequestMethod.POST, value = "/tokens/revokeRefreshToken/{tokenId:.*}")
#ResponseBody
public String revokeRefreshToken(#PathVariable String tokenId) {
if (tokenStore instanceof JdbcTokenStore){
((JdbcTokenStore) tokenStore).removeRefreshToken(tokenId);
}
return tokenId;
}

can I include user information while issuing an access token?

I have seen in some oauth2 implementations additional information on the response returned by the authorization server when it issues access tokens. I'm wondering if there is a way to accomplish this using spring-security-oauth2. I would love to be able to include some user authorities on the access token response so that my consuming applications don't need to manage the user authorities but can still set the user on their own security contexts and apply any of their own spring-security checks.
How would I get that information on the access token response?
How would I intercept that information on the oauth2 client side and set it on the security context?
I suppose another option would be to use JWT tokens and share the appropriate information with the client applications so that they can parse the user / authorities out of the token and set it on the context. This makes me more uncomfortable since I'd prefer to be in control of which client applications could have access to this information (trusted apps only) and AFAIK only the authorization server and resource server should know how to parse the JWT tokens.
You will need to implement a custom TokenEnhancer like so:
public class CustomTokenEnhancer implements TokenEnhancer {
#Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
User user = (User) authentication.getPrincipal();
final Map<String, Object> additionalInfo = new HashMap<>();
additionalInfo.put("customInfo", "some_stuff_here");
additionalInfo.put("authorities", user.getAuthorities());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
}
}
and add it to your AuthorizationServerConfigurerAdapter as a bean with the corresponding setters
#Configuration
#EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
// Some autowired stuff here
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// #formatter:off
endpoints
// ...
.tokenEnhancer(tokenEnhancer());
// #formatter:on
}
#Bean
#Primary
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
// ...
tokenServices.setTokenEnhancer(tokenEnhancer());
return tokenServices;
}
// Some #Bean here like tokenStore
#Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}
}
then in a controller (for example)
#RestController
public class MyController {
#Autowired
private AuthorizationServerTokenServices tokenServices;
#RequestMapping(value = "/getSomething", method = RequestMethod.GET)
public String getSection(OAuth2Authentication authentication) {
Map<String, Object> additionalInfo = tokenServices.getAccessToken(authentication).getAdditionalInformation();
String customInfo = (String) additionalInfo.get("customInfo");
Collection<? extends GrantedAuthority> authorities = (Collection<? extends GrantedAuthority>) additionalInfo.get("authorities");
// Play with authorities
return customInfo;
}
}
I'm personnaly using a JDBC TokenStore so my "Some autowired stuff here" are corresponding to some #Autowired Datasource, PasswordEncoder and what not.
Hope this helped!
If you are using Spring's JwtAccessTokenConverter or DefaultAccessTokenConverter you can add your custom CustomTokenEnhancer (see first response) and apply it using a TokenEnhancerChain like this:
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
enhancerChain.setTokenEnhancers(Arrays.asList(customTokenEnhancer(), accessTokenConverter()));
endpoints.tokenStore(tokenStore())
.tokenEnhancer(enhancerChain)
.authenticationManager(authenticationManager);
}
#Bean
protected JwtAccessTokenConverter jwtTokenEnhancer() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("my_signing_key");
return converter;
}
#Bean public TokenEnhancer customTokenEnhancer() {
return new CustomTokenEnhancer();
}
Another solution is to create a custom TokenConverter that extends Spring's JwtAccessTokenConverter and override the enhance() method with your custom claims.
public class CustomTokenConverter extends JwtAccessTokenConverter {
#Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
final Map<String, Object> additionalInfo = new HashMap<>();
additionalInfo.put("customized", "true");
User user = (User) authentication.getPrincipal();
additionalInfo.put("isAdmin", user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()).contains("BASF_ADMIN"));
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return super.enhance(accessToken, authentication);
}
}
And then:
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore())
.tokenEnhancer(customTokenEnhancer())
.authenticationManager(authenticationManager);
}
#Bean public CustomTokenConverter customTokenEnhancer() {
return new CustomTokenConverter();
}
Together with:
#Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}
You have to include
#Bean
public DefaultAccessTokenConverter accessTokenConverter() {
return new DefaultAccessTokenConverter();
}
and add everything to endpoints config:
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.tokenStore(tokenStore)
.tokenEnhancer(tokenEnhancer())
.accessTokenConverter(accessTokenConverter())
.authorizationCodeServices(codeServices)
.authenticationManager(authenticationManager)
;
}
Without it, your CustomTokenEnhancer will not work.
package com.security;
import java.util.HashMap;
import java.util.Map;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.stereotype.Component;
#Component
public class CustomTokenEnhancer implements TokenEnhancer {
#Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken,
OAuth2Authentication authentication) {
// TODO Auto-generated method stub
User user = (User) authentication.getPrincipal();
final Map<String, Object> additionalInfo = new HashMap<>();
additionalInfo.put("customInfo", "some_stuff_here");
additionalInfo.put("authorities", user.getAuthorities());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
}
}
Following is the xml configuration:
<bean id="tokenEnhancer" class="com.security.CustomTokenEnhancer" />
<!-- Used to create token and and every thing about them except for their persistence that is reposibility of TokenStore (Given here is a default implementation) -->
<bean id="tokenServices" class="org.springframework.security.oauth2.provider.token.DefaultTokenServices">
<property name="tokenStore" ref="tokenStore" />
<property name="accessTokenValiditySeconds" value="30000000"></property>
<property name="refreshTokenValiditySeconds" value="300000000"></property>
<property name="supportRefreshToken" value="true"></property>
<property name="clientDetailsService" ref="clientDetails"></property>
<property name="tokenEnhancer" ref="tokenEnhancer" />
</bean>
That's how I was able to add extra information to the Token.
create a class file CustomTokenEnhancer
#Component
public class CustomTokenConverter extends JwtAccessTokenConverter {
#Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
final Map<String, Object> additionalInfo = new HashMap<>();
additionalInfo.put("customized", "true");
User user = (User) authentication.getPrincipal();
additionalInfo.put("role", user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()));
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return super.enhance(accessToken, authentication);
}
}
paste below written code in AuthorizationServerConfig
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
enhancerChain.setTokenEnhancers(Arrays.asList(customTokenEnhancer(),accessTokenConverter()));
endpoints
.tokenStore(tokenStore())
.tokenEnhancer(customTokenEnhancer())
.authenticationManager(authenticationManager);
}
#Bean
protected JwtAccessTokenConverter jwtTokenEnhancer() {
JwtAccessTokenConverter converter= new JwtAccessTokenConverter();
converter.setSigningKey("my_signing_key");
return converter;
}
#Bean
public CustomTokenConverter customTokenEnhancer() {
return new CustomTokenConverter();
}
#Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
import appropriate libraries after paste the above codes
output response of Custom Token Enhancer..click here
I solve this problem when excluded UserDetailsServiceAutoConfiguration.
Like this. Maybe wiil be helpful in OAuth2 resource servers.
#SpringBootApplication(exclude = [UserDetailsServiceAutoConfiguration::class])
class Application
fun main(args: Array<String>) {
runApplication<Application>(*args)
}

Resources