SoT
SoT

Reputation: 1203

Extend Keycloak token in Spring boot

I'm using Keycloak to secure my Spring boot backend.

Dependencies:

<dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-spring-boot-2-adapter</artifactId>
            <version>12.0.3</version>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-tomcat7-adapter-dist</artifactId>
            <version>12.0.3</version>
            <type>pom</type>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-spring-security-adapter</artifactId>
            <version>12.0.3</version>
        </dependency>

Security config:

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        super.configure(http);
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry expressionInterceptUrlRegistry = http.cors()
                .and()
                .csrf().disable()                
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) 
                .and() 
                .authorizeRequests();

        expressionInterceptUrlRegistry = expressionInterceptUrlRegistry.antMatchers("/iam/accounts/promoters*").hasRole("PROMOTER");
        expressionInterceptUrlRegistry.anyRequest().permitAll();
    }

Everything work fine!

But now I add a new section in keycloak token "roles" and I need to somehow extend keycloak jwt class in my Spring boot and write some code to parse and store the roles information to SecurityContext. Could you Guy please tell me how to archive the goal?

Upvotes: 1

Views: 6162

Answers (2)

Gabriell Lopes
Gabriell Lopes

Reputation: 11

I didn't understand why do you need extend Keycloak Token. The roles already there are in Keycloak Token. I will try explain how to access it, the Keycloak have two levels for roles, 1) Realm level and 2) Application (Client) level, by default your Keycloak Adapter use realm level, to use application level you need setting the propertie keycloak.use-resource-role-mappings with true in your application.yml

How to create roles in realm enter image description here

How to creare roles in client enter image description here

User with roles ADMIN (realm) and ADD_USER (application) enter image description here

To have access roles you can use KeycloakAuthenticationToken class in your Keycloak Adapter, you can try invoke the following method:

...
public ResponseEntity<Object> getUsers(final KeycloakAuthenticationToken authenticationToken)  {
    final AccessToken token = authenticationToken.getAccount().getKeycloakSecurityContext().getToken();
    final Set<String> roles = token.getRealmAccess().getRoles();
    final Map<String, AccessToken.Access> resourceAccess = token.getResourceAccess();
...
}
...

To protect any router using Spring Security you can use this annotation,  example below:

@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/users")
public ResponseEntity<Object> getUsers(final KeycloakAuthenticationToken token)  {
   return ResponseEntity.ok(service.getUsers());
}

Obs: The  keycloak.use-resource-role-mappings set up using @PreAuthorize Annotation.  If set to true, @PreAuthorize checks roles in token.getRealmAccess().getRoles(), if false it checks roles in token.getResourceAccess().

If you want add any custom claim in token, let me know that I can explain better.

I put here how I set up my Keycloak Adapter and the properties in my  application.yml:

SecurityConfig.java

...
@KeycloakConfiguration
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

    @Value("${project.cors.allowed-origins}")
    private String origins = "";

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) {
        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }

    @Bean
    public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
        return new KeycloakSpringBootConfigResolver();
    }

    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new NullAuthenticatedSessionStrategy();
    }

    @Override
    protected KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
        KeycloakAuthenticationProcessingFilter filter = new KeycloakAuthenticationProcessingFilter(this.authenticationManagerBean());
        filter.setSessionAuthenticationStrategy(this.sessionAuthenticationStrategy());
        filter.setAuthenticationFailureHandler((request, response, exception) -> {
            response.addHeader("Access-Control-Allow-Origin", origins);
            if (!response.isCommitted()) {
                response.sendError(401, "Unable to authenticate using the Authorization header");
            } else if (200 <= response.getStatus() && response.getStatus() < 300) {
                throw new RuntimeException("Success response was committed while authentication failed!", exception);
            }
        });
        return filter;
    }

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        super.configure(http);
        http.csrf()
                .disable()
                .authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS, "**").permitAll()
                .antMatchers("/s/**").authenticated()
                .anyRequest().permitAll();

    }
}

application.yml

..
keycloak: 
    enabled: true 
    auth-server-url: http://localhost:8080/auth 
    resource: myclient 
    realm: myrealm 
    bearer-only: true 
    principal-attribute: preferred_username 
    use-resource-role-mappings: true
..

Upvotes: 1

SoT
SoT

Reputation: 1203

First, extends keycloak AccessToken:

@Data
static class CustomKeycloakAccessToken extends AccessToken {

    @JsonProperty("roles")
    protected Set<String> roles;

}

Then:

@KeycloakConfiguration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
    @Override
    protected KeycloakAuthenticationProvider keycloakAuthenticationProvider() {
        return new KeycloakAuthenticationProvider() {

            @Override
            public Authentication authenticate(Authentication authentication) throws AuthenticationException {
                KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) authentication;
                List<GrantedAuthority> grantedAuthorities = new ArrayList<>();

                for (String role : ((CustomKeycloakAccessToken)((KeycloakPrincipal)token.getPrincipal()).getKeycloakSecurityContext().getToken()).getRoles()) {
                    grantedAuthorities.add(new KeycloakRole(role));
                }

                return new KeycloakAuthenticationToken(token.getAccount(), token.isInteractive(), new SimpleAuthorityMapper().mapAuthorities(grantedAuthorities));
            }

        };
    }

    /**
     * Use NullAuthenticatedSessionStrategy for bearer-only tokens. Otherwise, use
     * RegisterSessionAuthenticationStrategy.
     */
    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new NullAuthenticatedSessionStrategy();
    }

    @Override
    protected KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
        KeycloakAuthenticationProcessingFilter filter = new KeycloakAuthenticationProcessingFilter(authenticationManagerBean());
        filter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy());
        filter.setRequestAuthenticatorFactory(new SpringSecurityRequestAuthenticatorFactory() {

            @Override
            public RequestAuthenticator createRequestAuthenticator(HttpFacade facade,
                                                                   HttpServletRequest request, KeycloakDeployment deployment, AdapterTokenStore tokenStore, int sslRedirectPort) {
                return new SpringSecurityRequestAuthenticator(facade, request, deployment, tokenStore, sslRedirectPort) {

                    @Override
                    protected BearerTokenRequestAuthenticator createBearerTokenAuthenticator() {
                        return new BearerTokenRequestAuthenticator(deployment) {

                            @Override
                            protected AuthOutcome authenticateToken(HttpFacade exchange, String tokenString) {
                                log.debug("Verifying access_token");
                                if (log.isTraceEnabled()) {
                                    try {
                                        JWSInput jwsInput = new JWSInput(tokenString);
                                        String wireString = jwsInput.getWireString();
                                        log.tracef("\taccess_token: %s", wireString.substring(0, wireString.lastIndexOf(".")) + ".signature");
                                    } catch (JWSInputException e) {
                                        log.errorf(e, "Failed to parse access_token: %s", tokenString);
                                    }
                                }
                                try {
                                    TokenVerifier<CustomKeycloakAccessToken> tokenVerifier = AdapterTokenVerifier.createVerifier(tokenString, deployment, true, CustomKeycloakAccessToken.class);

                                    // Verify audience of bearer-token
                                    if (deployment.isVerifyTokenAudience()) {
                                        tokenVerifier.audience(deployment.getResourceName());
                                    }
                                    token = tokenVerifier.verify().getToken();
                                } catch (VerificationException e) {
                                    log.debug("Failed to verify token");
                                    challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.INVALID_TOKEN, "invalid_token", e.getMessage());
                                    return AuthOutcome.FAILED;
                                }
                                if (token.getIssuedAt() < deployment.getNotBefore()) {
                                    log.debug("Stale token");
                                    challenge = challengeResponse(exchange,  OIDCAuthenticationError.Reason.STALE_TOKEN, "invalid_token", "Stale token");
                                    return AuthOutcome.FAILED;
                                }
                                boolean verifyCaller;
                                if (deployment.isUseResourceRoleMappings()) {
                                    verifyCaller = token.isVerifyCaller(deployment.getResourceName());
                                } else {
                                    verifyCaller = token.isVerifyCaller();
                                }
                                surrogate = null;
                                if (verifyCaller) {
                                    if (token.getTrustedCertificates() == null || token.getTrustedCertificates().isEmpty()) {
                                        log.warn("No trusted certificates in token");
                                        challenge = clientCertChallenge();
                                        return AuthOutcome.FAILED;
                                    }

                                    // for now, we just make sure Undertow did two-way SSL
                                    // assume JBoss Web verifies the client cert
                                    X509Certificate[] chain = new X509Certificate[0];
                                    try {
                                        chain = exchange.getCertificateChain();
                                    } catch (Exception ignore) {

                                    }
                                    if (chain == null || chain.length == 0) {
                                        log.warn("No certificates provided by undertow to verify the caller");
                                        challenge = clientCertChallenge();
                                        return AuthOutcome.FAILED;
                                    }
                                    surrogate = chain[0].getSubjectDN().getName();
                                }
                                log.debug("successful authorized");
                                return AuthOutcome.AUTHENTICATED;
                            }

                        };
                    }
                };
            }
        });
        return filter;
    }

}

Upvotes: 2

Related Questions