Reputation: 39
We have a Keycloak server that is securing our Spring Boot application. That works fine so far. However we now need a forgot password page, which has to be reachable without login of course. We are not able to accomplish this.
We are implementing a KeycloakWebSecurityConfigurerAdapter
and overriding the configure(HttpSecurity)
method. Implementation looks like this:
super.configure(http);
http.csrf().disable()
.exceptionHandling()
.accessDeniedPage("/accessDenied");
http.anonymous.disable();
http.authorizeRequests();
With that code only, indeed every page is freely accessible, except the root page. As soon as we add calls to antMatcher()
or anyRequest()
method followed by permitAll()
or fullyAuthenticated()
, just to achieve the differentiation in allowed and disallowed pages, all pages are secured/disallowed. We played around a lot and tried to find help here and anywhere else but found no solution. Current implemented example is:
http.authorizeRequests().antMatchers(HttpMethod.GET, "/public/forgotPassword").permitAll()
.anyRequest().fullyAuthenticated();
The result is, as stated, that every pages needs authentication, also the public/forgotPassword page. Does anyone have an idea about what the problem might be?
Thx in advance!
Upvotes: 3
Views: 1495
Reputation: 1964
I've implemented this springboot.keycloak.mre1 to demonstrate — in a stripped-down way — how a previous project I worked on similarly implemented what I think you're requesting.
In a nutshell, the gist of the solution is…
…
public class SecurityConfiguration extends KeycloakWebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests().antMatchers("/login", "/login.html")
.permitAll().antMatchers("/dashboard", "/dashboard.html")
.authenticated();
}
…
}
The steps to build and run the MRE are straightforward. But if you get stuck building or running it, let me know if I can help you in any way.
And if I've completely misinterpreted what you've requested, then please feel free to clone and modify the project to be more like your use case. If you then upload your modifications, and elaborate on the specifics of your use case in the repo's Issues area, I will investigate and get back to you.
1 The MRE uses docker-compose because the original project it's based on did.
Upvotes: 2
Reputation: 2779
In my applications I am using the following config scheme:
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package com.wissance.orgstructure.application.configuration;
import com.goodt.drive.goals.application.authentication.AppAuthenticationEntryPoint;
import com.goodt.drive.goals.application.services.users.KeyCloakUserInfoExtractorService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.oauth2.resource.PrincipalExtractor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.headers().frameOptions().sameOrigin(); // it is to fix issue with h2-console access
http.cors();
http.csrf().disable()
.authorizeRequests().antMatchers("/", "/callback", "/login**", "/webjars/**", "/error**").permitAll()
.and()
.authorizeRequests().antMatchers("/api/**").authenticated()
.and()
.authorizeRequests().antMatchers("/h2-console/**").permitAll()
.and()
.authorizeRequests().antMatchers("/swagger-ui.html").permitAll()
.and()
.authorizeRequests().antMatchers("/swagger-ui/**").permitAll()
.and()
.exceptionHandling().authenticationEntryPoint(new AppAuthenticationEntryPoint())
.and()
.logout().permitAll().logoutSuccessUrl("/");
}
@Bean
public PrincipalExtractor getPrincipalExtractor(){
return new KeyCloakUserInfoExtractorService();
}
@Autowired
private ResourceServerTokenServices resourceServerTokenServices;
}
@ControllerAdvice
public class AppAuthenticationEntryPoint implements AuthenticationEntryPoint{
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
// 401
logger.debug(String.format("Access to resource is denied (401) for request: \"%s\" message: \"%s\"", request.getRequestURL(), authException.getMessage()));
setResponseError(response, HttpServletResponse.SC_UNAUTHORIZED, "Authentication Failed");
}
@ExceptionHandler (value = {AccessDeniedException.class})
public void commence(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
// 403
logger.debug(String.format("Access to resource is forbidden (403) for request: \"%s\" message: \"%s\"", request.getRequestURL(), accessDeniedException.getMessage()));
setResponseError(response, HttpServletResponse.SC_FORBIDDEN, String.format("Access Denies: %s", accessDeniedException.getMessage()));
}
@ExceptionHandler (value = {NotFoundException.class})
public void commence(HttpServletRequest request, HttpServletResponse response, NotFoundException notFoundException) throws IOException {
// 404
logger.debug(String.format("Object was not found (404) for request: \"%s\" message: \"%s\"", request.getRequestURL(), notFoundException.getMessage()));
setResponseError(response, HttpServletResponse.SC_NOT_FOUND, String.format("Not found: %s", notFoundException.getMessage()));
}
@ExceptionHandler (value = {Exception.class})
public void commence(HttpServletRequest request, HttpServletResponse response, Exception exception) throws IOException {
logger.error(String.format("An error occurred during request: %s %s error message: %s",
request.getMethod(), request.getRequestURL(), exception.getMessage()));
// 500
setResponseError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, String.format("Internal Server Error: %s", exception.getMessage()));
}
private void setResponseError(HttpServletResponse response, int errorCode, String errorMessage) throws IOException{
response.setStatus(errorCode);
response.getWriter().write(errorMessage);
response.getWriter().flush();
response.getWriter().close();
}
private final Logger logger = LoggerFactory.getLogger(this.getClass());
}
Config of spring security (application-local.yml) related to KeyCloak was listed below, in my app i have at least 3 different keycloak servers and i switch them from time to time, all my KeyCloak values passes from base settings (application.yml) currently using defined in appConfig.keyCloak.using as yml placeholder to selected keycloak? example of spring security config section:
security:
basic:
enabled: false
oauth2:
client:
clientId: ${appConfig.keyCloak.using.clientId}
clientSecret: ${appConfig.keyCloak.using.clientSecret}
accessTokenUri: ${appConfig.keyCloak.using.baseUrl}/protocol/openid-connect/token
userAuthorizationUri: ${appConfig.keyCloak.using.baseUrl}/protocol/openid-connect/auth
authorizedGrantTypes: code token
scope: local
username: ${appConfig.keyCloak.using.serviceUsername}
password: ${appConfig.keyCloak.using.servicePassword}
resource:
userInfoUri: ${appConfig.keyCloak.using.baseUrl}/protocol/openid-connect/userinfo
Example of one of KeyCloak server config:
baseUrl: http://99.220.112.131:8080/auth/realms/master
clientId: api-service-agent
clientSecret: f4901a37-efda-4110-9ba5-e3ff3b221abc
serviceUsername: api-service-agent
servicePassword: x34yui9034*&1
In my above example all pages that have the /api
path in their url, i.e. /api/employee
or /api/employee/find/?
or others, are accessible only after authentication + authorization. All Swaggers pages or the login page are available without any authentication.
Upvotes: 0