I have two different users in my application. Ldap users and api users. Ldap users have privilege to access an endpoint and api users a different endpoint. I have implemented the api user authentication using UserDetailsService and having the details in my application.yaml file.
The issue I am facing now is, The endpoint that only Ldap users should access is now being accessed my api users as well. How can I prevent this. Please find my code snippet below
public class ServiceSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
#Qualifier("ldapProvider")
private AuthenticationProvider authenticationProvider;
#Override
protected void configure(HttpSecurity http) throws Exception {
// security for apiuser
http
.authorizeRequests()
.antMatchers(“/abcd/**).hasRole(“admin”)
.and()
.httpBasic().and().userDetailsService(userDetailsService());
// security for ldap users
http
.csrf().disable()
.authorizeRequests()
.antMatchers(“/ghhgh” + "/**").fullyAuthenticated()
.antMatchers("/login*").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().and()
.authenticationProvider(authenticationProvider)
.exceptionHandling();
}
public UserDetailsService userDetailsService() {
UserDetails user = User.withUsername(“api”)
.password(passwordEncoder().encode(“test”))
.roles(“admin”)
return new InMemoryUserDetailsManager(user);
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
In spring security it is indeed possible to register multiple authentication mechanisms.
BUT you cannot register a specific authentication provider to a specific route.
The spring securty docs say:
ProviderManager is the most commonly used implementation of AuthenticationManager. ProviderManager delegates to a List of AuthenticationProviders. Each AuthenticationProvider has an opportunity to indicate that authentication should be successful, fail, or indicate it cannot make a decision and allow a downstream AuthenticationProvider to decide.
So in every request, the registered AuthenticationProviders are checked one after the other until one is successful, or all fail.
To solve your problem, you need to define multiple custom authorities, that you assign your users.
Then you secure your endpoints using these authorities.
E.g. you give every ldap user the authority LDAP_USER and every api user the authority API_USER. Then you configure your security accordingly:
Register all AuthenticationProviders:
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(ldapProvider);
auth.userDetailsService(userDetailsService());
}
And configure the endpoints:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
(...)
.authorizeRequests()
// security for apiuser
.antMatchers(“/abcd/**).hasRole(“API_USER”)
// security for ldap users
.antMatchers(“/ghhgh” + "/**").hasRole("LDAP_USER")
(...)
}
I have an application using a custom Jwt token implementation.
The authentication portion works just fine, with the token being created/validated just fine. My Security configuration looks like this:
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
#EnableWebSecurity
public class DJWTSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
JwtTokenProvider jwtTokenProvider;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/*").authenticated()
.antMatchers("/api/auth/signin").permitAll()
.and()
.apply(new JwtConfigurer(jwtTokenProvider));
}
}
For some reason, security is not being enforced to api requests. For requests sent without bearer header, it seems that Spring considers "anonymous" users as being authenticated. Inspecting the security context:
org.springframework.security.authentication.AnonymousAuthenticationToken#f27f7551:
Principal: anonymousUser;
Credentials: [PROTECTED];
Authenticated: true;
Details: org.springframework.security.web.authentication.WebAuthenticationDetails#b364: RemoteIpAddress: 0:0:0:0:0:0:0:1;
SessionId: null;
Granted Authorities: ROLE_ANONYMOUS
Is this expected behavior that should be fixed by adding a role constraint to the route?
Wildcard was wrong - it should be /api/***
not relevant for the given error, but the order should be the other way around
I have a spring boot oauth2 server that uses a JDBC implementation. It is configured as an authorization server with #EnableAuthorizationServer.
I'd like to scale that application horyzontally but it doesn't seem to work properly.
I can connect only if I have one instance (pods) of the server.
I use autorisation_code_client grant from another client service to get the token.
So first the client service redirect the user to the oauth2 server form, then once the user is authenticated he is supposed to be redirect to the client-service with a code attached to the url, finally the client use that code to request the oauth2 server again and obtain the token.
Here the user is not redirected at all if I have several instance of the oauth2-server. With one instance it works well.
When I check the log of the two instances in real time, I can see that the authentication works on one of them. I don't have any specific error the user is just not redirected.
Is there a way to configure the oauth2-server to be stateless or other way to fix that issue ?
Here is my configuration, the AuthorizationServerConfigurerAdapter implementation.
#Configuration
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
#Bean
#ConfigurationProperties(prefix = "spring.datasource")
public DataSource oauthDataSource() {
return DataSourceBuilder.create().build();
}
#Autowired
#Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
#Bean
public JdbcClientDetailsService clientDetailsSrv() {
return new JdbcClientDetailsService(oauthDataSource());
}
#Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(oauthDataSource());
}
#Bean
public ApprovalStore approvalStore() {
return new JdbcApprovalStore(oauthDataSource());
}
#Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new JdbcAuthorizationCodeServices(oauthDataSource());
}
#Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}
#Bean
#Primary
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore());
tokenServices.setTokenEnhancer(tokenEnhancer());
return tokenServices;
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsSrv());
}
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients();
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)
.approvalStore(approvalStore())
//.approvalStoreDisabled()
.authorizationCodeServices(authorizationCodeServices())
.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancer());
}
}
The main class
#SpringBootApplication
#EnableResourceServer
#EnableAuthorizationServer
#EnableConfigurationProperties
#EnableFeignClients("com.oauth2.proxies")
public class AuthorizationServerApplication {
public static void main(String[] args) {
SpringApplication.run(AuthorizationServerApplication.class, args);
}
}
The Web Security Configuration
#Configuration
#Order(1)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Bean
#Override
public UserDetailsService userDetailsServiceBean() throws Exception {
return new JdbcUserDetails();
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity http) throws Exception { // #formatter:off
http.requestMatchers()
.antMatchers("/",
"/login",
"/login.do",
"/registration",
"/registration/confirm/**",
"/registration/resendToken",
"/password/forgot",
"/password/change",
"/password/change/**",
"/oauth/authorize**")
.and()
.authorizeRequests()//autorise les requetes
.antMatchers(
"/",
"/login",
"/login.do",
"/registration",
"/registration/confirm/**",
"/registration/resendToken",
"/password/forgot",
"/password/change",
"/password/change/**")
.permitAll()
.and()
.requiresChannel()
.anyRequest()
.requiresSecure()
.and()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login.do")
.usernameParameter("username")
.passwordParameter("password")
.and()
.userDetailsService(userDetailsServiceBean());
} // #formatter:on
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsServiceBean()).passwordEncoder(passwordEncoder());
}
}
Client side the WebSecurityConfigurerAdapter
#EnableOAuth2Sso
#Configuration
public class UiSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.authorizeRequests()
.antMatchers(
"/",
"/index.html",
"/login**",
"/logout**",
//resources
"/assets/**",
"/static/**",
"/*.ico",
"/*.js",
"/*.json").permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().csrfTokenRepository(csrfTokenRepository())
.and()
.addFilterAfter(csrfHeaderFilter(), SessionManagementFilter.class);
}
}
the oauth2 configuration properties
oauth2-server is the service name (load balancer) on kubernetes and also the server path that is why it appears twice.
security:
oauth2:
client:
clientId: **********
clientSecret: *******
accessTokenUri: https://oauth2-server/oauth2-server/oauth/token
userAuthorizationUri: https://oauth2.mydomain.com/oauth2-server/oauth/authorize
resource:
userInfoUri: https://oauth2-server/oauth2-server/me
Here an important detail, the value of userAuthorizationUri is the address to access the oauth2-server from the outside of the k8s cluster. The client-service send back that address into the response with a 302 http code if the user is not connected and tries to access to the /login path of the client-service. then the user is redirected to the /login path of the oauth2-server.
https://oauth2.mydomain.com target an Nginx Ingress controller that handle the redirection to the load balancer service.
Here is a solution to this problem. It's not a Spring issue at all but a bad configuration of the Nginx Ingress controller.
The authentication process is done in several stages :
1 - the user clic on a login button that target the /login path of the client-server
2 - the client-server, if the user is not authenticated yet, send a response to the
browser with a 302 http code to redirect the user to the oauth2-server, the value of
the redirection is composed with the value of the
security.oauth2.client.userAuthorizationUri property
and the redirection url that will be used by the browser to allow the client-server to get the Token once the user is authenticated.
That url look like this :
h*tps://oauth2.mydomain.com/oauth2-server/oauth/authorize?client_id=autorisation_code_client&redirect_uri=h*tps://www.mydomain.com/login&response_type=code&state=bSWtGx
3 - the user is redirected to the previous url
4 - the oauth2-server send a 302 http code to the browser with the login url of the
oauth2-server, h*tps://oauth2.mydomain.com/oauth2-server/login
5 - the user submit his credentials and the token is created if they are correct.
6 - the user is redirected to the same address as at the step two, and the oauth-server
add informations to the redirect_uri value
7 - the user is redirected to the client-server. The redirection part of the response look like this :
location: h*tps://www.mydomain.com/login?code=gnpZ0r&state=bSWtGx
8 - the client-server contact the oauth2-server and obtain the token from the code and the state that authenticates it. It doesn't matter if the instance of the oauth2
server is different than the one used by the user to authenticate himself. Here the
client-server use the value of security.oauth2.client.accessTokenUri to get the
token, this is the internal load balancing service address that targets the oauth2 server
pods, so it doesn't pass through any Ingress controller.
So at the steps 3 to 6 the user must communicate with the same instance of the oauth2-server throught the Ingress controller in front of the load balancer service.
Its is possible by configuring the Nginx Ingress controller with a few annotations :
"annotations": {
...
"nginx.ingress.kubernetes.io/affinity": "cookie",
"nginx.ingress.kubernetes.io/session-cookie-expires": "172800",
"nginx.ingress.kubernetes.io/session-cookie-max-age": "172800",
"nginx.ingress.kubernetes.io/session-cookie-name": "route"
}
That way we ensure that the user will be redirected to the same pods/instance of the oauth2-server during the authentication process as long he's identified with the same cookie.
The affinity session mecanism is a great way to scale the authentication server and also the client-server. Once the user is authenticated he will always use the same instance of the client and keep his session informations.
Thanks to Christian Altamirano Ayala for his help.
By default an in-memory TokenStore is used.
The default InMemoryTokenStore is perfectly fine for a single server
If you want multiple pods, you probably should go for JdbcTokenStore
The JdbcTokenStore is the JDBC version of the same thing, which stores token data in a relational database. Use the JDBC version if you can share a database between servers, either scaled up instances of the same server if there is only one, or the Authorization and Resources Servers if there are multiple components. To use the JdbcTokenStore you need "spring-jdbc" on the classpath.
Source Spring Security: OAuth 2 Developers Guide
My WebSecurity Config is like below;
#EnableWebSecurity
#Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated().and().httpBasic();
}
#Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
builder.inMemoryAuthentication().withUser("hellouser")
.password("hellopass").roles("USER");
}
}
When i give wrong username, Authentication fails as expected. But, if i get success in authentication once, all other requests after that with wrong password but correct username gets authenticated successfully....
Is it getting cached somewhere?
Can i disable this feature?
Isn't it suppose to give authentication failure with wrong password?
NOTE: I am learning spring-security. I dont have any html pages in this app and testing from PostMan.
use http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); in the configure method.
#Override
protected void configure(HttpSecurity http) throws Exception {
http
//HTTP Basic authentication
.httpBasic()
.and()
....
.csrf().disable()
.formLogin().disable();
//to check password in each request
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
I was able to access the URL from below configuration using basic auth from Postman even with wrong credential, which was happening because once you provide the right credentials the credentials get stored in session and even if you repeats the same request the same session will be used to access the URL.
http
.httpBasic()
.and()
.authorizeRequests()
.antMatchers("/secure/admin/**").hasRole("ADMIN")
.antMatchers("/api/**","/secure/getUserByName/**").hasAnyRole("USER","ADMIN")
.anyRequest().fullyAuthenticated();
Solution:
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
Just add the above code. So this configuration assures that only a single instance of a user is authenticated at a time. And if the same user tries to access the URL then it's previous session is terminated and then the user has to provide login credentials again for which new session is created.
We are using a two-pronged approach to user authentication where we are using an in-memory authentication token store in clustered environment and saving the token in a shared database as well. Now I am able to get token using the in-memory store and authenticate users. Everything is working as expected.
I am looking for the following things :
Capture the token and save it in the database? How to achieve this with Spring Security (this should happen after successful authentication)?
If server restarts then I should still be able to validate the token from database.(if user name password is correct.)
#Component
public class CustomAuthSuccessHandler implements AuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
System.out.println(request);
System.out.println(response);
}
}
Following are my HTTP settings:
#Override
public void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(customAuthenticationEntryPoint)
.and()
.logout()
.logoutUrl("/oauth/logout")
.logoutSuccessHandler(customLogoutSuccessHandler)
.and()
.csrf()
.requireCsrfProtectionMatcher(
new AntPathRequestMatcher("/oauth/authorize"))
.disable().headers().frameOptions().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests().antMatchers("/hello/")
.permitAll().antMatchers("/secure/**").authenticated();
}
PS : It looks like adding an interceptor can help, however I am not sure how do I to get token from response. Any suggestions?