I'm currently developing a React WebApp with a Spring Boot REST API in the backend. I want to use OAuth2 to secure the API (and to improve my knowledge). I have however some questions regarding the correct authentication flow to use.
Since the frontend is using JavaScript I should not use flows that require a client secret.
The only flow that does not require a client secret in Spring is the Implicit Flow. With this flow however, Spring does not support refresh tokens. That means that after some time the user would get automatically logged out and needs to authorize the WebApp again.
Another option I saw was creating a client without a secret and then use the Authorization Code flow. But I have some doubts if this is the right way to go.
So my question basically: Which is the best OAuth2 flow to use with a Javascript frontend, when I don't want the user to be logged out after some time?
WebSecurityConfig
#Configuration
#EnableWebSecurity
#Import(Encoders.class)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsServiceImpl;
#Autowired
private PasswordEncoder userPasswordEncoder;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsServiceImpl).passwordEncoder(userPasswordEncoder);
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.formLogin().permitAll()
.and()
.authorizeRequests().antMatchers("/login", "/error**").permitAll()
.anyRequest().authenticated();
}
}
ResourceServerConfig
#Configuration
#EnableResourceServer
#EnableGlobalMethodSecurity
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
private static final String RESOURCE_ID = "resource-server-rest-api";
private static final String SECURED_READ_SCOPE = "#oauth2.hasScope('read')";
private static final String SECURED_WRITE_SCOPE = "#oauth2.hasScope('write')";
private static final String SECURED_PATTERN = "/api/**";
#Autowired
private DefaultTokenServices tokenServices;
#Autowired
private TokenStore tokenStore;
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(RESOURCE_ID)
.tokenServices(tokenServices)
.tokenStore(tokenStore);
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.antMatcher(SECURED_PATTERN).authorizeRequests().anyRequest().authenticated();
}
}
OAuth2Config
#Configuration
#PropertySource({"classpath:persistence.properties"})
#EnableAuthorizationServer
#Import(WebSecurityConfig.class)
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
#Autowired
#Qualifier("dataSource")
private DataSource dataSource;
#Autowired
private UserDetailsService userDetailsServiceImpl;
#Autowired
private PasswordEncoder oauthClientPasswordEncoder;
#Autowired
private AuthenticationManager authenticationManager;
#Bean
public OAuth2AccessDeniedHandler oauthAccessDeniedHandler() {
return new OAuth2AccessDeniedHandler();
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(
new ClassPathResource("mykeys.jks"),
"mypass".toCharArray());
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("mykeys"));
return converter;
}
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
defaultTokenServices.setTokenEnhancer(accessTokenConverter());
return defaultTokenServices;
}
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients().tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.passwordEncoder(oauthClientPasswordEncoder);
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
final TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter()));
endpoints
.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancerChain)
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsServiceImpl);
}
}
The Implicit flow is meant for JavaScript implementations according to the OAuth2 spec
Refresh tokens are not supported with the Implicit flow. The spring implementation is following the Oauth2 spec.
In case of a Javascript client implementations, tokens are stored on the client. When using refresh tokens, the refresh token needs to be persisted on the client in order to obtain a new access token in the future. If this is the case, you might as wel issue a long(er) lasting access token instead of a refresh token. When working client side, there is no advantage in using refresh token.
Related
I am migrating from Spring Boot 1.5.12 to Spring Boot 2.0 and also to Spring Security 5 and I am trying to do authenticate via OAuth 2. But I am getting this error even after using delegate {noop}:
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null
Here is my code :
SecurityConfig
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomUserDetailsService userDetailsService;
#Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
public SecurityConfig() {
super();
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}
#Override
protected void configure(final HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().exceptionHandling().and().authorizeRequests()
.antMatchers("/api/v1/**")
.authenticated().and().httpBasic();
}
#Override
public void configure(final WebSecurity web) throws Exception {
web.ignoring().antMatchers(
"/v2/api-docs","/configuration/ui","/swagger-resources", "/configuration/security", "/webjars/**",
"/swagger-resources/configuration/ui","/swagger-resources/configuration/security",
"/swagger-ui.html", "/admin11/*", "/*.html", "/*.jsp", "/favicon.ico", "//*.html", "//*.css", "//*.js",
"/admin11/monitoring","/proxy.jsp");
}
#Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
Oauth2AuthorizationServerConfig
public class Oauth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
#Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
#Autowired
private CustomUserDetailsService userDetailsService;
#Autowired
private JdbcTokenStore jdbcTokenStore;
#Bean
public TokenStore tokenStore() {
return jdbcTokenStore;
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
CustomTokenEnhancer converter = new CustomTokenEnhancer();
converter.setSigningKey("secret_api");
return converter;
}
#Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore())
.accessTokenConverter(accessTokenConverter())
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
.pathMapping("/oauth/token", "/api/v1/oauth/token");
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient("app").secret("{noop}secret")
.authorizedGrantTypes("password", "authorization_code").scopes("read", "write")
.autoApprove(true).accessTokenValiditySeconds(0);
}
#Override
public void configure(final AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");
}
#Bean
public DefaultTokenServices defaultTokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
return defaultTokenServices;
}
}
CustomUserDetailsService
public interface CustomUserDetailsService extends UserDetailsService {
UserDetails getByMsisdn(String msisdn);
void initDummyUsers();
}
To solve this issue I tried the below questions from Stackoverflow:
Spring Boot PasswordEncoder Error
The Spring Security documentation is addressing your exact problem.
The following error occurs when one of the passwords that are stored
has no id as described in Password Storage Format.
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped
for the id "null"
at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:233)
at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:196)
The easiest way to resolve the error is to switch to explicitly
provide the PasswordEncoder that you passwords are encoded with. The
easiest way to resolve it is to figure out how your passwords are
currently being stored and explicitly provide the correct
PasswordEncoder.
If you are migrating from Spring Security 4.2.x you can revert to the
previous behavior by exposing a NoOpPasswordEncoder bean.
So you should be able to fix this by explicitly providing the PasswordEncoder
// remember, its bad practice
#Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
Or even better supply a custom defined password encoder delegator:
String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());
PasswordEncoder passwordEncoder =
new DelegatingPasswordEncoder(idForEncode, encoders);
All taken from the official spring security docs on password encoding.
I had Oauth implemented in Spring Boot 1.5.7 but when I switched to 2 it showed me error "java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null".
From some research, I found that this may be an issue about password storage and password encoding.
What I've tried - I tried encoding the client secret in the authorization server file but that doesn't do anything and the error remains.
I've also tried saving the password with {bcrypt} as a prefix as spring security 5 looks for ann {id} during the password search.
I'm not able to fetch the access token and the above error doesn't go. Can someone help me figure this out? I've read and implemented almost everything and it doesn't seem to work.
Update: I was able to solve the above error by saving the password with {bcrypt} format. Similarly applying passwordEncoder in other required places.
Issue: I'm now facing an error with bad credentials. I've debugged and figured that its not getting the username we're trying to pass in the api and receiving null parameter. The flow reaches the userDetailservice but with an epmty parameter. I've attached my UserDetailsService along with this.
SecurityConfig.java
#Configuration
#EnableWebSecurity
public class OAuth2SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private ClientDetailsService clientDetailsService;
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private CustomPasswordEncoder customPasswordEncoder;
#Autowired
public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(customPasswordEncoder);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.anonymous().disable()
.authorizeRequests()
.antMatchers("/oauth/token").permitAll();
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
#Autowired
public TokenStoreUserApprovalHandler userApprovalHandler(TokenStore tokenStore) {
TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();
handler.setTokenStore(tokenStore);
handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));
handler.setClientDetailsService(clientDetailsService);
return handler;
}
#Bean
#Autowired
public ApprovalStore approvalStore(TokenStore tokenStore) throws Exception {
TokenApprovalStore store = new TokenApprovalStore();
store.setTokenStore(tokenStore);
return store;
}
#Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
AuthorizationServerConfig.java
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
private static String REALM = "api-security";
#Value("${app.oauth.client-id}")
private String CLIENT_ID;
#Value("${app.oauth.client-secret}")
private String CLIENT_SECRET;
#Value("${app.oauth.access-token-validity}")
private int accessTokenValidity;
#Value("${app.oauth.refresh-token-validity}")
private int refreshTokenValidity;
#Autowired
#Qualifier("tokenStore")
private TokenStoreService tokenStore;
#Autowired
private UserApprovalHandler userApprovalHandler;
#Autowired
private BCryptPasswordEncoder passwordEncoder;
#Autowired
#Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient(CLIENT_ID)
.authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
.authorities("ROLE_ADMIN").scopes("read", "write", "trust").secret(passwordEncoder.encode(CLIENT_SECRET))
.accessTokenValiditySeconds(accessTokenValidity).refreshTokenValiditySeconds(refreshTokenValidity);
System.out.println(passwordEncoder.encode(CLIENT_SECRET));
System.out.println(CLIENT_SECRET);
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore).userApprovalHandler(userApprovalHandler)
.authenticationManager(authenticationManager);
}
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.realm(REALM + "/client");
}
}
UserDetailsService.java
#Configuration
#EnableWebSecurity
public class OAuth2SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private ClientDetailsService clientDetailsService;
#Autowired
#Qualifier("userDetailsService")
private UserDetailsService userDetailsService;
#Autowired
public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.anonymous().disable()
.authorizeRequests()
.antMatchers("/oauth/token").permitAll();
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
#Autowired
public TokenStoreUserApprovalHandler userApprovalHandler(TokenStore tokenStore) {
TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();
handler.setTokenStore(tokenStore);
handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));
handler.setClientDetailsService(clientDetailsService);
return handler;
}
#Bean
#Autowired
public ApprovalStore approvalStore(TokenStore tokenStore) throws Exception {
TokenApprovalStore store = new TokenApprovalStore();
store.setTokenStore(tokenStore);
return store;
}
#Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
// #Bean
// #Override
// public UserDetailsService userDetailsServiceBean() throws Exception {
// return super.userDetailsServiceBean();
// }
// #Bean
// public UserDetailsService userDetailsService() {
// return super.userDetailsService();
// }
}
For whoever finds this useful, I was able to solve this by the following points:
If you clear your access token collection or table, you'll be able to get the access toke once but that's it. Every request you do after that will go with "500 error - Internal server error".
This happens because spring boot wasn't able to understand the access token from the DB when making other requests, for which you can use "org.springframework.util.SerializationUtils" package. You can search about this, it serializes and deserializes the access tokens and refresh token when requests are made.
I use spring-security-oauth2.
I would like to custom configure the /oauth/token end-point.
After registration a new client, I need to use login and password from this client and create a token for him and return this token.
Can I custom configure this /oauth/token endpoint?
Can I return a custom error if I receive incorrect information for this end-point (incorrect login or password)?
Can I put here a client-id and client-secret and don`t enter this in front-end?
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
private DataSource dataSource;
#Autowired
private TokenStore tokenStore;
#Autowired
private JwtAccessTokenConverter jwtTokenEnhancer;
#Autowired
private UserApprovalHandler userApprovalHandler;
#Autowired
private AuthenticationManager authenticationManager;
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}
#Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security.checkTokenAccess("isAuthenticated()");
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore).tokenEnhancer(jwtTokenEnhancer).userApprovalHandler(userApprovalHandler)
.authenticationManager(authenticationManager);
}
}
I have a Spring Boot project in which I've configured a Spring OAuth2 authentication process which partially works. I can authenticate OK but when I'm trying to get a refresh token I get an exception.
OAuth configuration:
#Configuration
public class OAuth2ServerConfiguration {
private static final String RESOURCE_ID = "xxx";
#Configuration
#EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID);
}
#Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/api/**").authenticated();
}
}
#Configuration
#EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
#Value("${clientDetailsService.clientName}")
private String clientName;
#Value("${clientDetailsService.clientSecret}")
private String clientSecret;
#Autowired
#Qualifier("authenticationManager")
private AuthenticationManager authenticationManager;
#Autowired
private ClientDetailsService clientDetailsService;
#Autowired
#Qualifier("tokenServices")
private AuthorizationServerTokenServices tokenServices;
#Autowired
#Qualifier("codeServices")
private AuthorizationCodeServices codeServices;
#Autowired
#Qualifier("requestFactory")
private OAuth2RequestFactory requestFactory;
#Autowired
#Qualifier("tokenGranter")
private TokenGranter tokenGranter;
private final TokenStore tokenStore = new InMemoryTokenStore();
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.setClientDetailsService(clientDetailsService);
endpoints.tokenServices(tokenServices)
.tokenStore(tokenStore)
.authorizationCodeServices(codeServices)
.authenticationManager(authenticationManager)
.requestFactory(requestFactory)
.tokenGranter(tokenGranter);
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService);
}
#Bean(name = "tokenGranter")
#Primary
public TokenGranter tokenGranter() {
final List<TokenGranter> tokenGranters = new ArrayList<TokenGranter>();
tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, codeServices, clientDetailsService, requestFactory));
tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetailsService, requestFactory));
tokenGranters.add(new ImplicitTokenGranter(tokenServices, clientDetailsService, requestFactory));
tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetailsService, requestFactory));
tokenGranters.add(new CustomTokenGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory));
return new CompositeTokenGranter(tokenGranters);
}
#Bean
#Primary
public ClientDetailsService clientDetailsService(){
final InMemoryClientDetailsServiceBuilder builder = new InMemoryClientDetailsServiceBuilder();
builder.withClient(clientName)
.authorizedGrantTypes("password", "refresh_token")
.authorities("USER")
.scopes("read", "write")
.resourceIds(RESOURCE_ID)
.secret(clientSecret);
try {
return builder.build();
} catch (final Exception e) {
e.printStackTrace();
}
return null;
}
#Bean(name = "tokenServices")
#Primary
public AuthorizationServerTokenServices tokenServices() {
final DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setSupportRefreshToken(true);
tokenServices.setClientDetailsService(clientDetailsService);
tokenServices.setTokenStore(tokenStore);
tokenServices.setAuthenticationManager(authenticationManager);
return tokenServices;
}
#Bean(name = "requestFactory")
#Primary
public OAuth2RequestFactory requestFactory() {
return new DefaultOAuth2RequestFactory(clientDetailsService);
}
#Bean(name = "codeServices")
#Primary
public AuthorizationCodeServices authorizationCodeServices() {
return new InMemoryAuthorizationCodeServices();
}
}
I also have some custom components defined, like a custom Token Granter, custom authentication provider etc. I'll post them if necessary.
As I said, authentication flow works OK. When I POST to /oauth/token I get a token and a refresh token, but when I then try to exchange my refresh token for a new token (by POSTing http://localhost:8080/oauth/token with grant_type=refresh_token and refresh_token=my refresh token) I get an exception:
No AuthenticationProvider found for org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
Where do I set the authentication provider? How do I get Spring to use my custom authentication provider for refresh tokens also?
I fixed this by explicitly defining a PreAuthenticationProvider:
#Component("preAuthProvider")
public class CustomPreAuthProvider extends PreAuthenticatedAuthenticationProvider {
#Autowired
private AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> userService;
public CustomPreAuthProvider(){
super();
}
#PostConstruct
public void init(){
super.setPreAuthenticatedUserDetailsService(userService);
}
}
and then a custom userservice:
#Service
public class CustomPreAuthUserDetailsService implements AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
#Override
public final UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken token) {
...
}
}
and adding this provider to the oauth2 config:
#Autowired
private AuthenticationProvider authenticationProvider;
#Autowired
#Qualifier("preAuthProvider")
private AuthenticationProvider preAuthProvider;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider).authenticationProvider(preAuthProvider);
}
As an alternative way to Jesper's answer, if you want to reuse your current UserDetailsService for this purpose, you can just do it the same way as Spring does it with their DefaultTokenServices:
#Bean
public CustomTokenServices tokenServices() {
CustomTokenServices tokenServices = new CustomTokenServices();
tokenServices.setTokenStore(tokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setReuseRefreshToken(false);
tokenServices.setClientDetailsService(clientDetailsService);
tokenServices.setAuthenticationManager(createPreAuthProvider());
return tokenServices;
}
private ProviderManager createPreAuthProvider() {
PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
provider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper<>(userDetailsService));
return new ProviderManager(Arrays.asList(provider));
}
UserDetailsService is autowired into this #Configuration class.
I can not be disagree with Jesper's answer, but in my case, that same error has been fixed removing:
tokenServices.setAuthenticationManager(authenticationManager)
from tokenService()
I have a web application with 2 types of resources.
web pages
web services
I want to secure the web pages using one authentication provider (i.e. CAS) and the web services using another authentication provider (i.e. BASIC authentication).
I found a solution which could work here, but it uses XML, and I would prefer to not use XML configuration if possible.
Is there a Java Config solution to this?
Well it took a while to figure out how to do it...
Basically I split up my original security configuration class into 3 separate configuration classes.
This is basically how I did it...
The main security configuration...
#Configuration
#Import({WebPageSecurityConfig.class, WebServiceSecurityConfig.class})
public class SecurityConfig {
}
The security configuration for web pages... (URL does not begin with /service/**)
#Configuration
#Order(200)
#EnableWebMvcSecurity
public class WebPageSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
public void configureGlobal(final AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(casAuthenticationProvider());
}
#Override
public void configure(final HttpSecurity http) throws Exception {
http.csrf().disable();
http.requestMatcher(new RequestMatcher() {
#Override
public boolean matches(final HttpServletRequest request) {
final String url = request.getServletPath() + StringUtils.defaultString(request.getPathInfo());
return !(url.startsWith("/service/"));
}
});
http.addFilter(casAuthenticationFilter()).exceptionHandling().authenticationEntryPoint(casAuthenticationEntryPoint());
http.authorizeRequests().
antMatchers("/securedPage").hasAuthority("ROLE_CAS_USER"). // /securedPage can only be accessed by cas user
anyRequest().permitAll(); // all other pages are unsecured
}
// General Application Security (CAS Authentication)
#Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
final CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
casAuthenticationFilter.setAuthenticationManager(authenticationManager());
return casAuthenticationFilter;
}
#Bean
public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
final CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint();
casAuthenticationEntryPoint.setLoginUrl(env.getRequiredProperty("cas.server.url") + "/login");
casAuthenticationEntryPoint.setServiceProperties(casServiceProperties());
return casAuthenticationEntryPoint;
}
#Bean
public ServiceProperties casServiceProperties() {
final ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService(env.getRequiredProperty("cas.service.url") + "/j_spring_cas_security_check");
serviceProperties.setSendRenew(false);
return serviceProperties;
}
#Bean
public CasAuthenticationProvider casAuthenticationProvider() {
final CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
casAuthenticationProvider.setAuthenticationUserDetailsService(casAuthenticationUserDetailsService());
casAuthenticationProvider.setServiceProperties(casServiceProperties());
casAuthenticationProvider.setTicketValidator(casTicketValidator());
casAuthenticationProvider.setKey("casAuthenticationProviderKey");
casAuthenticationProvider.setStatelessTicketCache(casStatelessTicketCache());
return casAuthenticationProvider;
}
#Bean
public AuthenticationUserDetailsService casAuthenticationUserDetailsService() {
final AbstractCasAssertionUserDetailsService authenticationUserDetailsService = new AbstractCasAssertionUserDetailsService() {
#Override
protected UserDetails loadUserDetails(final Assertion assertion) {
final String username = assertion.getPrincipal().getName();
final List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_CAS_USER"));
return new User(username, "notused", authorities);
}
};
return authenticationUserDetailsService;
}
#Bean
public TicketValidator casTicketValidator() {
final Saml11TicketValidator ticketValidator = new Saml11TicketValidator(env.getRequiredProperty("cas.server.url"));
ticketValidator.setTolerance(env.getRequiredProperty("cas.ticket.tolerance", Long.class));
return ticketValidator;
}
#Bean
public StatelessTicketCache casStatelessTicketCache() {
final EhCacheBasedTicketCache ticketCache = new EhCacheBasedTicketCache();
ticketCache.setCache(casCache());
return ticketCache;
}
#Bean(initMethod = "initialise", destroyMethod = "dispose")
public Cache casCache() {
final Cache cache = new Cache("casTickets", 50, true, false, 3600, 900);
return cache;
}
#Autowired
private Environment env;
}
The security configuration for RESTful web services (URL starts with /service/**)
#Configuration
#Order(300)
#EnableWebMvcSecurity
public class WebServiceSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
public void configureGlobal(final AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().
withUser("admin").password("password").authorities(new SimpleGrantedAuthority("ROLE_WS_USER"));
}
#Override
public void configure(final HttpSecurity http) throws Exception {
http.csrf().disable();
http.
antMatcher("/service/**"). // only process URLs that begin with /service/
sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and(). // RESTful web services are stateless
addFilter(wsAuthenticationFilter()).exceptionHandling().authenticationEntryPoint(wsAuthenticationEntryPoint());
http.authorizeRequests().anyRequest().hasAuthority("ROLE_WS_USER"); // all requests are secured
}
// Web Service Security (BASIC Authentication)
#Bean
public BasicAuthenticationFilter wsAuthenticationFilter() throws Exception {
final BasicAuthenticationFilter wsAuthenticationFilter = new BasicAuthenticationFilter(authenticationManager(), wsAuthenticationEntryPoint());
return wsAuthenticationFilter;
}
#Bean
public BasicAuthenticationEntryPoint wsAuthenticationEntryPoint() {
final BasicAuthenticationEntryPoint wsAuthenticationEntryPoint = new BasicAuthenticationEntryPoint();
wsAuthenticationEntryPoint.setRealmName("My Realm");
return wsAuthenticationEntryPoint;
}
#Autowired
private Environment env;
}
It's explain how to create multiple securities in the docs
http://docs.spring.io/spring-security/site/docs/current/reference/html/jc.html#multiple-httpsecurity
something like this should work
#EnableWebSecurity
public class MultiHttpSecurityConfig {
#Configuration
#Order(1)
public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Resource private UserDetailsService userBasicAuthService;
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/api/**")
.userDetailsService(userBasicAuthService)
.authorizeRequests()
.and()
.httpBasic();
}
}
#Configuration
public static class PagesWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
#Resource private UserDetailsService userCasService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/pages/**")
.userDetailsService(userCasService)
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin();
}
}
}