3

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!

deduper
  • 1,944
  • 9
  • 22
  • read https://stackoverflow.com/questions/49181881/keycloak-forgot-password-email-link – sandes Sep 21 '20 at 07:48
  • @sandes how does this help here? the redirection from keycloak login page via link "forgot password" to the application is working but on the web app side security configuration doesn't fit. – Steffen Harbich Sep 23 '20 at 07:57
  • So did you get this to work eventually @user7372914? – deduper Sep 25 '20 at 13:04
  • @deduper no, we are still having the problem. – Steffen Harbich Sep 26 '20 at 12:03
  • „*...we are still having the problem...*“ – @SteffenHarbich — I worked on a project earlier this year that solved the same problem. Let me know if the already-proposed answer isn't satisfactory? I'll dig out that project, adapt it to an [*MRE*](https://stackoverflow.com/help/minimal-reproducible-example) and propose it as an alternative solution. TIA. P.S. — You *do* formally click *Accept* on answers if you implement them and they unblock you? Right? – deduper Sep 26 '20 at 12:16
  • @deduper yes, I am familar with SO mechanics :) ... the issue is not solved so far and I doubt we will fix it with the information given in the answer because we tried it already that way. An MRE would really help and worth the bounty. – Steffen Harbich Sep 26 '20 at 12:26
  • „*...yes, I am familar with SO mechanics :)...*“ – @SteffenHarbich — Cool! Don't get me wrong. It's not you. Ordinarily, I immediately and unconditionally volunteer to help people out. But lately, I've become a little gun-shy from [*recent encounters*](https://stackoverflow.com/a/63926746/4465539) with what SO calls „[*Help Vampires*](https://meta.stackexchange.com/questions/19665/the-help-vampire-problem)“. So, that question was just a reflex. I'll get back to you in a tick. BTW, is extending *`KeycloakWebSecurityConfigurerAdapter`* a mandatory constraint? TIA. – deduper Sep 26 '20 at 12:40
  • @deduper it's not mandatory as long as redirection to keycloak is still working when navigating to an URL that needs to be authenticated – Steffen Harbich Sep 26 '20 at 12:57

2 Answers2

2

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.

deduper
  • 1,944
  • 9
  • 22
  • 1
    Alright, your MRE is working for me. Thanks! We will now check the differences to our app and report back. The bounty is already over but I will set up a new one for you. – Steffen Harbich Sep 29 '20 at 12:04
  • „*…your MRE is working for me…*“ – @SteffenHarbich — Hey! That's awesome news! Stripping down the original to the simpler MRE brought back some valuable memories of a couple things I'd since forgotten. And that's always a good thing for reinforcing stronger memories for the future. So have an upvote :) — „*…I will set up a new one for you…*“ — Cool! Thanks again. Here's hoping the solution unblocks you. – deduper Sep 29 '20 at 12:28
  • 1
    With the help of your sample application i started to investigate our problem in more detail. I found out, that there seems to be no difference. This led me to analyze deeper the failure messages and their origin. So i finally recognized the problem: In our complexer application we often make requests to the currentUser, which finally was the cause of our problem. So the ui application itself always worked as desired, but requests to currentUser were redirected to keycloak. Its a little bit embarrassing, but thank you so much for your help! – user7372914 Oct 06 '20 at 13:49
  • „*…With the help of your sample application…thank you so much for your help!…*“ – @user7372914 — You're more than welcome. — „*…In our complexer application…*“ — Sounds…umm…complex? :¬) I'm glad the MRE helped. – deduper Oct 06 '20 at 14:51
0

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.

Michael Ushakov
  • 1,639
  • 1
  • 10
  • 18
  • Can you share more of your configuration class? Do you derive from `KeycloakWebSecurityConfigurerAdapter`? Do you make a `super` call? – Steffen Harbich Sep 26 '20 at 12:01
  • @SteffenHarbich, I've updated my answer and attached full security config file – Michael Ushakov Sep 26 '20 at 16:39
  • Unfortunately this is no solution for us. How does the application get in connection with keycloak here? And what about AppAuthenticationEntryPoint? Are these your classes? – user7372914 Sep 28 '20 at 08:52
  • @user7372914, My app interacts with keycloak using PKCE (https://oauth.net/2/pkce/). I've cinfigured keycloak endpoint, secret, and so on therefo spring security handle every request using token from HTTP header. For what purposes do you need AppAuthenticationEntryPoint? It handles error, but if you ask i could attach it to my answer. – Michael Ushakov Sep 28 '20 at 12:28
  • @user7372914, I've edited my answer and attach as much as possible – Michael Ushakov Sep 28 '20 at 12:59