Spring boot Whitelabel page error for unauthorized ldap group access - spring-boot

I'm developing a web application in spring boot 2.0.0 and spring security 5.0.6 and I've had perfect luck with it until I added some logic that validates that the user is part of an LDAP group. If the user provides invalid credentials, the login page is shown again with a validation error displayed, but when the credentials are correct but the user is not part of the required LDAP group, the application is redirected to a Whitelabel Error Page that, along with that title in bold letters shows this:
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Fri Feb 22 18:40:55 EST 2019
There was an unexpected error (type=Forbidden, status=403).
Forbidden
This is the correct error, so I know the authentication is working. But I want to stay on the login page and not redirect, and I cannot figure out how to do that.
My entire configure method for my WebSecurityConfigurerAdapter is shown here below:
#Override
protected void configure(HttpSecurity http) throws Exception {
// Disabling CSRF because it causes issues with API requests (POSTs don't
// contain the CSRF tokens).
http.csrf().disable();
http.headers().frameOptions().disable();
http.authorizeRequests()
// This line turns off authentication for all management endpoints, which means all
// endpoints that start with "/actuator" (the default starter path for management endpoints
// in spring boot applications). To selectively choose which endpoints to exclude from authentication,
// use the EndpointRequest.to(String ... method, as in the following example:
// .requestMatchers(EndpointRequest.to("beans", "info", "health", "jolokia")).permitAll()
.requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
// Do not authenticate resource requests
.antMatchers(
"/app/css/**",
"/app/img/**",
"/app/js/**",
"/app/bootstrap/**").permitAll()
.antMatchers(
"/admin/**",
"/app/builds/**",
"/app/monitor/**",
"/app/review/**")
.hasRole(requiredRole)
// All other requests are authenticated
.anyRequest().authenticated()
// Any unauthenticated request is forwarded to the login page
.and()
.formLogin()
.loginPage(LOGIN_FORM)
.permitAll()
.successHandler(successHandler())
.and()
.exceptionHandling()
.authenticationEntryPoint(delegatingAuthenticationEntryPoint())
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher(LOGOUT_FORM))
.logoutSuccessUrl(LOGIN_FORM);
}
I'm open to critique of the construct of this entire method, btw--I've taken over this project and this is new to me. This code was working perfectly before I introduced the 6 lines ending with .hasRole(requiredRole), and it still works as long as the user is part of the required group.
I haven't provided the source for some of the methods that are called here, and I'm happy to paste them if someone would like. I'm guessing someone that knows this stuff well will spot the problem right away.
Any advice would be appreciated.

The solution I landed upon is similar to those I've found by googling around. I created an error controller that resolves to /error. At first I didn't think my error handler was ever being reached because I wasn't properly configured in my editor to debug a spring boot application, but I resolved that this morning. I added an error controller that looks like this:
#Controller
public class MyErrorController implements ErrorController {
private static final String ERROR_PATH = "/error";
#Autowired
private ErrorAttributes errorAttributes;
#RequestMapping(value = ERROR_PATH)
public String error(WebRequest request, HttpServletRequest httpServletRequest) {
Map<String, Object> attrs = this.errorAttributes.getErrorAttributes(request, false);
Integer status = (Integer) attrs.get("status");
if (status != null && status == 403) {
httpServletRequest.setAttribute("unauthorized", true);
}
return "/app/login";
}
#Override
public String getErrorPath() {
return ERROR_PATH;
}
}
So whenever an unauthorized attempt is made, this controller is hit and the condition of a status 403 triggers the logical branch in this method. I added the request attribute "unauthorized" and forward to the login jsp page, to which I've added a section that detects the presence of the request attribute. Voila, I'm all set.

Related

Default 401 instead of redirecting for OAuth2 login Spring Security

I support two types of authentication and need to return 401 for most paths instead of redirects. For Keycloak I used the HttpUnauthorizedEntryPoint below and its fine, but for the OAuth2 login, it prevents the automatic redirect (on "/auth/challenge" in my case) to "/oauth2/authorization/azure" on NegatedRequestMatcher(/login, and some other things) to be put in place. The valid process is reflected in logs below:
org.springframework.security.web.util.matcher.NegatedRequestMatcher: matches = true
org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint: Match found! Executing org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint#112c824c
org.springframework.security.web.context.SecurityContextPersistenceFilter: SecurityContextHolder now cleared, as request processing completed org.springframework.security.web.DefaultRedirectStrategy: Redirecting to 'http://localhost:2222/oauth2/authorization/azure'
This is the code that adds the Ouath2 bit to the common configuration:
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http.commonConfiguration()
.exceptionHandling()
.authenticationEntryPoint(HttpUnauthorizedEntryPoint())
.and()
.oauth2Login()
.userInfoEndpoint()
.oidcUserService(userService)
}
public class HttpUnauthorizedEntryPoint implements AuthenticationEntryPoint {
private static final Logger LOG = LoggerFactory.getLogger(HttpUnauthorizedEntryPoint.class);
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException arg) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "This endpoint requires authorization.");
}
}
The question is, how can I by default return 401 and let all the OAuth2 redirects to be placed under the hood?
Thanks in advance.
I know that because of the HttpUnauthorizedEntryPoint the DelegatingAuthenticationEntryPoint is not filled in by Spring Security. I tried to add this manually, but I would rather have this process done by Spring.
val entryPoints = LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>()
entryPoints[loginPageMatcher] = LoginUrlAuthenticationEntryPoint("/oauth2/authorization/azure")
val loginEntryPoint = DelegatingAuthenticationEntryPoint(entryPoints)
loginEntryPoint.setDefaultEntryPoint(HttpUnauthorizedEntryPoint())
My guess is you serve both a REST API and server-side rendered UI (Thymeleaf, JSF or whatever) and you want to return
401 for unauthorized requests to #RestController
302 (redirect to login) for unauthorized requests to UI pages.
Define two SecurityFilterChain beans:
first with client configuration restricted to UI paths
#Order(Ordered.HIGHEST_PRECEDENCE)
http.securityMatcher(new OrRequestMatcher(new AntPathRequestMatcher("/login/**"), new AntPathRequestMatcher("/oauth2/**"), ...); // add here all paths to UI resources
http.oauth2Login(); // and all client configuration you need for UI
http.authorizeHttpRequests().requestMatchers("/login/**").permitAll().requestMatchers("/oauth2/**").permitAll().anyRequest().authenticated();
second being default with resource-server configuration (no securityMatcher and an order greater than HIGHEST_PRECEDENCE)
Details in this answer and that tutorial

Spring Boot OAuth2 errors after invalidating session and accessing data from Resource Server

My architecture consists of three applications:
Authorization Server (10.10.1.1:8080)
Client A (10.10.3.3:8084)
Client B (10.10.2.2:8089)
Both Client A and B are serving static Angular files. Both clients are "communicating" with each other - it is possible to navigate from the first one to another one and back as well (through normal window.location.replace).
Everything works great instead of one specific situation.
I'm logging into Client A application (through Authorization server redirect).
I'm opening Client B - user is properly authenticated based on Client A.
I'm coming back to Client A - user is still authenticated. (I can repeat steps 2 and 3 endlessly)
I'm logging out from Client A.
I'm logging in again into Client A with the same or different user.
I'm opening Client B and getting blank page due to some network issues.
After page refresh everything works fine (JSESSIONID is changing in the browser and user is properly authenticated).
I've tried couple different approaches and configurations using session invalidation. Session is properly invalidated but then it is not created again (user is changing to anonymous instead of being properly taken from Client A).
Then follows redirect to authorization server, which isn't available for some reason.
The problem here is that normal flow after redirect (step 2) is:
Redirect to 10.10.2.2:8089/home-page
/home-page gets 302 REDIRECT in network tab to /login
/login redirects to 10.10.1.1:8080/oauth/authorize
then it redirects back to /home-page with status 200 OK.
Error flow after redirect (step 6) is:
Redirect to 10.10.2.2:8089/home-page
/home-page gets 200 OK in network tab
application loads the page and it makes request for user data (/api/user) which gets 401
the entire redirect cycle takes place and ends with unability to redirect to 10.10.1.1:8080/oauth/authorize
after page refresh everything works fine.
I've tried:
couple different approaches and configurations in security (both on Client A and Client B)
allowing all origins in CorsFilter (for testing purposes - even that didn't help)
adding another cookie through server.servlet.cookie.name and erasing it by deleteCookies() or proper handler
adding maximumSessions(2) for tests purposes - even that didn't help
At last I made some tricky solution. I made request to Client B before redirect to Client A. It removed JSESSIONID through HttpServletResponse. It helped, but only when I'm working on one browser tab.
If I have two tabs opened (one with Client A and one with Client B) after doing step 5 and 6 and refreshing the page on Client B, problem still persists (because I didn't erase JSESSIONID from the browser).
I don't know if I understand this problem properly (that JSESSIONID is problematic in the browser), so correct me if I'm wrong. Also - I don't know how to erase this cookie or allow OAuth2 Filters to automatically create new one and invalidate the session in proper way.
Can anybody help me with this problem and show what I'm doing wrong here?
Client A - Security Configuration
#Configuration
#EnableOAuth2Sso
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public OAuth2RestTemplate oauth2RestTemplate(final OAuth2ClientContext context,
final OAuth2ProtectedResourceDetails details) {
return new OAuth2RestTemplate(details, context);
}
#Override
public void configure(final HttpSecurity http) throws Exception {
http
.formLogin()
.and()
.logout().clearAuthentication(true)
.invalidateHttpSession(true)
// .deleteCookies("JSESSIONID")
// .logoutSuccessHandler((httpServletRequest, httpServletResponse, authentication) -> {httpServletResponse.setStatus(HttpServletResponse.SC_OK);})
.addLogoutHandler(new ProperCookieClearingLogoutHandler("JSESSIONID"))
.and()
.authorizeRequests()
.antMatchers("/index.html", "/main.html", "/login", "/resources/**", ...)
.permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
Client B - Security Configuration
#Configuration
#EnableOAuth2Sso
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public OAuth2RestTemplate oauth2RestTemplate(final OAuth2ClientContext context,
final OAuth2ProtectedResourceDetails details) {
return new OAuth2RestTemplate(details, context);
}
#Override
public void configure(HttpSecurity http) throws Exception {
http
.logout().clearAuthentication(true)
.invalidateHttpSession(true)
// .deleteCookies("JSESSIONID")
// .logoutSuccessHandler((httpServletRequest, httpServletResponse, authentication) -> {httpServletResponse.setStatus(HttpServletResponse.SC_OK);})
.addLogoutHandler(new ProperCookieClearingLogoutHandler("JSESSIONID"))
.and()
.authorizeRequests()
.antMatchers("/index.html", "/main.html", "/resources/**", "/login/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
Authorization Server - Security Configuration
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login")
.permitAll().and()
.formLogin().failureHandler(new FailureAuthenticationHandler())
.loginPage("/login").permitAll()
.and().requestMatchers()
.antMatchers("/login")
.antMatchers("/oauth/authorize")
.antMatchers("/oauth/confirm_access")
.and()
.anyRequest().authenticated()
.and().sessionManagement().maximumSessions(-1).expiredUrl("/...").sessionRegistry(sessionRegistry());
}
#Bean
SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
... authentication providers ...
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
CustomTokenStore customTokenStore;
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient("x").secret("x")
.authorizedGrantTypes("x").autoApprove(true).scopes("x");
}
#Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.tokenStore(customTokenStore).authenticationManager(authenticationManager);
}
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer.allowFormAuthenticationForClients();
}
}
Full log from Client B:
https://pastebin.com/aVc5AXcx
Thank you in advance.
24.10.2020 - TOPIC EDIT
After digging deeper and doing some research I probably found a core of the problem, but I don't know how to solve it yet.
First problem
There are two OAuth2 clients (annoted with #EnableOAuth2Sso) that share the same session and user data, but they don't know about each other openly (and about each other login/logout state).
I'm working on Client B and triggering logout call on that specific client.
Then I'm redirecting to authorization server login page with specific logout params.
I'm making logout call to authorization server on POST method and /logout path.
After successfull user logout I'm doing window.location.replace to Client A, which gets unauthorized error in network tab (401):
WWW-Authenticate header: Bearer realm="oauth2-resource", error="invalid_token", error_description="Invalid access token: 27ef8abe-e8e5-4d07-aaf4-a82a8757614e"
And in console of Client A I get:
UserInfoTokenServices: Could not fetch user details: class org.springframework.security.oauth2.client.resource.UserRedirectRequiredException, A redirect is required to get the users approval.
Second problem
Similiar situation is in the base problem stated in this topic. After relogin on Client A and page refresh on Client B, it has some session/token in cache and think that user is still authenticated in that client. It returns status 200 OK in HTML routing path (f.e. /home-page), but gets unauthorized on first request to API and giving the same invalid_token header:
WWW-Authenticate header: Bearer realm="oauth2-resource", error="invalid_token", error_description="Invalid access token: 4026cf9f-8081-4870-b9bf-6e6ff89d4ded" (401)
And in Resource Server I get:
Unable to obtain a new access token for resource 'null'. The provider manager is not configured to support it.
Both clients have the same configuration in properties
security.oauth2.client.client-id=x
security.oauth2.client.client-secret=y
security.oauth2.client.user-authorization-uri=http://${auhorization.server.url}/oauth/authorize
security.oauth2.client.access-token-uri=http://${auhorization.server.url}/oauth/token
security.oauth2.resource.user-info-uri=http://${resource.server.url}/user
Conclusion
I've tried adding csrfHeaderFilter and OAuth2ClientContextFilter from this topic, but it didn't help.
https://github.com/spring-guides/tut-spring-security-and-angular-js/issues/76
So the question is how to handle logout / refresh session and user context in another client after logout from the second one (and authorization server)? I don't know if I'm getting this process right, but I'm still anylizing what's going on here...
Can anybody show me some solution?

How to return http status code instead of login page in multi HttpSecurity case

I have a spring boot app which provides HTML page service via / and also rest api via /api. The former requires login via a Login form and the later requires HTTP basic auth, and hereby, I configure two HttpSecurity section as follows:
#Configuration
#Order(1)
public static class ApiSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/api/**")
.cors().and().csrf().disable()
.authorizeRequests().anyRequest().authenticated()
.and()
.httpBasic(Customizer.withDefaults());
}
}
#Configuration
public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.cors().disable()
.authorizeRequests()
.antMatchers("/login", "/js/**", "/css/**").permitAll()
.antMatchers("/**").hasRole("ADMIN")
.and().formLogin().loginPage("/login").permitAll().defaultSuccessUrl("/index")
.and().logout().invalidateHttpSession(true)
.deleteCookies("JSESSIONID").logoutSuccessUrl("/login").permitAll();
}
}
The configuration works perfectly in normal case. However, if wrong credentials are provides in rest api clients, an HTML code of login page with HTTP status code 200 returns from the app instead of an excepted json message with HTTP status code 401 or 403.
I'm afraid it is because the url pattern of /api/** both matches "/api/**" and "/**", and therefore, the request will pass both the filter chain for rest api and HTML page. Finally, because of the lower order of formLogin, a login page returns in the case.
So, How can I get the excepted result for rest api clients? Is there an only solution to separate the two url patterns which should not match each other?
Addition 1:
I think there are three cases which will raise exceptions in the security filter chain:
No credentials provided;
Wrong credentials provided;
Right credentials provided but not matched the required roles
And the results for the cases are as follows:
Return HTTP status 401 with a json error message
Return HTTP status 302 and try to redirect to login page
Return HTTP status 403 with a json error message
Therefore, it seems that only the case of wrong credentials provided will
be routed to /error endpoint (as what Eleftheria said in the answer), and the difference between 1,3 and 2 is the exception type -- org.springframework.security.access.AccessDeniedException: Access is denied for 1 and 3; org.springframework.security.authentication.BadCredentialsException: Bad credentials for 2.
In the formLogin() case, if the BadCredentialsException rises, it will be routed to the failure-url, but how to configure the failure-url in the httpbasic case? (seems no such method in HttpBasicConfigurer)
This is happening because the failed authentication is throwing an exception and the "/error" page is secured by your second filter chain.
Try permitting all requests to "/error" in your second filter chain.
http.antMatcher("/**")
.authorizeRequests()
.antMatchers("/error").permitAll()
// ....
Note that the request will only be processed by one filter chain, and that is the first filter chain that it matches.
The request to "/api/123" is only processed by the first filter chain, however the second filter chain was invoked because there was an error.

Spring Security OAuth - how to disable login page?

I want to secure my application with Spring Security, using OAuth 2. However, I don't want the server to redirect incoming unauthorized requests, but instead to respond with HTTP 401. Is it possible?
Example: this code redirects requests to a default login page.
application.properties
spring.security.oauth2.client.registration.google.client-id=...
spring.security.oauth2.client.registration.google.client-secret=...
AuthConfig.java
#Configuration
public class AuthConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/secured/**").authenticated()
.anyRequest().permitAll()
.and()
.oauth2Login();
// https://stackoverflow.com/questions/31714585/spring-security-disable-login-page-redirect
// deos not work
// .and()
// .formLogin().successHandler((request, response, authentication) -> {});
}
}
You need to create new authentication entry point and set it in configuration.
#Configuration
public class AuthConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new AuthenticationEntryPoint())
.and()
.authorizeRequests()
.antMatchers("/secured/**").authenticated()
.anyRequest().permitAll()
.and()
.oauth2Login();
}
}
public class AuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {
public AuthenticationEntryPoint() {
super("");
}
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.sendError(401, "Unauthorized");
}
}
You need to set oauth2Login.loginPage in your HttpSecurity config and create a controller mapping to return whatever you want. Here's a simple example.
So in your security config
http
.authorizeRequests()
.antMatchers("/noauth").permitAll()
.oauth2Login()
.loginPage("/noauth")
In a controller
#GetMapping("/noauth")
public ResponseEntity<?> noAuth() {
Map<String, String> body = new HashMap<>();
body.put("message", "unauthorized");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(body);
}
You can pass a map or pojo to the body method.
I would like to expand on Petr's answer by explaining that apparently for the time being first of all, the default login page is shown when there are more than one OAuth2 configured providers. I would expect that Spring Boot would have a smart trick to bypass this page easily and choose the right provider automatically, basing e.g. on the existence of the provider's client ID in the original request. I found out the hard way that this is not the case. So the way to do this is.. this not very apparent trick of providing a custom handler for failures - that will REDIRECT the user to the correct OAuth2 endpoint for each provider, based on the original HTTP request URL. I tried this and it works and I spent a whole day trying all manners of other solutions - my original scenario was to pass additional parameters to OAuth2 scheme in order to be able to get them back on successful authentication - they used to do this appending Base64 encoded information to the "state" URL request parameter, but Spring Security does not allow this at the moment. So the only alternative was to call a Spring Security-protected URL with those parameters already there, so when the successful authentication happens, this URL is accessed again automatically with those parameters intact.
Related: Multiple Login endpoints Spring Security OAuth2

Spring Boot Custom Authorization Header request not going through at all

So I have a Spring Boot application and I am sending a request to it using PostMan. It is using Spring Security along with JWT for authentication. I'm trying to get authorization to work but am running into issues. Spring is able to login the user and return a token fine. But when I put the token in the header it's not working at all. I get no response from the server. When the token is removed, it works fine. Right now all requests should be able to go through regardless of being logged in or not.
My Spring Web Configuration:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurity extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()));
}
}
The REST path I'm trying to access:
#RestController("threadService")
#RequestMapping("/api/thread")
public class ThreadService {
#RequestMapping(value="/list", method=RequestMethod.GET)
public List<ThreadDetails> getThreadList() {
logger.info("getThreadList");
return threadDao.getThreadList();
}
}
The failed GET request I'm issuing after I have logged in and gotten a token:
GET /api/thread/list HTTP/1.1
Host: localhost:8080
Authorization : Bearer (JWT token here)
Cache-Control: no-cache
Postman-Token: 69565839-4806-b4f6-9a03-11382a80c7da
The above request works fine when there is no Authorization in the header.
Not sure it is exactly the problem I was facing.
When I want to communicate with the restservice exposed by spring boot application, the "Authorization" is not set. I followed the steps which are required to communicate but the value wont be passes through header.
The solution I found, the "common-codec" library was missing. Once I add the dependency in my web application, it start sending the "Authorization" in header to my spring boot application.
Hope this helps to someone.

Resources