nono
nono

Reputation: 1692

Keycloak Spring boot configuration

I'm trying to configure Spring Boot and Keycloak for SSO. I've created a basic AngularJS application that do some requests to the Spring boot backend. using this

The Angular app is working fine and now I'm trying to follow the new Spring Boot Keycloak apdater docuementation here

This is my keycloak.json that is in the WEB-INF folder.

{
  "realm": "my-backend",
  "bearer-only": true,
  "realm-public-key": "MIIB...",
  "auth-server-url": "http://localhost:8180/auth",
  "ssl-required": "external",
  "resource": "my-backend",
  "principal-attribute": "preferred_username",
  "credentials": {
    "secret": "a75f55ca-8174-4072-8c60-b545c9ebf7e1"
}

Here is my security configuration :

@Configuration
@EnableWebSecurity
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter
{
    /**
     * Registers the KeycloakAuthenticationProvider with the authentication manager.
     */
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(keycloakAuthenticationProvider());
    }

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

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
    }

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

}

The 2 roles, 'admin' and 'user' are created in Keycloak and the current user has this roles.

@RestController
@RequestMapping("/")
@CrossOrigin("*")
public class PharmaController {

    public class Response{
        private String message;

        public Response(String msg){
            this.message = msg;
        }
        /**
         * @return the message
         */
        public String getMessage() {
            return message;
        }
        /**
         * @param message the message to set
         */
        public void setMessage(String message) {
            this.message = message;
        }

    }

    @RequestMapping(
            path="api/v1/userinfo", 
            method = RequestMethod.GET)
    @ResponseBody
    public void getUserInformation(KeycloakAuthenticationToken token) {
        if(token != null){
            System.out.println("token :" + token);

            try {
                System.out.println(token.getAccount().getPrincipal().getName());
                System.out.println(token.getAccount().getRoles());
            } catch (Exception e) {
                // TODO: handle exception
            } 

        }else{
            System.out.println("User not connected.");
        }
    }

    @RequestMapping(
            path="admin/hello", 
            method = RequestMethod.GET)
    @ResponseBody
    public Response adminHello(KeycloakAuthenticationToken token) {
        return new Response("Hello");
    }

}
  1. Even if the user doesn't have any of this role, he can access api/v1/userinfo with this response

{"data":"","status":200,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"jsonpCallbackParam":"callback","url":"http://localhost:8080/api/v1/userinfo","headers":{"Accept":"application/json, text/plain, */*","Access-Control-Allow-Origin":"*","Authorization":"BEARER eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJSN2JSQUgtcmUwNnVEMHh4Vlotd1dyWVIycVI0S0pyMDFIZWQ2QmJMNnA4In0.eyJqdGkiOiI3ZDU4ZGM0Yi0wYzgwLTQwZDYtYWE5OC0yNDk5YzEzZTg2NmMiLCJleHAiOjE0ODM5MTY3OTgsIm5iZiI6MCwiaWF0IjoxNDgzOTE2NDk4LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgxODAvYXV0aC9yZWFsbXMvbXktcGhhcm1hIiwiYXVkIjoicGhhcm1hLXdlYmFwcCIsInN1YiI6IjIxMjI2YzNlLWYwY2UtNGRjNC1hYzk0LTRlNTVmZjc4YWRlMSIsInR5cCI6IkJlYXJlciIsImF6cCI6InBoYXJtYS13ZWJhcHAiLCJub25jZSI6Ijg4MWI1MjhhLWFkOTktNDcwYy04YzJiLTlhYjI0MzM2N2IwOCIsImF1dGhfdGltZSI6MTQ4MzkxNjQ5OCwic2Vzc2lvbl9zdGF0ZSI6Ijg4ZmExODJjLTljOTctNGI2Ny1hMTMzLTk5YjFkNmU4OTZiYyIsImFjciI6IjEiLCJjbGllbnRfc2Vzc2lvbiI6IjJiM2FjNDg1LWQ5ZjUtNDc2ZC1hYjQ1LTA2ZGZmM2VjMTQxMiIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vbG9jYWxob3N0OjkwMDAiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsInZpZXctcHJvZmlsZSJdfX0sIm5hbWUiOiJVc2VyMSBGTiBVc2VyMSBMTiIsInByZWZlcnJlZF91c2VybmFtZSI6InVzZXIxIiwiZ2l2ZW5fbmFtZSI6IlVzZXIxIEZOIiwiZmFtaWx5X25hbWUiOiJVc2VyMSBMTiIsImVtYWlsIjoidXNlcjFAeW91LmNvbSJ9.M1RvECaBV3jvNXRxQzLzS4bfKnK-gQp85mkr9GD8HbOsGRui81pZP3Pb_NJ-ieaQ7pca7tO_06UNeSqbHut7c1APV3_GEGTnwuCkKdbu1QKrVwXSXMWNyt0nu_MOdjhzG3bQat3aG68b744KdCSi5i8OBg2L4I3Zmc6nPX5vklf1U7LUXyvs_bswLPZEy1_VQ_ACu_BSIVA8iv64Nl4ng4QlEc6pyEHbhQ2pKpE7wNIiZe-ndfeQWU5FgnV0Ya16b2Up9ZnFw7fpGHDGjzlIEV_As3K32vON171OuAhTKmIbVnG4kuoijQzeqHmkoB-ldfMKPPlLheSILtHvRn8WkA"}},"statusText":""}

  1. The endpoint admin/hello is not reachable.

This is the stack trace :

[org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@63858877:org.apache.tomcat.util.net.NioChannel@4d4fa76c:java.nio.channels.SocketChannel[connected local=/0:0:0:0:0:0:0:1:8080 remote=/0:0:0:0:0:0:0:1:51832]], Status in: [OPEN_READ], State out: [OPEN]
    2017-01-09 07:11:46.739 DEBUG 14692 --- [nio-8080-exec-2] o.a.coyote.http11.Http11InputBuffer      : Received [GET /admin/hello HTTP/1.1
    Host: localhost:8080
    Connection: keep-alive
    Pragma: no-cache
    Cache-Control: no-cache
    Access-Control-Allow-Origin: *
    Accept: application/json, text/plain, */*
    Origin: http://localhost:9000
    User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36
    Authorization: BEARER eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJSN2JSQUgtcmUwNnVEMHh4Vlotd1dyWVIycVI0S0pyMDFIZWQ2QmJMNnA4In0.eyJqdGkiOiIyMzMzZmI0OC0yMGE2LTQ2YzctODM1YS0wNGRiMDYyNmEyYWEiLCJleHAiOjE0ODM5MTc0MDQsIm5iZiI6MCwiaWF0IjoxNDgzOTE3MTA0LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgxODAvYXV0aC9yZWFsbXMvbXktcGhhcm1hIiwiYXVkIjoicGhhcm1hLXdlYmFwcCIsInN1YiI6IjIxMjI2YzNlLWYwY2UtNGRjNC1hYzk0LTRlNTVmZjc4YWRlMSIsInR5cCI6IkJlYXJlciIsImF6cCI6InBoYXJtYS13ZWJhcHAiLCJub25jZSI6IjQwZDc3MzAyLWViNTYtNGFhMS05OTllLTcyNDRiNmY3MTFkYiIsImF1dGhfdGltZSI6MTQ4MzkxNzEwNCwic2Vzc2lvbl9zdGF0ZSI6IjdmODlkODMyLTNkNTktNDc1Yi04NzI3LTRiZGQ5MDc4ODQ5YSIsImFjciI6IjEiLCJjbGllbnRfc2Vzc2lvbiI6IjJiOGNjMzc3LWY1NjUtNGE3NS1iMWY5LWEyNWFlZTY0ZGM3MCIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vbG9jYWxob3N0OjkwMDAiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsInZpZXctcHJvZmlsZSJdfX0sIm5hbWUiOiJVc2VyMSBGTiBVc2VyMSBMTiIsInByZWZlcnJlZF91c2VybmFtZSI6InVzZXIxIiwiZ2l2ZW5fbmFtZSI6IlVzZXIxIEZOIiwiZmFtaWx5X25hbWUiOiJVc2VyMSBMTiIsImVtYWlsIjoidXNlcjFAeW91LmNvbSJ9.S5Jhrea_JzlXEuMPSfJ9Sd8HdNQyknklfdZFDMH_vaFWHiQShVVQAhM3wbwrz8NoJs3M6iFnkA-kuMPhCUR52y65HJ9mEXSxrUN6hPY4U9mEYIKw_kGVXFf_xOirA8lO9cvEmw7c7p2BN0DWmi85RshqhM6CEdGAtIL4z-rl-b3UDJkm9dT3uaMxcYb3l8lq0AkixqnaI8seFLdLgacdhfMblnKuyP6bgWUD2jl2X9ruVGfIHQeBdA19WesMJKHm9XqQaF1mjl0AM0k52bU7GZZ6cOD3yFwl2RMAUMlUPPyX9xq2L5kNEsgYdw4qlgdvjLaX_HipqHh7JHQksJv4Sg
    Referer: http://localhost:9000/
    Accept-Encoding: gzip, deflate, sdch, br
    Accept-Language: en-GB,en;q=0.8,en-US;q=0.6,fr;q=0.4

    ]
    2017-01-09 07:11:46.740 DEBUG 14692 --- [nio-8080-exec-2] o.a.c.authenticator.AuthenticatorBase    : Security checking request GET /admin/hello
    2017-01-09 07:11:46.740 DEBUG 14692 --- [nio-8080-exec-2] org.apache.catalina.realm.RealmBase      :   No applicable constraints defined
    2017-01-09 07:11:46.740 DEBUG 14692 --- [nio-8080-exec-2] o.a.c.authenticator.AuthenticatorBase    :  Not subject to any constraint
    2017-01-09 07:11:46.740 DEBUG 14692 --- [nio-8080-exec-2] org.apache.tomcat.util.http.Parameters   : Set encoding to UTF-8
    2017-01-09 07:11:46.740 DEBUG 14692 --- [nio-8080-exec-2] o.s.b.w.f.OrderedRequestContextFilter    : Bound request context to thread: org.apache.catalina.connector.RequestFacade@2683da6f
    2017-01-09 07:11:46.740 DEBUG 14692 --- [nio-8080-exec-2] o.s.s.w.u.matcher.AntPathRequestMatcher  : Request 'GET /admin/hello' doesn't match 'OPTIONS /**
    2017-01-09 07:11:46.741 DEBUG 14692 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy        : /admin/hello at position 1 of 13 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
    2017-01-09 07:11:46.741 DEBUG 14692 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy        : /admin/hello at position 2 of 13 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
    2017-01-09 07:11:46.742 DEBUG 14692 --- [nio-8080-exec-2] w.c.HttpSessionSecurityContextRepository : No HttpSession currently exists
    2017-01-09 07:11:46.742 DEBUG 14692 --- [nio-8080-exec-2] w.c.HttpSessionSecurityContextRepository : No SecurityContext was available from the HttpSession: null. A new one will be created.
    2017-01-09 07:11:46.743 DEBUG 14692 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy        : /admin/hello at position 3 of 13 in additional filter chain; firing Filter: 'HeaderWriterFilter'
    2017-01-09 07:11:46.743 DEBUG 14692 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy        : /admin/hello at position 4 of 13 in additional filter chain; firing Filter: 'CsrfFilter'
    2017-01-09 07:11:46.744 DEBUG 14692 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy        : /admin/hello at position 5 of 13 in additional filter chain; firing Filter: 'KeycloakPreAuthActionsFilter'
    2017-01-09 07:11:46.744 DEBUG 14692 --- [nio-8080-exec-2] o.k.adapters.PreAuthActionsHandler       : adminRequest http://localhost:8080/admin/hello
    2017-01-09 07:11:46.744 DEBUG 14692 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy        : /admin/hello at position 6 of 13 in additional filter chain; firing Filter: 'LogoutFilter'
    2017-01-09 07:11:46.744 DEBUG 14692 --- [nio-8080-exec-2] o.s.s.w.u.matcher.AntPathRequestMatcher  : Request 'GET /admin/hello' doesn't match 'POST /sso/logout
    2017-01-09 07:11:46.744 DEBUG 14692 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy        : /admin/hello at position 7 of 13 in additional filter chain; firing Filter: 'KeycloakAuthenticationProcessingFilter'
    2017-01-09 07:11:46.744 DEBUG 14692 --- [nio-8080-exec-2] o.s.s.web.util.matcher.OrRequestMatcher  : Trying to match using Ant [pattern='/sso/login']
    2017-01-09 07:11:46.744 DEBUG 14692 --- [nio-8080-exec-2] o.s.s.w.u.matcher.AntPathRequestMatcher  : Checking match of request : '/admin/hello'; against '/sso/login'
    2017-01-09 07:11:46.744 DEBUG 14692 --- [nio-8080-exec-2] o.s.s.web.util.matcher.OrRequestMatcher  : Trying to match using RequestHeaderRequestMatcher [expectedHeaderName=Authorization, expectedHeaderValue=null]
    2017-01-09 07:11:46.744 DEBUG 14692 --- [nio-8080-exec-2] o.s.s.web.util.matcher.OrRequestMatcher  : matched
    2017-01-09 07:11:46.744 DEBUG 14692 --- [nio-8080-exec-2] f.KeycloakAuthenticationProcessingFilter : Request is to process authentication
    2017-01-09 07:11:46.744 DEBUG 14692 --- [nio-8080-exec-2] f.KeycloakAuthenticationProcessingFilter : Attempting Keycloak authentication
    2017-01-09 07:11:46.871 DEBUG 14692 --- [nio-8080-exec-2] a.s.a.SpringSecurityRequestAuthenticator : Completing bearer authentication. Bearer roles: [uma_authorization] 
    2017-01-09 07:11:46.871 DEBUG 14692 --- [nio-8080-exec-2] o.k.adapters.RequestAuthenticator        : User 'user1' invoking 'http://localhost:8080/admin/hello' on client 'pharma-backend'
    2017-01-09 07:11:46.871 DEBUG 14692 --- [nio-8080-exec-2] o.k.adapters.RequestAuthenticator        : Bearer AUTHENTICATED
    2017-01-09 07:11:46.871 DEBUG 14692 --- [nio-8080-exec-2] f.KeycloakAuthenticationProcessingFilter : Auth outcome: AUTHENTICATED
    2017-01-09 07:11:46.871 DEBUG 14692 --- [nio-8080-exec-2] o.s.s.authentication.ProviderManager     : Authentication attempt using org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider
    2017-01-09 07:11:46.872 DEBUG 14692 --- [nio-8080-exec-2] o.s.b.f.s.DefaultListableBeanFactory     : Returning cached instance of singleton bean 'delegatingApplicationListener'
    2017-01-09 07:11:46.889 DEBUG 14692 --- [nio-8080-exec-2] o.k.a.s.management.HttpSessionManager    : Session created: D309F84825BE807C7B34F16B111E92CD
    2017-01-09 07:11:46.890 DEBUG 14692 --- [nio-8080-exec-2] o.s.s.core.session.SessionRegistryImpl   : Registering session D309F84825BE807C7B34F16B111E92CD, for principal user1
    2017-01-09 07:11:46.891 DEBUG 14692 --- [nio-8080-exec-2] f.KeycloakAuthenticationProcessingFilter : Authentication success using bearer token/basic authentication. Updating SecurityContextHolder to contain: org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken@a08a84cd: Principal: user1; Credentials: [PROTECTED]; Authenticated: true; Details: org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount@4ebe3c30; Granted Authorities: KeycloakRole{role='uma_authorization'}
    2017-01-09 07:11:46.891 DEBUG 14692 --- [nio-8080-exec-2] o.s.b.f.s.DefaultListableBeanFactory     : Returning cached instance of singleton bean 'delegatingApplicationListener'
    2017-01-09 07:11:46.891 DEBUG 14692 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy        : /admin/hello at position 8 of 13 in additional filter chain; firing Filter: 'RequestCacheAwareFilter'
    2017-01-09 07:11:46.891 DEBUG 14692 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy        : /admin/hello at position 9 of 13 in additional filter chain; firing Filter: 'SecurityContextHolderAwareRequestFilter'
    2017-01-09 07:11:46.892 DEBUG 14692 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy        : /admin/hello at position 10 of 13 in additional filter chain; firing Filter: 'AnonymousAuthenticationFilter'
    2017-01-09 07:11:46.892 DEBUG 14692 --- [nio-8080-exec-2] o.s.s.w.a.AnonymousAuthenticationFilter  : SecurityContextHolder not populated with anonymous token, as it already contained: 'org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken@a08a84cd: Principal: user1; Credentials: [PROTECTED]; Authenticated: true; Details: org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount@4ebe3c30; Granted Authorities: KeycloakRole{role='uma_authorization'}'
    2017-01-09 07:11:46.892 DEBUG 14692 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy        : /admin/hello at position 11 of 13 in additional filter chain; firing Filter: 'SessionManagementFilter'
    2017-01-09 07:11:46.892 DEBUG 14692 --- [nio-8080-exec-2] s.CompositeSessionAuthenticationStrategy : Delegating to org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy@29a23c3d
    2017-01-09 07:11:46.892 DEBUG 14692 --- [nio-8080-exec-2] o.s.s.core.session.SessionRegistryImpl   : Registering session D309F84825BE807C7B34F16B111E92CD, for principal user1
    2017-01-09 07:11:46.892 DEBUG 14692 --- [nio-8080-exec-2] o.s.s.core.session.SessionRegistryImpl   : Removing session D309F84825BE807C7B34F16B111E92CD from principal's set of registered sessions
    2017-01-09 07:11:46.892 DEBUG 14692 --- [nio-8080-exec-2] o.s.s.core.session.SessionRegistryImpl   : Removing principal user1 from registry
    2017-01-09 07:11:46.892 DEBUG 14692 --- [nio-8080-exec-2] s.CompositeSessionAuthenticationStrategy : Delegating to org.springframework.security.web.csrf.CsrfAuthenticationStrategy@20f0cc02
    2017-01-09 07:11:46.892 DEBUG 14692 --- [nio-8080-exec-2] w.c.HttpSessionSecurityContextRepository : SecurityContext 'org.springframework.security.core.context.SecurityContextImpl@a08a84cd: Authentication: org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken@a08a84cd: Principal: user1; Credentials: [PROTECTED]; Authenticated: true; Details: org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount@4ebe3c30; Granted Authorities: KeycloakRole{role='uma_authorization'}' stored to HttpSession: 'org.apache.catalina.session.StandardSessionFacade@1a43aa14
    2017-01-09 07:11:46.892 DEBUG 14692 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy        : /admin/hello at position 12 of 13 in additional filter chain; firing Filter: 'ExceptionTranslationFilter'
    2017-01-09 07:11:46.892 DEBUG 14692 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy        : /admin/hello at position 13 of 13 in additional filter chain; firing Filter: 'FilterSecurityInterceptor'
    2017-01-09 07:11:46.893 DEBUG 14692 --- [nio-8080-exec-2] o.s.s.w.u.matcher.AntPathRequestMatcher  : Request 'GET /admin/hello' doesn't match 'POST /sso/logout
    2017-01-09 07:11:46.893 DEBUG 14692 --- [nio-8080-exec-2] o.s.s.w.u.matcher.AntPathRequestMatcher  : Checking match of request : '/admin/hello'; against '/api/v1*'
    2017-01-09 07:11:46.893 DEBUG 14692 --- [nio-8080-exec-2] o.s.s.w.u.matcher.AntPathRequestMatcher  : Checking match of request : '/admin/hello'; against '/admin/hello*'
    2017-01-09 07:11:46.893 DEBUG 14692 --- [nio-8080-exec-2] o.s.s.w.a.i.FilterSecurityInterceptor    : Secure object: FilterInvocation: URL: /admin/hello; Attributes: [hasRole('ROLE_admin')]
    2017-01-09 07:11:46.893 DEBUG 14692 --- [nio-8080-exec-2] o.s.s.w.a.i.FilterSecurityInterceptor    : Previously Authenticated: org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken@a08a84cd: Principal: user1; Credentials: [PROTECTED]; Authenticated: true; Details: org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount@4ebe3c30; Granted Authorities: KeycloakRole{role='uma_authorization'}
    2017-01-09 07:11:46.895 DEBUG 14692 --- [nio-8080-exec-2] o.s.s.access.vote.AffirmativeBased       : Voter: org.springframework.security.web.access.expression.WebExpressionVoter@58311096, returned: -1
    2017-01-09 07:11:46.896 DEBUG 14692 --- [nio-8080-exec-2] o.s.b.f.s.DefaultListableBeanFactory     : Returning cached instance of singleton bean 'delegatingApplicationListener'
    2017-01-09 07:11:46.899 DEBUG 14692 --- [nio-8080-exec-2] o.s.s.w.a.ExceptionTranslationFilter     : Access is denied (user is not anonymous); delegating to AccessDeniedHandler

    org.springframework.security.access.AccessDeniedException: Access is denied
        at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84) ~[spring-security-core-4.1.4.RELEASE.jar:4.1.4.RELEASE]
        at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:233) ~[spring-security-core-4.1.4.RELEASE.jar:4.1.4.RELEASE]
        at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:124) ~[spring-security-web-4.1.4.RELEASE.jar:4.1.4.RELEASE]
        at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:91) ~[spring-security-web-4.1.4.RELEASE.jar:4.1.4.RELEASE]
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) [spring-security-web-4.1.4.RELEASE.jar:4.1.4.RELEASE]
        at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:115) ~[spring-security-web-4.1.4.RELEASE.jar:4.1.4.RELEASE]
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) [spring-security-web-4.1.4.RELEASE.jar:4.1.4.RELEASE]
  ...
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_111]
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-8.5.6.jar:8.5.6]
        at java.lang.Thread.run(Thread.java:745) [na:1.8.0_111]

    2017-01-09 07:11:46.899 DEBUG 14692 --- [nio-8080-exec-2] o.s.s.w.header.writers.HstsHeaderWriter  : Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@5ca0c4c5
    2017-01-09 07:11:46.899 DEBUG 14692 --- [nio-8080-exec-2] w.c.HttpSessionSecurityContextRepository : SecurityContext 'org.springframework.security.core.context.SecurityContextImpl@a08a84cd: Authentication: org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken@a08a84cd: Principal: user1; Credentials: [PROTECTED]; Authenticated: true; Details: org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount@4ebe3c30; Granted Authorities: KeycloakRole{role='uma_authorization'}' stored to HttpSession: 'org.apache.catalina.session.StandardSessionFacade@1a43aa14
    2017-01-09 07:11:46.899 DEBUG 14692 --- [nio-8080-exec-2] s.s.w.c.SecurityContextPersistenceFilter : SecurityContextHolder now cleared, as request processing completed
    2017-01-09 07:11:46.899 DEBUG 14692 --- [nio-8080-exec-2] o.s.b.w.f.OrderedRequestContextFilter    : Cleared thread-bound request context: org.apache.catalina.connector.RequestFacade@2683da6f
    2017-01-09 07:11:46.900 DEBUG 14692 --- [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost]           : Processing ErrorPage[errorCode=0, location=/error]
    2017-01-09 07:11:46.901 DEBUG 14692 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : DispatcherServlet with name 'dispatcherServlet' processing GET request for [/error]
    2017-01-09 07:11:46.902 DEBUG 14692 --- [nio-8080-exec-2] s.w.s.m.m.a.RequestMappingHandlerMapping : Looking up handler method for path /error
    2017-01-09 07:11:46.904 DEBUG 14692 --- [nio-8080-exec-2] s.w.s.m.m.a.RequestMappingHandlerMapping : Returning handler method [public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)]
    2017-01-09 07:11:46.904 DEBUG 14692 --- [nio-8080-exec-2] o.s.b.f.s.DefaultListableBeanFactory     : Returning cached instance of singleton bean 'basicErrorController'
    2017-01-09 07:11:46.904 DEBUG 14692 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Last-Modified value for [/error] is: -1
    2017-01-09 07:11:46.927 DEBUG 14692 --- [nio-8080-exec-2] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Written [{timestamp=Mon Jan 09 07:11:46 MYT 2017, status=403, error=Forbidden, message=Access is denied, path=/admin/hello}] as "application/json" using [org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@45db4b8b]
    2017-01-09 07:11:46.927 DEBUG 14692 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Null ModelAndView returned to DispatcherServlet with name 'dispatcherServlet': assuming HandlerAdapter completed request handling
    2017-01-09 07:11:46.927 DEBUG 14692 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Successfully completed request
    2017-01-09 07:11:46.928 DEBUG 14692 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet]    :  Disabling the response for futher output
    2017-01-09 07:11:46.928 DEBUG 14692 --- [nio-8080-exec-2] o.apache.coyote.http11.Http11Processor   : Socket: [org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@63858877:org.apache.tomcat.util.net.NioChannel@4d4fa76c:java.nio.channels.SocketChannel[connected local=/0:0:0:0:0:0:0:1:8080 remote=/0:0:0:0:0:0:0:1:51832]], Status in: [OPEN_READ], State out: [OPEN]

Upvotes: 8

Views: 16313

Answers (4)

ch4mp
ch4mp

Reputation: 12639

2022 updates

Keycloak spring adapters are deprecated. Do not use it.

Instead, use directly spring-boot-starter-oauth2-resource-server or better libs from this repo. Here after, the configuration for both solutions (with a thin wrapper I wrote to enable resource-server configuration from properties file, and "pure Spring" which is quite more verbose).

Resource-server security with the two options

requirements

  • Spring Boot 2.7 and later: without the deprecated WebSecurityConfigurerAdapter
  • User authorities from realm_access.roles, resource-access.spring-addons-confidential.roles and resource-access.spring-addons-public.roles
  • 401 HTTP status if authentication is missing/invalid and 403 if access is denied
  • CORS: required for "pure" resource servers; UI components being served from another socket, host, or domain; cross-origin is necessary
  • "Stateless" session management: no servlet session; client state is managed with URIs and access token
  • disabled CSRF protection if sessions are disabled (but enabled CSRF if session management is not set to stateless)
  • OpenAPI spec (from spring-doc-openapi) as well as readiness and liveness probes should accessible to anonymous
  • All other routes restricted to authenticated users
  • Force HTTPS usage if SSL is enabled

spring-addons

All of above is done in 2 very simple steps:

  • choose one of the 4 starters (depends you're configuring servlet / reactive and JWT-decoder / introspection resource-server): spring-addons-{webmvc|webflux}-{jwt|introspecting}-resource-server
  • define a few properties:
com.c4-soft.springaddons.security.issuers[0].location=https://localhost:8443/realms/master
com.c4-soft.springaddons.security.issuers[0].authorities.claims=realm_access.roles,resource_access.spring-addons-public.roles,resource_access.spring-addons-confidential.roles
com.c4-soft.springaddons.security.cors[0].path=/greet/**
com.c4-soft.springaddons.security.permit-all=/actuator/health/readiness,/actuator/health/liveness,/v3/api-docs/**

And this is all of java conf you need (every thing else is auto-configured with components you can override):

@Configuration
@EnableMethodSecurity
public static class WebSecurityConfig {
}

spring-boot-starter-oauth2-resource-server

You'll have to write by yourself all that was auto-configured above, including authorities mapping from all private claims Keycloak puts it into. This is quite some Java conf. Acceptable if you maintain few applications, less in micro-services scenarios:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class WebSecurityConfig {

    public interface Jwt2AuthoritiesConverter extends Converter<Jwt, Collection<? extends GrantedAuthority>> {
    }

    @SuppressWarnings("unchecked")
    @Bean
    public Jwt2AuthoritiesConverter authoritiesConverter() {
        // This is a converter for roles as embedded in the JWT by a Keycloak server
        // Roles are taken from both realm_access.roles & resource_access.{client}.roles
        return jwt -> {
            final var realmAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("realm_access", Map.of());
            final var realmRoles = (Collection<String>) realmAccess.getOrDefault("roles", List.of());

            final var resourceAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("resource_access", Map.of());
            // We assume here you have "spring-addons-confidential" and "spring-addons-public" clients configured with "client roles" mapper in Keycloak
            final var confidentialClientAccess = (Map<String, Object>) resourceAccess.getOrDefault("spring-addons-confidential", Map.of());
            final var confidentialClientRoles = (Collection<String>) confidentialClientAccess.getOrDefault("roles", List.of());
            final var publicClientAccess = (Map<String, Object>) resourceAccess.getOrDefault("spring-addons-public", Map.of());
            final var publicClientRoles = (Collection<String>) publicClientAccess.getOrDefault("roles", List.of());

            return Stream.concat(realmRoles.stream(), Stream.concat(confidentialClientRoles.stream(), publicClientRoles.stream()))
                    .map(SimpleGrantedAuthority::new).toList();
        };
    }

    public interface Jwt2AuthenticationConverter extends Converter<Jwt, AbstractAuthenticationToken> {
    }

    @Bean
    public Jwt2AuthenticationConverter authenticationConverter(Jwt2AuthoritiesConverter authoritiesConverter) {
        return jwt -> new JwtAuthenticationToken(jwt, authoritiesConverter.convert(jwt));
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, Jwt2AuthenticationConverter authenticationConverter, ServerProperties serverProperties)
            throws Exception {

        // Enable OAuth2 with custom authorities mapping
        http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(authenticationConverter);

        // Enable anonymous
        http.anonymous();

        // Enable and configure CORS
        http.cors().configurationSource(corsConfigurationSource());

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

        // Enable CSRF with cookie repo because of state-less session-management
        http.csrf().csrfTokenRepository(new CookieCsrfTokenRepository());

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

        // If SSL enabled, disable http (https only)
        if (serverProperties.getSsl() != null && serverProperties.getSsl().isEnabled()) {
            http.requiresChannel().anyRequest().requiresSecure();
        } else {
            http.requiresChannel().anyRequest().requiresInsecure();
        }

        // Route security: authenticated to all routes but actuator and Swagger-UI
        // @formatter:off
        http.authorizeRequests()
            .antMatchers("/actuator/health/readiness", "/actuator/health/liveness", "/v3/api-docs/**").permitAll()
            .anyRequest().authenticated();
        // @formatter:on

        return http.build();
    }

    private CorsConfigurationSource corsConfigurationSource() {
        // Very permissive CORS config...
        final var configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("*"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setExposedHeaders(Arrays.asList("*"));

        // Limited to API routes (neither actuator nor Swagger-UI)
        final var source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/greet/**", configuration);

        return source;
    }
}

PS

The difference between Spring roles vs authorities mentionned in other answers still applies: for Spring, roles are just authorities with the ROLE_ prefix. This means that hasRole("USER") is strictly equivalent to hasAuthority("ROLE_USER").

If you want to use hasRole and not hasAuthority, You'll have to choose between one of the following:

  • prefix all roles with ROLE_ in Keycloak
  • configure authorities mapper in Spring to add ROLE_ prefix (there is a property for that in spring-addons lib referenced above)

Upvotes: 1

Fernix
Fernix

Reputation: 371

You could implement SimpleAuthorityMapper() :

@Bean
public GrantedAuthoritiesMapper grantedAuthoritiesMapper() {
  SimpleAuthorityMapper mapper = new SimpleAuthorityMapper();
  mapper.setConvertToUpperCase(true);
  return mapper;
}

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
    keycloakAuthenticationProvider.setGrantedAuthoritiesMapper( grantedAuthoritiesMapper());
    auth.authenticationProvider(keycloakAuthenticationProvider);
}

configureGlobal: Here we change the Granted Authority Mapper. By default in Spring Security, roles are prefixed with ROLE_. We could change that in our Realm configuration but it could be confusing for other applications that do not know this convention, so here we assign a SimpleAuthorityMapper that will make sure no prefix is added.

Click here!

ROLE_ is the prefix default for sring security roles.

Upvotes: 0

hakamairi
hakamairi

Reputation: 4678

I went through the same and here are my findings.

org.springframework.security.access.vote.RoleVoter assumes that your roles start from

private String rolePrefix = "ROLE_";

There is no such assumption in Keycloak (unless you name all your roles ROLE_someName), therefore no roles are actually found matching.

Similar explanation can be obtained from Spring Security Javadoc of SecurityExpressionOperations class for hasRole method

This is similar to {@link #hasAuthority(String)} except that this method implies
that the String passed in is a role. For example, if "USER" is passed in the
implementation may convert it to use "ROLE_USER" instead. The way in which the role
is converted may depend on the implementation settings.

To sum it up, I've ended with exactly the same solution as you did, using hasAuthority instead of hasRole now.

Upvotes: 9

nono
nono

Reputation: 1692

I solved it myself by replacing hasRole() by hasAuthority(). I still don't know why keycloak role is mapped to spring security authority. Any explaination is welcomed. Thank you.

Upvotes: 5

Related Questions