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;
}
Related
I am trying to configure my Spring Boot 2, OAuth2 with JWT Authorization Server, which needs to do the following:
take a username/password, build a CustomUserDetails object based on db data and salesforce data, and return a JWT token if authentication passes (this works)
take a refresh token and return a new JWT refresh and access token (this works)
(NEW) take a refresh token, check against the db for a stored token ID before returning a new JWT access + refresh token (this is the trouble spot) The point of this is to ensure only one device is logged in with the user's credentials at a time.
In order to do #3, I am trying to customize the PreAuthenticatedAuthenticationProvider by giving it a custom UserDetailsService, and the AuthenticationManagerBuilder bean needs to be passed both the customized PreAuthenticatedAuthenticationProvider and the DaoAuthenticationProvider.
Assuming I am going in the right direction with that, here is my configuration code:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
UserDetailsService userDetailsService;
#Autowired
PasswordEncoder passwordEncoder;
//implements AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken>
#Autowired
CustomPreauthenticatedUserDetailsService customPreauthenticatedUserDetailsService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/actuator/**").permitAll()
.antMatchers("/swagger-ui**","/webjars/**","/swagger-resources/**", "/v2/**").permitAll()
.antMatchers("/oauth/token/revokeById/**").permitAll()
.antMatchers("/oauth/token/**").permitAll()
.anyRequest().authenticated()
.and().csrf().disable();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder authenticationManager) throws Exception {
authenticationManager.authenticationProvider(preauthAuthProvider());
authenticationManager.authenticationProvider(dbAuthProvider());
}
#Bean
#Qualifier(value = "authenticationManagerBean")
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean(value="preAuthProvider")
public PreAuthenticatedAuthenticationProvider preauthAuthProvider() {
PreAuthenticatedAuthenticationProvider preauthAuthProvider = new PreAuthenticatedAuthenticationProvider();
LOGGER.info("Setting customPreauthenticatedUserDetailsService");
preauthAuthProvider.setPreAuthenticatedUserDetailsService(customPreauthenticatedUserDetailsService);
return preauthAuthProvider;
}
#Bean(value="dbAuthProvider")
public DaoAuthenticationProvider dbAuthProvider() {
DaoAuthenticationProvider dbAuthProvider = new DaoAuthenticationProvider();
dbAuthProvider.setUserDetailsService(userDetailsService);
dbAuthProvider.setPasswordEncoder(passwordEncoder);
return dbAuthProvider;
}
}
On the AuthorizationServerConfig side:
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private static final Integer ACCESS_TOKEN_VALIDITY_SECONDS = 300;
private static final Integer REFRESH_TOKEN_VALIDITY_SECONDS = 60 * 60 * 24 * 60;
#Autowired
public AuthorizationServerConfig(AuthenticationManager authenticationManagerBean, PasswordEncoder passwordEncoder, CustomTokenEnhancer customTokenEnhancer, TokenStore tokenStore, JwtAccessTokenConverter accessTokenConverter) {
this.authenticationManagerBean = authenticationManagerBean;
this.passwordEncoder = passwordEncoder;
this.customTokenEnhancer = customTokenEnhancer;
this.tokenStore = tokenStore;
this.accessTokenConverter = accessTokenConverter;
}
private AuthenticationManager authenticationManagerBean;
private PasswordEncoder passwordEncoder;
private CustomTokenEnhancer customTokenEnhancer;
private JwtAccessTokenConverter accessTokenConverter;
private TokenStore tokenStore;
#Override
public void configure(AuthorizationServerSecurityConfigurer authorizationServerSecurityConfigurer) {
authorizationServerSecurityConfigurer.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");
}
#Override
public void configure(ClientDetailsServiceConfigurer clientDetailsServiceConfigurer) throws Exception {
clientDetailsServiceConfigurer.inMemory().withClient("someclient")
.authorizedGrantTypes("password", "refresh_token")
.scopes("read", "write").accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS)
.refreshTokenValiditySeconds(REFRESH_TOKEN_VALIDITY_SECONDS)
.secret(this.passwordEncoder.encode("somepassword"));
}
#Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(customTokenEnhancer, accessTokenConverter));
endpoints.tokenStore(tokenStore).tokenEnhancer(tokenEnhancerChain)
.authenticationManager(this.authenticationManagerBean);
}
}
Upon start up of the app, the first sign of trouble I see is:
s.c.a.w.c.WebSecurityConfigurerAdapter$3 : No authenticationProviders and no parentAuthenticationManager defined. Returning null.
Oddly, that appears to be a lie, or it's talking about something else, because when I try to login with username and password - it works, and I get both my refresh and access tokens. The debugger shows that the ProviderManager has both my custom authentication provider classes and uses them for authentication.
However, when I try to retrieve a new token with the refresh token, the debugger shows that the app appears to be taking a different ProviderManager path - this ProviderManager only has PreAuthenticatedAuthenticationProvider in its list, and that provider is NOT the one I configured. This PreAuthenticatedAuthenticationProvider is trying to retrieve a UserDetailsService from WebSecurityConfigurerAdapter$UserDetailsServiceDelegator, and the result is an error:
2019-06-08 13:27:24.764 ERROR 8731 --- [nio-8080-exec-3] o.s.s.o.provider.endpoint.TokenEndpoint : Handling error: IllegalStateException, UserDetailsService is required.
So, what configuration step am I missing? Why does the refresh token call go elsewhere? Is it the authenticationManagerBean that I'm passing in the AuthorizationServerConfig?
I have an API that I've developed in Spring Boot, and I've just noticed that it's not returning a refresh token when you request an access token.
The response from the API looks like this;
{
"access_token": "ed0bdc62-dccf-4f58-933c-e28ad9598843",
"token_type": "bearer",
"expires_in": 2589494,
"scope": "read write"
}
My configuration looks like this;
#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);
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.jdbc(dataSource);
}
}
}
I previously had the project setup to use JWT for access tokens and that did return a refresh token, however I had to remove JWT as it wasn't compatible with using the token store.
To confirm, it returns a refresh token when the grant_type = password, but not when it's set to 'client_credentials'.
Does anyone have any suggestions why my configuration doesn't return a refresh token?
4.3.3. Access Token Response in RFC 6749 (The OAuth 2.0 Authorization Framework) says "A refresh token SHOULD NOT be included." Therefore, most implementations of OAuth 2.0 authorization servers do not generate a refresh token in Client Credentials flow.
I got the same issue, then I changed in this method, I added REFRESH_TOKEN then in the response I am getting refresh_token value.
static final String REFRESH_TOKEN = "refresh_token";
#Override
public void configure(ClientDetailsServiceConfigurer configurer) throws Exception {
configurer
.inMemory()
.withClient(CLIENT_ID)
.secret(messageDigestPasswordEncoder.encode(CLIENT_SECRET))
.authorizedGrantTypes(GRANT_TYPE,REFRESH_TOKEN)
.scopes(SCOPE_READ, SCOPE_WRITE ,TRUST)
.accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS).
refreshTokenValiditySeconds(REFRESH_TOKEN_VALIDITY_SECONDS);
}
By default when OAuth2 authorisation is enabled in Spring framework (see the configuration below) and we make a call to /oauth/token to issue an access token, the following request is being sent:
POST /oauth/token
Authorization: Basic Y34tcF9ib3VpOg==
POST data:
grant_type=password&username=myuser&password=mypass
The basic authorisation above is client-id and client's secret in the following form:
myclient:secret123
I can then send this request to Spring's /oauth/check_token:
POST /oauth/check_token
Authorization: Basic Y34tcF9ib3VpOg==
POST data:
token=the_token_retrieved_from_last_request
This works fine and it does basic authorisation before serving my request.
Note that the basic authorisation here goes to Spring's JdbcClientDetailsService in which it looks up a table named oauth_client_details, this is fine too.
Now for some reason I need to have a customised endpoint instead of Spring's /token/check_access. So I have created a controller similar to the Spring's CheckTokenEndpoint.java and named it TokenIntrospectionEndpoint. The URL pattern for my endpoint is set to be /oauth/introspect:
#Controller
#RequestMapping("/oauth")
public class TokenIntrospectionEndpointImpl {
private RestTemplate restTemplate;
#RequestMapping(value = "/introspect")
#ResponseBody
#Override
public Map<String, ?> introspect(#RequestParam("token") String token) {
// return data
}
}
Now the problem is, the request to this endpoint is being served without considering basic authorisation. So I've added this line in the configuration:
#Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/oauth/introspect").access("isAuthenticated()");
}
Now Spring security kicks in but it doesn't treat this request the same way it does for /oauth/check_token and by that I mean it doesn't look up table oauth_client_details automatically just like the same way it does for other oauth related requests. As such, I get 401 http error code.
I think I am missing something here to tell Spring that this is oauth2 request so that it considers client-id/secret and authenticate it automatically. Any hint would be appreciated.
My configurations:
#Configuration
#EnableAuthorizationServer
public class OAuth2AuthorisationServerConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
private MySecuritySettings securitySetting;
#Autowired
private DataSource dataSource;
#Autowired
private JdbcTemplate jdbcTemplate;
#Autowired
private AuthenticationManager authenticationManager;
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
//TODO I'd rather not to override Spring's endpoint URL but had issues with authentication.
.pathMapping("/oauth/check_token", "/oauth/introspect").tokenStore(this.tokenStore())
.authenticationManager(authenticationManager)
.tokenServices(tokenServices())
.accessTokenConverter(tokenConverter())
.requestValidator(createOAuth2RequestValidator());
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(myClientDetailsService());
}
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.checkTokenAccess("isAuthenticated()")
.tokenKeyAccess("permitAll()")
.passwordEncoder(passwordEncoder());
}
#Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
#Bean
public MyClientDetailsService myClientDetailsService(){
MyClientDetailsService myClientDetailsService = new MyClientDetailsService(dataSource);
myClientDetailsService.setPasswordEncoder(passwordEncoder());
return myClientDetailsService;
}
#Bean
public JwtTokenStore tokenStore() {
return new JwtTokenStore(tokenConverter());
}
#Bean
public JwtAccessTokenConverter tokenConverter() {
final JwtAccessTokenConverter jwtAccessTokenConverter = new CompJwtAccessTokenConverter();
DefaultAccessTokenConverter defaultAccessTokenConverter = new DefaultAccessTokenConverter();
defaultAccessTokenConverter.setUserTokenConverter(new CompPrincipalExtractor());
jwtAccessTokenConverter.setAccessTokenConverter(defaultAccessTokenConverter);
KeyPair keyPair = new KeyStoreKeyFactory(
new ClassPathResource(securitySetting.getKeystoreFileName()),
securitySetting.getStorepass().toCharArray())
.getKeyPair(securitySetting.getKeyAlias(),
securitySetting.getKeypass().toCharArray());
jwtAccessTokenConverter.setKeyPair(keyPair);
return jwtAccessTokenConverter;
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore());
tokenServices.setSupportRefreshToken(securitySetting.isRefreshAccessToken());
tokenServices.setReuseRefreshToken(securitySetting.isReuseRefreshToken());
tokenServices.setTokenEnhancer(tokenConverter());
tokenServices.setAccessTokenValiditySeconds(securitySetting.getAccessTokenValiditySeconds());
return tokenServices;
}
#Bean
#Primary
public OAuth2RequestValidator createOAuth2RequestValidator() {
return new ExpressionBasedOAuth2RequestValidator();
}
}
AND this:
#Configuration
#EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
private static final String RESOURCE_ID = "auth_serv";
#Autowired
TokenStore tokenStore;
#Autowired
MySecuritySettings mySecuritySettings;
#Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources
.resourceId(RESOURCE_ID)
.tokenStore(tokenStore);
}
#Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/oauth/introspect").access("isAuthenticated()")
.and()
.authorizeRequests()
.antMatchers("/api/**/*").access("#oauth2.hasScope('" + mySecuritySettings.getAuthserverScopenameAllAccess() + "')");
}
}
I used JDBC token store in authorization server. But I don't know about the consumer token services. Can anyone explain it? How to revoke the access token using consumer token services?
Authorization Config
#Configuration
#EnableAuthorizationServer
public class OAuthServerConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
private UserDetailsServiceImpl userDetailsService;
#Autowired
AuthenticationManager authenticationManager;
#Autowired
Environment environment;
#Autowired
#Qualifier("dataSourceApi")
DataSource dataSource;
#Primary
#Bean
public ConsumerTokenServices defaultTokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
return defaultTokenServices;
}
#Bean
public JdbcTokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
#Bean
public JdbcClientDetailsService jdbcClientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore()).authenticationManager(authenticationManager);
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
}
}
Old post, but this should have an answer for future devs.
When a user logs out, the access token should (must?!) be revoked. This can be done via
tokenServices.revokeToken(tokenId);
See eg. http://www.baeldung.com/logout-spring-security-oauth
I'm trying to BCrypt the client secrets I'm storing in the database when using Spring Security Oauth2. I can see that JdbcClientDetailsService has a setPasswordEncoder method (as mentioned in this question). However, the ClientDetailsServiceConfigurer on AuthorizationServerConfigurerAdapter does not show any obvious way of setting the password encoder. Does anyone know how to do this? I've included the authorization server configuration:
#Configuration
#EnableAuthorizationServer
public static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
#Autowired
private DataSource dataSource;
#Autowired
private PasswordEncoder passwordEncoder;
#Autowired
private TokenStore tokenStore;
#Autowired
private UserApprovalHandler userApprovalHandler;
#Autowired
private ClientDetailsService clientDetailsService;
#Autowired
private AuthenticationManager authenticationManager;
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}
#Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
#Bean
public TokenApprovalStore tokenApprovalStore() {
TokenApprovalStore tokenApprovalStore = new TokenApprovalStore();
tokenApprovalStore.setTokenStore(tokenStore);
return tokenApprovalStore;
}
#Bean
public UserApprovalHandler userApprovalHandler() {
LocalUserApprovalHandler handler = new LocalUserApprovalHandler();
handler.setApprovalStore(tokenApprovalStore());
handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));
handler.setClientDetailsService(clientDetailsService);
handler.setUseApprovalStore(true);
return handler;
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.tokenStore(tokenStore)
.userApprovalHandler(userApprovalHandler)
.authenticationManager(authenticationManager);
}
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer.realm("abcdefgh/client");
}
}
As of release 2.0.5, passwordEncoder(...) methods are now available on both ClientDetailsServiceConfigurer and AuthorizationServerSecurityConfigurer, which are made available when extending AuthorizationServerConfigurerAdapter. Use the same PasswordEncoder implementation on both and the configuration is relatively easy.
ClientDetailsServiceConfigurer doesn't really need to encode passwords if they are already in the database. If you use a backend store you should just inject it into the configurer, and deal with creating the data in the back end as a separate problem.