I'm implementing OAuth2 Authorization server, and I have multiples authorizations providers, without User Details Service, like #813 issue, but when I try to get a refresh token of passord grant they return the error 500 with then Exception bellow
Error 500 with then Exception bellow
java.lang.IllegalStateException: UserDetailsService is required.
at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$UserDetailsServiceDelegator.loadUserByUsername(WebSecurityConfigurerAdapter.java:462) ~[spring-security-config-5.2.0.RELEASE.jar:5.2.0.RELEASE]
at org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper.loadUserDetails(UserDetailsByNameServiceWrapper.java:68) ~[spring-security-core-5.2.0.RELEASE.jar:5.2.0.RELEASE]
at org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider.authenticate(PreAuthenticatedAuthenticationProvider.java:103) ~[spring-security-web-5.2.0.RELEASE.jar:5.2.0.RELEASE]
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:175) ~[spring-security-core-5.2.0.RELEASE.jar:5.2.0.RELEASE]
at org.springframework.security.oauth2.provider.token.DefaultTokenServices.refreshAccessToken(DefaultTokenServices.java:150) ~[spring-security-oauth2-2.3.7.RELEASE.jar:na]
at org.springframework.security.oauth2.provider.refresh.RefreshTokenGranter.getAccessToken(RefreshTokenGranter.java:47) ~[spring-security-oauth2-2.3.7.RELEASE.jar:na]
at org.springframework.security.oauth2.provider.token.AbstractTokenGranter.grant(AbstractTokenGranter.java:67) ~[spring-security-oauth2-2.3.7.RELEASE.jar:na]
at org.springframework.security.oauth2.provider.CompositeTokenGranter.grant(CompositeTokenGranter.java:38) ~[spring-security-oauth2-2.3.7.RELEASE.jar:na]
at org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer$4.grant(AuthorizationServerEndpointsConfigurer.java:583) ~[spring-security-oauth2-2.3.7.RELEASE.jar:na]
at org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken(TokenEndpoint.java:132) ~[spring-security-oauth2-2.3.7.RELEASE.jar:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
Test Result
MockHttpServletRequest:
HTTP Method = POST
Request URI = /oauth/token
Parameters = {grant_type=[refresh_token], refresh_token=[f59f10ef-7b91-4c6d-8fea-ded4674a40a5]}
Headers = [Authorization:"Basic Y2xpZW50X3B3ZF90ZXN0OnNlY3JldA=="]
Body = null
Session Attrs = {}
Handler:
Type = org.springframework.security.oauth2.provider.endpoint.TokenEndpoint
Method = org.springframework.security.oauth2.provider.endpoint.TokenEndpoint#postAccessToken(Principal, Map)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = java.lang.IllegalStateException
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 500
Error message = null
Headers = [Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", Cache-Control:"no-store", Pragma:"no-cache", Content-Type:"application/json", X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", X-Frame-Options:"DENY"]
Content type = application/json
Body = {"error":"server_error","error_description":"Internal Server Error"}
Forwarded URL = null
Redirected URL = null
Cookies = []
Refresh token generated with success
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private DbAuthenticationProvider dbAuthenticationProvider;
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
@Bean
public ApprovalStore approvalStore() {
return new JdbcApprovalStore(dataSource);
}
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new JdbcAuthorizationCodeServices(dataSource);
}
@Bean
public JdbcClientDetailsService jdbcClientDetailsService() {
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
jdbcClientDetailsService.setPasswordEncoder(passwordEncoder());
return jdbcClientDetailsService;
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.withClientDetails(jdbcClientDetailsService());
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)
.approvalStore(approvalStore())
.authorizationCodeServices(authorizationCodeServices())
.tokenStore(tokenStore())
;
}
}
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DbAuthenticationProvider dbAuthenticationProvider;
@Override
public void configure(WebSecurity web) {
web.ignoring()
.antMatchers("/js/**", "/css/**", "/img/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().disable()
.csrf().disable()
.authorizeRequests()
.antMatchers("/password/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login").permitAll()
.and()
.logout().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.authenticationProvider(dbAuthenticationProvider);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
spring boot 2.2.0.RELEASE
spring security oauth2 autoconfigure 2.2.0.RELEASE (spring security oauth2 2.3.7.RELEASE)
I use the same approach of stackoverflow but with modifications to solve my problem
On my AuthenticationProvider I add support to PreAuthenticatedAuthenticationToken
@Component
public class DbAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserCredentialsRepository credentialRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
UserCredentials credentials = credentialRepository.findByName(username)
.orElseThrow(() -> new BadCredentialsException("Invalid username and/or password"));
List<GrantedAuthority> authorities = null;
if (credentials.getAuthorities() != null) {
authorities = credentials.getAuthorities().stream()
.map(it -> new SimpleGrantedAuthority(it.getAuthority()))
.collect(Collectors.toList());
}
if (authentication instanceof UsernamePasswordAuthenticationToken) {
if (passwordEncoder.matches(authentication.getCredentials().toString(), credentials.getPassword())) {
return new UsernamePasswordAuthenticationToken(authentication.getName(), authentication.getCredentials(), authorities);
}
} else if (authentication instanceof PreAuthenticatedAuthenticationToken) {
return new PreAuthenticatedAuthenticationToken(authentication.getName(), authentication.getCredentials(), authorities);
}
throw new BadCredentialsException("Invalid username and/or password");
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class)
|| authentication.equals(PreAuthenticatedAuthenticationToken.class);
}
}
and on my AuthorizationServerConfig I add my own tokenServices
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
....
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)
.approvalStore(approvalStore())
.authorizationCodeServices(authorizationCodeServices())
.tokenStore(tokenStore())
.tokenServices(tokenServices(endpoints))
;
}
private AuthorizationServerTokenServices tokenServices(final AuthorizationServerEndpointsConfigurer endpoints) {
final DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setReuseRefreshToken(true);
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
tokenServices.setAuthenticationManager(authenticationManager);
return tokenServices;
}
}
but I belived that is a Issue because this wr
I face similar issue at my current project, I think this problem must be treated with High Priority since there are many cases where you need Custom Authentication Provider in order to authenticate with both Username and Password.
I came accross this issue looking for something else. Anyway, maybe you just need to override the method WebSecurityConfigurerAdapter.userDetailsServiceBean, like you did with the authenticationManagerBean. The JavaDoc says:
Override this method to expose a UserDetailsService created from configure(AuthenticationManagerBuilder) as a bean. In general only the following override should be done of this method:
@Bean(name = "myUserDetailsService")
// any or no name specified is allowed
@Override
public UserDetailsService userDetailsServiceBean() throws Exception {
return super.userDetailsServiceBean();
}
To change the instance returned, developers should change userDetailsService() instead
@diegobmd If you could put together a minimal sample with steps to reproduce I'll have a look at this.
@jgrandja the thing with having a custom authentication provider where you need to authenticate using both username and password (which is the case for many production systems out there e.g. when authenticating against an external system) is when it comes to refresh the token, you cannot simply load a user by username. So, which are the best practices for such cases? It would be really helpful to provide the options and the trade-offs. What means to attempt to refresh a token in such situations? One approach could be when you first authenticate a user and obtain the access token, to save a local copy of the user information to a DB for example and use this when refreshing a token. Of course if in the meantime something has changed (e.g. the user is deactivated or has changed password in the external system) such changes will not be reflected when refreshing token.
@kmandalas I agree that a custom AuthenticationProvider is a common use case. I do believe there is a way to do this today. It's just a matter of custom configuration. I can see what can be done with the existing codebase today. But I will need a sample that reproduces the issue so I can work out the right solution. Feel free to provide a minimal sample with reproducible steps and I'll take a look.
@jgrandja ok, I will try to create a minimal sample on GitHub and post the link here
@jgrandja minimal sample uploaded at: https://github.com/kmandalas/spring-security-demo
Hi, i have the same error, the problem start on "UserDetailsServiceDelegator" when is created the "defaultUserDetailsService" that are on "delegateBuilders" are null. is doesnt care if you set it later on, it doesnt take changes. So to fixed for now, i see this post. In my personal case i solved setting the "userDetailsService" on "AuthorizationServerEndpointsConfigurer". I check it the source publish by @kmandalas and have the same soluction, however in his case he does implement the "loadUserByUsername" on "MyUserDetailsServiceImpl".
So please @jgrandja could you check why is not detecting the changes of "defaultUserDetailsService", i believe the problem is the early creation on that class.
@nekperu15739 hello. Just to make clear that in my case/sample refresh token does not work. It throws error IllegalStateException, UserDetailsService is required. In my case where I authenticate against an external service, the demand to have a UserDetailsService and loadUserByUsername during refresh token is ambiguous and I report this issue in order to receive feedback what is the best practice in such cases. When we refresh a token does this mean attempt to re-authenticate the user? Or to simply "copy" previous token claims and generate a new access token? What if something has changed in the meantime, for example some user privilege?
Moreover I would like to know how such case should be approached taking into consideration the upcoming changes described in: https://spring.io/blog/2019/11/14/spring-security-oauth-2-0-roadmap-update
@kmandalas I took a look at your sample and the main issue why MyAuthenticationProvider is not being called for the refresh_token grant is because it does not support PreAuthenticatedAuthenticationToken.
If you look at DefaultTokenServices.refreshAccessToken(), you will see that it creates a PreAuthenticatedAuthenticationToken and then passes it to the AuthenticationManager. Given that MyAuthenticationProvider only supports UsernamePasswordAuthenticationToken (via AbstractUserDetailsAuthenticationProvider) it does not get called.
I'm attaching a git patch that you can apply and see what changes I made. The changes I made is a workaround. It will call MyAuthenticationProvider but it will still fail on a later check with missing credentials. There are some other changes I applied to AuthorizationServerConfiguration to get things working. This will at least move you ahead but there is some cleanup required.
@kmandalas you don't precise the userDetailsService for endpoints in your * AuthorizationServerConfiguration*.
You must do that
@Autowired
private YourUserDetailsService yourUserDetailsService;
...
...
endpoints.authenticationManager(authenticationManager).userDetailsService(yourUserDetailsService)...;
@akoua when we want to implement our CustomAuthenticationProvider, What is the need to provide a userdetailsservice. In my case, I dont want to validate the user with the password but authenticate with a 2fa token. I can't user userdetailsservice when I dont have any password right. Any suggestion would be helpful.
@jgrandja I tried to put the PreAuthenticatedAuthenticationToken.class in the supports method. Still I don't see it working. Could you please let me know if there is any mistake.
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Object details = authentication.getDetails();
Map<String, String> detailsMap = null;
if(details instanceof Map){
detailsMap = (Map)details;
}
String username = detailsMap.get("username");
// get some more details and authenticate the user here
UserDetails user = new UserImpl(username, otherDetailsButNotPassword);
return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class) || authentication.equals(PreAuthenticatedAuthenticationToken.class);
}
}
If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.
Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open the issue.
Most helpful comment
I face similar issue at my current project, I think this problem must be treated with High Priority since there are many cases where you need Custom Authentication Provider in order to authenticate with both Username and Password.