22

I updated to Spring Boot 3 in a project that uses the Keycloak Spring Adapter. Unfortunately, it doesn't start because the KeycloakWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter which was first deprecated in Spring Security and then removed. Is there currently another way to implement security with Keycloak? Or to put it in other words: How can I use Spring Boot 3 in combination with the Keycloak adapter?

I searched the Internet, but couldn't find any other version of the adapter.

Dmitriy Popov
  • 2,150
  • 3
  • 25
  • 34
Samuel
  • 547
  • 1
  • 3
  • 14
  • 1
    Latest updates on the Keycloak adapter deprecation can be found here: https://www.keycloak.org/2023/03/adapter-deprecation-update.html – Danylo Zatorsky May 01 '23 at 06:49

6 Answers6

53

You can't use Keycloak adapters with spring-boot 3 for the reason you found, plus a few others related to transitive dependencies. As most Keycloak adapters were deprecated in early 2022, it is very likely that no update will be published to fix that.

Instead, use spring-security 6 libs for OAuth2. Don't panic, it's an easy task with spring-boot.

In the following, I'll consider you have a good understanding of OAuth2 concepts and know exactly why you need to configure an OAuth2 client or an OAuth2 resource server. In case of doubt, please refer to the OAuth2 essentials section of my tutorials.

I'll only detail here the configuration of servlet application as a resource server, and then as a client, for a single Keycloak realm, with and then without spring-addons-starter-oidc, a Spring Boot starter of mine. Browse directly to the section you are interested in (but be prepared to write much more code if you don't want to use "my" starter).

Also refer to my tutorials for different use-cases like:

  • accepting tokens issued by multiple realms or instances (known in advance or dynamically created in a trusted domain)
  • reactive applications (webflux), like spring-cloud-gateway for instance
  • apps publicly serving both a REST API and a server-side rendered UI to consume it
  • advanced access-control rules
  • BFF pattern
  • ...

1. OAuth2 Resource Server

App exposes a REST API secured with access tokens. It is consumed by an OAuth2 REST client. A few sample of such clients:

  • another Spring application configured as an OAuth2 client and using WebClient, @FeignClient, RestTemplate or alike
  • development tools like Postman capable of fetching OAuth2 tokens and issuing REST requests
  • Javascript based application configured as a "public" OAuth2 client with a library like angular-auth-oidc-client

1.1. With spring-addons-starter-oidc

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
    <groupId>com.c4-soft.springaddons</groupId>
    <artifactId>spring-addons-starter-oidc</artifactId>
    <version>7.1.3</version>
</dependency>
origins: http://localhost:4200
issuer: http://localhost:8442/realms/master

com:
  c4-soft:
    springaddons:
      oidc:
        ops:
        - iss: ${issuer}
          username-claim: preferred_username
          authorities:
          - path: $.realm_access.roles
            prefix: ROLE_
          - path: $.resource_access.*.roles
        resourceserver:
          cors:
          - path: /**
            allowed-origin-patterns: ${origins}
          permit-all: 
          - "/actuator/health/readiness"
          - "/actuator/health/liveness"
          - "/v3/api-docs/**"

Prefix for realm roles in the conf above are there only for illustration purposes, you might remove it. The CORS configuration would need some refinements too.

@Configuration
@EnableMethodSecurity
public static class WebSecurityConfig { }

Nothing more is needed to configure a resource-server with fine tuned CORS policy and authorities mapping. Bootiful, isn't it?.

As you can guess from the ops property being an array, this solution is actually compatible with "static" multi-tenancy: you can declare as many trusted issuers as you need and it can be heterogeneous (use different claims for username and authorities).

Also, this solution is compatible with reactive application: spring-addons-starter-oidc will detect it from what is on the class-path and adapt its security auto-configuration.

1.2. With just spring-boot-starter-oauth2-resource-server

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
    <!-- used when converting Keycloak roles to Spring authorities -->
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
</dependency>
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:8442/realms/master
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public static class WebSecurityConfig {

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http, Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter) throws Exception {

        http.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter)));

        // Enable and configure CORS
        http.cors(cors -> cors.configurationSource(corsConfigurationSource("http://localhost:4200")));

        // State-less session (state in access-token only)
        http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        // Disable CSRF because of state-less session-management
        http.csrf(csrf -> csrf.disable());

        // Return 401 (unauthorized) instead of 302 (redirect to login) when
        // authorization is missing or invalid
        http.exceptionHandling(eh -> eh.authenticationEntryPoint((request, response, authException) -> {
            response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Bearer realm=\"Restricted Content\"");
            response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
        }));

        // @formatter:off
        http.authorizeHttpRequests(accessManagement -> accessManagement
            .requestMatchers("/actuator/health/readiness", "/actuator/health/liveness", "/v3/api-docs/**").permitAll()
            .anyRequest().authenticated()
        );
        // @formatter:on

        return http.build();
    }

    private UrlBasedCorsConfigurationSource corsConfigurationSource(String... origins) {
        final var configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList(origins));
        configuration.setAllowedMethods(List.of("*"));
        configuration.setAllowedHeaders(List.of("*"));
        configuration.setExposedHeaders(List.of("*"));

        final var source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    @RequiredArgsConstructor
    static class JwtGrantedAuthoritiesConverter implements Converter<Jwt, Collection<? extends GrantedAuthority>> {

        @Override
        @SuppressWarnings({ "rawtypes", "unchecked" })
        public Collection<? extends GrantedAuthority> convert(Jwt jwt) {
            return Stream.of("$.realm_access.roles", "$.resource_access.*.roles").flatMap(claimPaths -> {
                Object claim;
                try {
                    claim = JsonPath.read(jwt.getClaims(), claimPaths);
                } catch (PathNotFoundException e) {
                    claim = null;
                }
                if (claim == null) {
                    return Stream.empty();
                }
                if (claim instanceof String claimStr) {
                    return Stream.of(claimStr.split(","));
                }
                if (claim instanceof String[] claimArr) {
                    return Stream.of(claimArr);
                }
                if (Collection.class.isAssignableFrom(claim.getClass())) {
                    final var iter = ((Collection) claim).iterator();
                    if (!iter.hasNext()) {
                        return Stream.empty();
                    }
                    final var firstItem = iter.next();
                    if (firstItem instanceof String) {
                        return (Stream<String>) ((Collection) claim).stream();
                    }
                    if (Collection.class.isAssignableFrom(firstItem.getClass())) {
                        return (Stream<String>) ((Collection) claim).stream().flatMap(colItem -> ((Collection) colItem).stream()).map(String.class::cast);
                    }
                }
                return Stream.empty();
            })
            /* Insert some transformation here if you want to add a prefix like "ROLE_" or force upper-case authorities */
            .map(SimpleGrantedAuthority::new)
            .map(GrantedAuthority.class::cast).toList();
        }
    }

    @Component
    @RequiredArgsConstructor
    static class SpringAddonsJwtAuthenticationConverter implements Converter<Jwt, JwtAuthenticationToken> {

        @Override
        public JwtAuthenticationTokenconvert(Jwt jwt) {
            final var authorities = new JwtGrantedAuthoritiesConverter().convert(jwt);
            final String username = JsonPath.read(jwt.getClaims(), "preferred_username");
            return new JwtAuthenticationToken(jwt, authorities, username);
        }
    }
}

In addition to being much more verbose than preceding one, this solution is also less flexible:

  • not adapted to multi-tenancy (multiple Keycloak realms or instances)
  • hardcoded allowed origins
  • hardcoded claim names to fetch autorities from
  • hardcoded "permitAll" path matchers

2. OAuth2 Client

App exposes any kind of resources secured with sessions (not access tokens). It is consumed directly by a browser (or any other user agent capable of maintaining a session) without the need of a scripting language or OAuth2 client lib (authorization-code flow, logout and token storage are handled by Spring on the server). Common uses-cases are:

  • applications with server-side rendered UI (with Thymeleaf, JSF, or whatever)
  • spring-cloud-gateway used as Backend For Frontend: configured as OAuth2 client with TokenRelay filter (hides OAuth2 tokens from the browser and replaces session cookie with an access token before forwarding a request to downstream resource server(s))

Note that the Back-Channel Logout is not implemented by Spring yet. If you need it, use "my" starters (or copy from it).

2.1. With spring-addons-starter-oidc

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-client</artifactId>
</dependency>
<dependency>
    <groupId>com.c4-soft.springaddons</groupId>
    <artifactId>spring-addons-starter-oidc</artifactId>
    <version>7.1.3</version>
</dependency>
issuer: http://localhost:8442/realms/master
client-id: spring-addons-confidential
client-secret: change-me
client-uri: http://localhost:8080

spring:
  security:
    oauth2:
      client:
        provider:
          keycloak:
            issuer-uri: ${issuer}
        registration:
          keycloak-login:
            authorization-grant-type: authorization_code
            client-name: My Keycloak instance
            client-id: ${client-id}
            client-secret: ${client-secret}
            provider: keycloak
            scope: openid,profile,email,offline_access

com:
  c4-soft:
    springaddons:
      oidc:
        ops:
        - iss: ${issuer}
          username-claim: preferred_username
          authorities:
          - path: $.realm_access.roles
          - path: $.resource_access.*.roles
        client:
          client-uri: ${client-uri}
          security-matchers: /**
          permit-all:
          - /
          - /login/**
          - /oauth2/**
          csrf: cookie-accessible-from-js
          post-login-redirect-path: /home
          post-logout-redirect-path: /
          back-channel-logout-enabled: true
@Configuration
@EnableMethodSecurity
public class WebSecurityConfig {
}

As for resource server, this solution works in reactive applications too.

There is also an optional support for multi-tenancy on clients: allow a user to be logged in simultaneously on several OpenID Providers, on which he might have different usernames (subject by default, which is a UUID in Keycloak, and changes with each realm).

2.2. With just spring-boot-starter-oauth2-client

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
    <!-- used when converting Keycloak roles to Spring authorities -->
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
</dependency>
issuer: http://localhost:8442/realms/master
client-id: spring-addons-confidential
client-secret: change-me

spring:
  security:
    oauth2:
      client:
        provider:
          keycloak:
            issuer-uri: ${issuer}
        registration:
          keycloak-login:
            authorization-grant-type: authorization_code
            client-name: My Keycloak instance
            client-id: ${client-id}
            client-secret: ${client-secret}
            provider: keycloak
            scope: openid,profile,email,offline_access
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class WebSecurityConfig {

    @Bean
    SecurityFilterChain
            clientSecurityFilterChain(HttpSecurity http, InMemoryClientRegistrationRepository clientRegistrationRepository)
                    throws Exception {
        http.oauth2Login(withDefaults());
        http.logout(logout -> {
            logout.logoutSuccessHandler(new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository));
        });
        // @formatter:off
        http.authorizeHttpRequests(ex -> ex
                .requestMatchers("/", "/login/**", "/oauth2/**").permitAll()
                .requestMatchers("/nice.html").hasAuthority("NICE")
                .anyRequest().authenticated());
        // @formatter:on
        return http.build();
    }

    @Component
    @RequiredArgsConstructor
    static class GrantedAuthoritiesMapperImpl implements GrantedAuthoritiesMapper {

        @Override
        public Collection<? extends GrantedAuthority> mapAuthorities(Collection<? extends GrantedAuthority> authorities) {
            Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

            authorities.forEach(authority -> {
                if (OidcUserAuthority.class.isInstance(authority)) {
                    final var oidcUserAuthority = (OidcUserAuthority) authority;
                    final var issuer = oidcUserAuthority.getIdToken().getClaimAsURL(JwtClaimNames.ISS);
                    mappedAuthorities.addAll(extractAuthorities(oidcUserAuthority.getIdToken().getClaims()));

                } else if (OAuth2UserAuthority.class.isInstance(authority)) {
                    try {
                        final var oauth2UserAuthority = (OAuth2UserAuthority) authority;
                        final var userAttributes = oauth2UserAuthority.getAttributes();
                        final var issuer = new URL(userAttributes.get(JwtClaimNames.ISS).toString());
                        mappedAuthorities.addAll(extractAuthorities(userAttributes));

                    } catch (MalformedURLException e) {
                        throw new RuntimeException(e);
                    }
                }
            });

            return mappedAuthorities;
        };

        @SuppressWarnings({ "rawtypes", "unchecked" })
        private static Collection<GrantedAuthority> extractAuthorities(Map<String, Object> claims) {
            /* See resource server solution above for authorities mapping */
        }
    }
}

3. What is spring-addons-starter-oidc and why using it

This starter is a standard Spring Boot starter with additional application properties used to auto-configure default beans and provide it to Spring Security. It is important to note that the auto-configured @Beans are almost all @ConditionalOnMissingBean which enables you to override it in your conf.

It is open-source and you can change everything it pre-configures for you (refer to the Javadoc, the starter READMEs, or the many samples). You should read the starters source before deciding not to trust it, it is not that big. Start with imports resource, it defines what is loaded by Spring Boot for auto-configuration.

In my opinion (and as demonstrated above), Spring Boot auto-configuration for OAuth2 can be pushed one step further to:

  • make OAuth2 configuration more portable: with a configurable authorities converter, switching from an OIDC provider to another is just a matter of editing properties (Keycloak, Auth0, Cognito, Azure AD, etc.)
  • ease app deployment on different environments: CORS configuration is controlled from properties file
  • reduce drastically the amount of Java code (things get even more complicated if you are in multi-tenancy scenario)
  • support more than just one issuer by default
  • reduce chances of misconfiguration (for instance, it is frequent to see sample configurations with disabled CSRF protection on clients, or wasting resources with sessions on endpoints secured with access tokens)
ch4mp
  • 6,622
  • 6
  • 29
  • 49
  • 2
    Thank you. That looks quite bootiful and easy to migrate to. Will try this approach. – Samuel Nov 25 '22 at 13:32
  • I am currently implementing your suggestions. The rest service with the resource dependency starts but when visiting any url I am getting the error that no JwtDecoder is available. https://gitlab.com/samuel1998.schwanzer/SmartOrganizr/-/tree/migrate/oauth2 – Samuel Nov 25 '22 at 16:31
  • My local configuration looks like that: ```spring.security.oauth2.client.registration.keycloak.client-id=smartorganizr spring.security.oauth2.client.registration.keycloak.client-secret=dzmKY0QUuLflQBeceMIhPCr8gE5AN9YF spring.security.oauth2.client.provider.keycloak.issuer-uri=http://192.168.2.33/realms/master``` – Samuel Nov 25 '22 at 16:32
  • Sorry. I want to create a client. It should have the same configuration as a keycloak confidential client. You can find an updated version on gitlab. This is my yaml: https://pastebin.com/GFMqxPPE with password: sGAydP4AyT – Samuel Nov 25 '22 at 18:39
  • Anybody else getting the error for the uiFilterChain, that a bean of type ClientRegistrationRepository cannot be found? – Michael T Dec 28 '22 at 08:43
  • You are probably missing a dependency: `spring-boot-starter-oauth2-client` – ch4mp Dec 28 '22 at 18:26
  • Except that this solution does not have the self contained authorization solution. The essence of these adapters previously was that you upload a sophisticated authorization rrule set for your services into keycloak, like roles, policy enforcement, etc and the keycloak adapter could authorize methods accordingly. With this standard spring approach, it will not work. Would be hell of a lot tedious work to make it work. There is a conversation about it: https://github.com/keycloak/keycloak/discussions/11681 – newhouse Jan 05 '23 at 07:48
  • In short I am quote furious and mad about these responses everwhere on the internet, everybody shows the standard oidc + spring boot oauth2 solutions, and they forget that most of the real world usages of the keycloak security adapters involved heavily more sopshisticated features, than being a simple authentication server. It is like using a rocket ship to go home after work. – newhouse Jan 05 '23 at 07:50
  • I contributed in the thread you mention why this authorization service is, in my opinion, a bad thing. This kind of rocket ship will never fly you to the moon: it is way too adherent to Keycloak, hardly testable, and inefficient. Just as introspection, it requires a call to authorization server for each and every request, which introduces latency and poorly scales. – ch4mp Jan 05 '23 at 16:26
  • It is possible to do advanced access control with no adherence to Keycloak. I did it for a set of Healthcare professionals web services requiring authorization delegation between users for specific structures. Of course editing roles or permissions delegation does not require a restart and spring security DSL enhancement are defined at a single place for all services. – ch4mp Jan 05 '23 at 16:32
  • At the method `apiFilterChain`, I think there should be `Converter` instead `Converter`. Thanks for this answer. This one is great. – Paweł Walaszek Jan 22 '23 at 12:49
  • how to use policy enforce in your add-on? – nobjta_9x_tq Mar 06 '23 at 05:08
  • You call authorization service REST endpoint with a REST client like WebClient, @FeignClient, RestClient. But honestly, I wouldn't do that: refer to this answer why https://stackoverflow.com/a/75647048/619830 – ch4mp Mar 06 '23 at 06:16
  • if we have a OAuth2 Client, is it possible to access our endpoints through Postman? I have no problems in browser, when I access an endpoint it redirects to the login Keycloak page. But also in Postman, even if I send an access token in the Authorization header, I only get the login page as a response. – Octavia May 23 '23 at 13:07
  • Read my answer again, more carefuly: requests to clients are secured with sessions, not access tokens. Configure a resource server if you need requests to be secured with access tokens or refer to my tutorials if you need to mix client and resource server configurations – ch4mp May 23 '23 at 13:19
  • 1
    Awesome. Thanks for the follow up to Spring Boot 3.1/Security 6.1. – Samuel May 23 '23 at 17:19
  • @ch4mp I tried to use your starter for webflux but when I access route "/admin" and the user have role admin it give me access denied. any suggestion about how to fix that. – UN..D Jun 09 '23 at 11:13
  • With that level of details, it's impossible to find the cause of your issue. My guess is you used `hasRole("admin")` which requires exactly `ROLE_admin` authority. To have this authority, given the token contains the `admin` role (not `ADMIN`), use `prefix: ROLE_` property at the same level as the `path` one in application properties (a different prefix can be configured for each JSON-Path). Otherwize, use `hasAuthority` or `hasAnyAuthority` instead of `hasRole` or `hasAnyRole`. – ch4mp Jun 09 '23 at 19:42
  • I tried what you mentioned about ROLE_ but never worked (could you share a link to how prefix can be configured for each json-path), it worked with "hasAuthority & hasAnyAuthority. – UN..D Jun 21 '23 at 20:37
  • @Ch4mp could you please share how to use gateway with keycloak to login/signup (for microservices), I tried your tutorials (reactive-resource-server & bff) but still I had hard time, could you please suggest articles or source codes about that. Thanks – UN..D Jun 21 '23 at 20:42
  • I added illustrations of prefix usage in the response. OAuth2 login on a resource server is a nonsense, read the main readme for some OAuth2 background. If you followed the Keycloak setup REAME, then the BFF project should start and login work. – ch4mp Jun 21 '23 at 22:25
  • @Ch4mp could you please check this code https://github.com/magician20/learningcircle I want to know why gateway not redirect request to greeting service & how could I handle login and register from gateway service – UN..D Jul 08 '23 at 14:32
  • If you are calling `/api/test`, it is listed in `permit-all` => no authorization required => unauthorized requests are not redirected – ch4mp Jul 08 '23 at 15:55
  • @Ch4mp Thanks for your feedback, still not working – UN..D Jul 10 '23 at 18:28
  • Do you have any similar guide for openID connect. I'm trying to migrate my springboot applications to springboot 3.x and want to know how to integrate keycloak openid connect. – manjosh Aug 31 '23 at 06:34
  • OpenID is OAuth2, and all starters listed in the answer are using **O**pen**ID** **C**onnect to fetch authorization server OAuth2 configuration and JWT public signing keys. What exactly do you expect in addition to what is implemented above? – ch4mp Aug 31 '23 at 06:47
  • Thanks for this answer. I have one additonal question. Why can I only map the authorities with resource_access from ID Tokens. I would prefer to have that RBAC information only in the AccessToken rather than the ID Token. I want to protect some resources (xhtml sites) with client-roles. But as i understand the Code in OidcAuthorizationCodeAuthenticationProvider or OAuth2LoginAuthenticationProvider SpringBoot3 forces me to map these information into ID-Token on Keycloak side. I guess the KeycloakAdapter (SpringBoot2) handled that differently – Matthias H Sep 01 '23 at 13:20
  • Audience and purpose are not the same for ID and access tokens. Short answer is that ID tokens are intended to be read by clients and access tokens by resource servers. ID tokens should be used only to get some information about the identified user, not to grant access to resources. Access tokens are intended to grant access to resources: allow a client holding it to act on behalf of a user on a specific resource server. This differences make access token much more sensitive (if leaked, user identity can be stolen) – ch4mp Sep 01 '23 at 15:50
  • Because of the above, and because access tokens should be considered opaque on the client, Spring security **on OAuth2 clients** reads claims from either the ID token when available from the userinfo endpoint otherwize. **You can configure Keycloak to add client roles to ID tokens** (`clients -> your-client -> Client scopes -> your-client-dedicated -> Add mapper -> predifind -> client roles`, you then open the mapper details and enable Add to ID token and userinfo). – ch4mp Sep 01 '23 at 15:59
  • This will allow to read user roles on clients and adapt UI (hide menus, buttons and so on), not to filter accessible resources. Again, this roles should be checked again on the resource server, reading claims in an access token (JWT) or introspected from it (any format). – ch4mp Sep 01 '23 at 16:04
5

Use the standard Spring Security OAuth2 client instead of a specific Keycloak adapter and SecurityFilterChain instead of WebSecurityAdapter.

Something like this:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(jsr250Enabled = true, prePostEnabled = true)
class OAuth2SecurityConfig {

@Bean
fun customOauth2FilterChain(http: HttpSecurity): SecurityFilterChain {
    log.info("Configure HttpSecurity with OAuth2")

    http {
        oauth2ResourceServer {
            jwt { jwtAuthenticationConverter = CustomBearerJwtAuthenticationConverter() }
        }
        oauth2Login {}

        csrf { disable() }

        authorizeRequests {
            // Kubernetes
            authorize("/readiness", permitAll)
            authorize("/liveness", permitAll)
            authorize("/actuator/health/**", permitAll)
            // ...
            // everything else needs at least a valid login, roles are checked at method level
            authorize(anyRequest, authenticated)
        }
    }

    return http.build()
}

And then in application.yml:

spring:
  security:
    oauth2:
      client:
        provider:
          abc:
            issuer-uri: https://keycloak.../auth/realms/foo
        registration:
          abc:
            client-secret: ...
            provider: abc
            client-id: foo
            scope: [ openid, profile, email ]
      resourceserver:
        jwt:
          issuer-uri: https://keycloak.../auth/realms/foo
lathspell
  • 3,040
  • 1
  • 30
  • 49
  • Thank you for posting your configuration. I will try that way. Keycloak already announced moving away from their own adapter. – Samuel Nov 25 '22 at 10:41
  • Roles mapping is missing and this will we be a problem as soon as any role based access-control is required (`@PreAuthorise` expressions or `authoriseRequests` in security conf) . Also, this covers only part of the cases: resource-servers (REST APIs) are not covered. – ch4mp Nov 25 '22 at 12:26
  • You mention in your code a method that reads CustomBearerJwtAuthenticationConverter(). Where do I get that method from? – Samuel Nov 25 '22 at 15:58
  • Just implement Spring's interface. You might find inspiration from the Authentication converter in my answer for resource-servers. – ch4mp Nov 25 '22 at 19:34
3

Using Keycloak adapters is not possible because the KeycloakWebSecurityConfigurerAdapter inherited from the WebSecurityConfigurerAdapter class, which was deprecated in Spring Security and subsequently removed in the newer release.

I have published a detailed article on integrating Keycloak with Spring Boot 3.0 on Medium, which provides a step-by-step guide on how to integrate Keycloak with Spring Boot 3.0.

This guide is particularly helpful for those who are new to integrating Keycloak with Spring Boot 3.0 or migrating to Spring Boot 3.0 from an older version.

You can check out the article (https://medium.com/geekculture/using-keycloak-with-spring-boot-3-0-376fa9f60e0b) for a comprehensive explanation of the integration process. enter image description here

Hope this helps! If you have any questions, further clarifications or suggestions, Please feel free to leave a comment.

Yasas Sandeepa
  • 317
  • 4
  • 9
  • 1
    Do you realize thar your article covers only resource-servers and only *realm" roles, right? (when the answer above covers clients **and** clients roles too...) – ch4mp Feb 17 '23 at 08:42
  • I read your article and it works fine with spring-boot-starter-oath2-resource-server :) but still blocked with spring-boot-starter-oath2-client , it returns always a 404 ! Do you have a tutorial using spring-boot-starter-oath2-client with Thymleaf ? – Aguid Feb 19 '23 at 17:51
  • Hi @Aguid. As I described in the article, if you use it with Thymleaf, you must use oath2-client. Currently, I don't have a tutorial to guide you but I will write one. – Yasas Sandeepa Feb 20 '23 at 02:59
  • Hello Yasas and thank you for the helpful tutorial.. I have implemented the code you have mentioned in the tutorial but i still get an unauthorized access when i want to access to the user URL and I have double checked that I have assigned the user role to the user.. – Ghassen Jemaî Feb 28 '23 at 11:25
  • Update, my problem was solved with configuring the application.properties with the keycloak realm settings – Ghassen Jemaî Feb 28 '23 at 14:25
  • Hi @GhassenJemaî Thanks for the feedback and happy that you were able to solve the issue. – Yasas Sandeepa Mar 01 '23 at 07:53
0

Keycloak adapters are deprecated and there will not be any future updates or fixes as announce by Keycloak Team .

It is recommended to use Spring Security provided OAuth2 and OpenID Connect support.

ravthiru
  • 8,878
  • 2
  • 43
  • 52
-1

Keycloak 21.0.0 have introduced some new changes to support Spring Security 6.x.x and Spring Boot 3.x.x.. Here is a reference to that

  • 2
    The documentation is deprecated since it extends from WebSecurityConfigurerAdapater you still can use the oauth2 client dependency with spring security – Ghassen Jemaî Feb 28 '23 at 14:26
  • Isn't it still the [same](https://www.keycloak.org/docs/latest/securing_apps/#_spring_security_adapter)? – Samuel Feb 28 '23 at 19:13
  • yes the docs are pretty much deprecated since they implement the WebSecurityConfigurationAdapater – Ghassen Jemaî Feb 28 '23 at 19:20
-1

Based on different resources and whole weekend, spent on solving this new issue, I managed to find perfectly working solution.

I defined 2 roles: user and administrator on client level (not realm) and assigned to different users.

  • JDK 17
  • Keycloak 22.0.0.
  • Spring Boot 3.1.1

Here is the working solution for:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class OAuth2ResourceServerSecurityConfiguration {
    @Value("${keycloak.resource}")
    private String keycloakClientName;
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
        .authorizeHttpRequests((authorize) -> {
            authorize
            .anyRequest().authenticated();
        })
        .oauth2ResourceServer(httpSecurityOAuth2ResourceServerConfigurer -> httpSecurityOAuth2ResourceServerConfigurer
            .jwt(jwtConfigurer -> {
                jwtConfigurer.jwtAuthenticationConverter(jwtAuthenticationConverter());
            })
        );

        return httpSecurity.build();
    }
    private Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter() {
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRealmRoleConverter());

        return jwtAuthenticationConverter;
    }

    private class KeycloakRealmRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
        private Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        @Override
        public Collection<GrantedAuthority> convert(Jwt jwt) {
            final Map<String, Object> resourceAccess = (Map<String, Object>) jwt.getClaims().get("resource_access");

            if (resourceAccess != null) {
                final Map<String, Object> clientAccess = (Map<String, Object>) resourceAccess.get(OAuth2ResourceServerSecurityConfiguration.this.keycloakClientName);

                if (clientAccess != null) {
                    grantedAuthorities = ((List<String>) clientAccess.get("roles")).stream()
                            .map(roleName -> "ROLE_" + roleName) // Prefix to map to a Spring Security "role"
                            .map(SimpleGrantedAuthority::new)
                            .collect(Collectors.toList());
                }
            }

            return grantedAuthorities;
        }
    }
}

Keycloak configuration in properties:

keycloak:
  authServerUrl: http://<your_keycloak_host>:8989
  realm: <your_realm>
  resource: <your_client>
  useResourceRoleMappings: true
  cors: true
  corsMaxAge: 1000
  corsAllowedMethods: POST, PUT, DELETE, GET
  sslRequired: none
  bearerOnly: true
  publicClient: true
  principalAttribute: preferred_username
  credentials:
    secret: '{cipher}<your_encrypted_secret>'

And test controller:

@RestController
@RequestMapping("/api/v1/test")
public class TestController {
    @GetMapping("/")
    public String allAccess() {
        return "Public content";
    }

    @GetMapping("/endpoint1")
    @PreAuthorize("hasRole('user')")
    public String endpoint1() {
        return "User board";
    }

    @GetMapping("/endpoint2")
    @PreAuthorize("hasRole('administrator')")
    public String endpoint2() {
        return "Administrator board";
    }
}
Andrey
  • 1
  • 1
  • 2
    This is basically only part of the accepted answer (the resource server half), most of the `keycloak` properties are not used (only `keycloak.resource` is), and your code seems to require the `spring.security.oauth2.resourceserver` which are missing from your snippets. Also, in a resource server filter-chain, you should disable sessions (and CSRF protection), and make sure that 401 is returned to unauthorized requests. In short, if you had read the answer (and maybe followed the links), you'd have saved a weekend. – ch4mp Jul 17 '23 at 18:24