7

I am trying to modify code from a Keycloak and Spring Boot tutorial that I found online in various places (the code looks to have been duplicated). In any event, although Keycloak secures the proper URLs and Spring Security facilitates, when attempting to retrieve the Principal, it comes back as null. Here is my application.properties file:

keycloak.auth-server-url=http://localhost:9090/auth
keycloak.realm=<some realm>
keycloak.resource=<some resource>
keycloak.ssl-required=external
keycloak.public-client=true
keycloak.principal-attribute=preferred_username
keycloak.use-resource-role-mappings=true
server.port=8100

Here is my SecurityConfig.java file (pristine from the tutorials):

package com.bme.keycloakdemo.configuration;

import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.KeycloakSecurityComponents;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;

@Configuration
@EnableWebSecurity
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
    // Submits the KeycloakAuthenticationProvider to the AuthenticationManager
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }

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

    // Specifies the session authentication strategy
    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http.authorizeRequests()
            .antMatchers("/demo*")
            .hasRole("user")
            .anyRequest()
            .permitAll();
    }
}

My web controller follows:

package com.bme.keycloakdemo.controllers;

import java.security.Principal;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/demo")
public class DemoController {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @GetMapping(value = "/first", produces = {MediaType.APPLICATION_JSON_VALUE})
    public String first(Principal principal) {
        logger.info("WE GOT HERE");
        String retval = "WE GOT HERE! ";
        if (principal != null) {
            retval += principal.toString();
        }
        else {
            retval += "PRINCIPAL IS NULL";
        }
        return retval;
    }

    @GetMapping(value = "/second", produces = {MediaType.APPLICATION_JSON_VALUE})
    public String second(Principal principal) {
        logger.info("WE GOT HERE");
        String retval = "WE GOT HERE! " + principal.getName();
        return retval;
    }

    @GetMapping(value = "/third", produces = {MediaType.APPLICATION_JSON_VALUE})
    public String third(HttpServletRequest request) {
        logger.info("WE GOT HERE");
        Principal principal = request.getUserPrincipal();
        String retval = "WE GOT HERE! ";
        if (principal != null) {
            retval += principal.toString();
        }
        else {
            retval += "PRINCIPAL IS NULL";
        }
        return retval;
    }
}

Finally, here is my Maven configuration in case it makes a difference:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.batman-evolution</groupId>
    <artifactId>keycloakjournal</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>keycloakjournal</name>
    <description>Journal Secured with Keycloak</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.16.RELEASE</version>
        <relativePath/>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <keycloak.version>3.4.0.Final</keycloak.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-spring-boot-starter</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.keycloak.bom</groupId>
                <artifactId>keycloak-adapter-bom</artifactId>
                <version>${keycloak.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

So to recap the question, I am looking to get the Principal from Keycloak. I am currently using Spring Security to return the Principal but really do not care one way or the other so long as the Principal is usable. Thanks for your help!

Clayton
  • 249
  • 1
  • 5
  • 15

1 Answers1

3

This worked for me:

@GetMapping("/details")
public MyUserDetails getUserDetails(Principal principal) {

    KeycloakAuthenticationToken kp = (KeycloakAuthenticationToken) principal;

    SimpleKeycloakAccount simpleKeycloakAccount = (SimpleKeycloakAccount) kp.getDetails();

    AccesToken token  = simpleKeycloakAccount.getKeycloakSecurityContext().getToken();
    
    return  new MyUserDetails(token.getGivenName(), token.getFamilyName(), token.getEmail());
}
Adrian Mole
  • 49,934
  • 160
  • 51
  • 83
  • This approach works for token based applications (access type is public). For session based authentication (access type is confidential) then principal will always be null. Also you can simplify by using MyUserDetails getUserDetails(KeycloakAuthenticationToken kp) and eliminate the cast. – LeslieM May 18 '22 at 19:11
  • It answers the question, it parses the token that keycloak returns. So, I could have written it single-lined but instead, I decided to make it more readable. Also, I answered this question 2 years ago... I don't know if changes applied. – Prodromos Sarakinou May 18 '22 at 23:27