In the previous article we got familiar with the common configuration of Keycloak, today we will do an analysis of the execution flow of Keycloak adapted to Spring Security and briefly understand some of its customized Spring Security filters.

Execution flow of /admin/foo

In the Spring Boot application adapted with Keycloak and Spring Security, I wrote a /admin/foo interface and configured the permissions for this interface as follows.

1
2
3
4
5
6
7
8
9
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http
                .authorizeRequests()
                .antMatchers("/customers*").hasRole("USER")
                .antMatchers("/admin/**").hasRole("base_user")
                .anyRequest().permitAll();
    }

This is a typical Spring Security configuration where a user with the base_user role has access to /admin/**. What you need to understand here is that the so-called user and base_user roles are currently managed by the Keycloak platform, and our application can currently only control the access policy to resources. In order to see the flow of execution I turned on all log printing, and when I access /admin/foo I go through the following filters.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Security filter chain: [
  WebAsyncManagerIntegrationFilter
  SecurityContextPersistenceFilter
  HeaderWriterFilter
  CsrfFilter
  KeycloakPreAuthActionsFilter
  KeycloakAuthenticationProcessingFilter
  LogoutFilter
  RequestCacheAwareFilter
  SecurityContextHolderAwareRequestFilter
  KeycloakSecurityContextRequestFilter
  KeycloakAuthenticatedActionsFilter
  AnonymousAuthenticationFilter
  SessionManagementFilter
  ExceptionTranslationFilter
  FilterSecurityInterceptor
]

In addition to the regular built-in filters of Spring Security several filters of the Keycloak adapter have been added here, combine them with the execution flow to get to know them.

KeycloakPreAuthActionsFilter

The purpose of this filter is to expose a Keycloak adapter object PreAuthActionsHandler to Spring Security. And the role of this adapter is to intercept the processing of a Keycloak’s functional request handling interfaces, which are built in with specific suffixes.

1
2
3
4
5
6
7
8
// 退出端点
public static final String K_LOGOUT = "k_logout";
// 重置什么公钥的?
public static final String K_PUSH_NOT_BEFORE = "k_push_not_before";
// 测试用的
public static final String K_TEST_AVAILABLE = "k_test_available";
// 获取 jwk 相关的
public static final String K_JWKS = "k_jwks";

You can generally ignore this filter without going deep into the bottom.

KeycloakAuthenticationEntryPoint

KeycloakAuthenticationEntryPoint is an implementation of AuthenticationEntryPoint configured in KeycloakWebSecurityConfigurerAdapter.

When the request is filtered by the FilterSecurityInterceptor, the current user is found to be an anonymous and does not meet the access control requirements of /admin/foo and an AccessDeniedException is thrown. This is passed to KeycloakAuthenticationEntryPoint via ExceptionTranslationFilter to handle the 401 exception.

1
2
3
4
5
6
7
8
9
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        HttpFacade facade = new SimpleHttpFacade(request, response);
        if (apiRequestMatcher.matches(request) || adapterDeploymentContext.resolveDeployment(facade).isBearerOnly()) {
            commenceUnauthorizedResponse(request, response);
        } else {
            commenceLoginRedirect(request, response);
        }
    }

It implements two policies.

  • When the request is a login request /sso/login or BearerOnly (these properties were partly covered in the previous article) it returns a 401 response with a WWW-Authenticate header.
  • In all other cases, a OIDC authentication authorization request is jumped.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
protected void commenceLoginRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException {
    if (request.getSession(false) == null && KeycloakCookieBasedRedirect.getRedirectUrlFromCookie(request) == null) {
        // If no session exists yet at this point, then apparently the redirect URL is not
        // stored in a session. We'll store it in a cookie instead.
        response.addCookie(KeycloakCookieBasedRedirect.createCookieFromRedirectUrl(request.getRequestURI()));
    }

    String queryParameters = "";
    if (!StringUtils.isEmpty(request.getQueryString())) {
        queryParameters = "?" + request.getQueryString();
    }

    String contextAwareLoginUri = request.getContextPath() + loginUri + queryParameters;
    log.debug("Redirecting to login URI {}", contextAwareLoginUri);
    response.sendRedirect(contextAwareLoginUri);
}

Our interface obviously takes the above approach, so it’s obvious that we need to jump to the login page. At this point we need to see if /admin/foo is cached, because after login we have to perform the logic of /admin/foo. If Spring Security does not store the Session or Cookie, it will cache /admin/foo to the Cookie and then redirect to the Keycloak authorization page:

1
http://localhost:8011/auth/realms/felord.cn/protocol/openid-connect/auth?response_type=code&client_id=CLIENT&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fsso%2Flogin&state=STATE&login=true&scope=openid

KeycloakAuthenticationProcessingFilter

The above is a typical Authorization Code Flow pattern. When an account password is entered to agree to authorization, the authorization server requests a callback link with code and state (in this case /sso/login). The interceptor for /sso/login is the KeycloakAuthenticationProcessingFilter. This interface does not only handle logins, but also intercepts any authorization header Authorization, access_token, or Keycloak Cookie.

In this filter and the familiar UsernamePasswordAuthenticationFilter are inherited from AbstractAuthenticationProcessingFilter in fact, the general process is very similar, except that the Keycloak authentication authorization API is used. If the authentication authorization is successful, the /admin/foo interface is retrieved from the Session and jumped. The entire simple Keycloak authentication authorization process is complete.

KeycloakSecurityContextRequestFilter

This filter has a single function, it is used to determine if it is RefreshableKeycloakSecurityContext, refreshable security context, if it is, put a RefreshableKeycloakSecurityContext in the ServletRequest object, the subsequent other filters will do something based on this marker.

KeycloakAuthenticatedActionsFilter

This filter is used to catch the RefreshableKeycloakSecurityContext placed by the KeycloakSecurityContextRequestFilter in the request object ServletRequest. The core is this.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    public boolean handledRequest() {
        log.debugv("AuthenticatedActionsValve.invoke {0}", facade.getRequest().getURI());
        if (corsRequest()) return true;
        String requestUri = facade.getRequest().getURI();
        if (requestUri.endsWith(AdapterConstants.K_QUERY_BEARER_TOKEN)) {
            queryBearerToken();
            return true;
        }
        if (!isAuthorized()) {
            return true;
        }
        return false;
    }

Returning true here blocks it from going further. It is mainly based on the policy provided by Keycloak to determine whether authorization has been granted, and the logic looks quite complicated.

Based on the principle of space, we will introduce Keycloak’s filters in detail afterwards, but today we know roughly what they are used for.

Supplementary

If you want to figure out the flow of any framework, the best way is to extract some key points from the log printing. I took apart the annotation that opens the Keycloak adapter to open the Spring Security logs.

1
2
3
4
5
6
7
8
@Configuration
@ComponentScan(
        basePackageClasses = {KeycloakSecurityComponents.class}
)
@EnableWebSecurity(debug = true)
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
//ignore
}

To see more logs, adjust the logs for Spring Boot’s org related packages to debug as well.

1
2
3
logging:  
   level:    
     org : debug

Then the code running process will be very clear in the console Console, which greatly facilitated me to figure out the running process of Keycloak. Keycloak’s process is simple to understand, it feels very bland and boring, and most of them do not need customization. I personally think that the focus is actually not here, how to customize Keycloak’s user management, role management and a series of management APIs according to the business is the key to using it well. Do not go away, the follow-up will be combined with some scenarios to modify keycloak.

Reference https://felord.cn/keycloak5.html