javavaadin

Oauth + Spring Security + mapping role claim to Grants


I want to build a web application that used roles that are passed via the identity token.
I got a keycloak server that is configured to add a claim that contains the roles. So far so good now I found this doc, that shows a simple way to get the id-token and return the Grands.

But it doesn't really work when I add the RolesAllowed Annotation.

It seams like the rolles are added at thre wrong place somehow. As the logs show (i think):

2024-09-30T20:38:18.061+02:00 DEBUG 234589 --- [0.1-8080-exec-1] o.s.security.web.FilterChainProxy        : Securing GET /
2024-09-30T20:38:18.068+02:00 DEBUG 234589 --- [0.1-8080-exec-1] o.s.s.w.a.AnonymousAuthenticationFilter  : Set SecurityContextHolder to anonymous SecurityContext
2024-09-30T20:38:18.080+02:00 DEBUG 234589 --- [0.1-8080-exec-1] o.s.s.w.s.HttpSessionRequestCache        : Saved request http://localhost:8080/?continue to session
2024-09-30T20:38:18.080+02:00 DEBUG 234589 --- [0.1-8080-exec-1] s.w.a.DelegatingAuthenticationEntryPoint : Trying to match using com.vaadin.flow.spring.security.VaadinWebSecurity$$Lambda$1158/0x00000008016d0630@727d56fa
2024-09-30T20:38:18.082+02:00 DEBUG 234589 --- [0.1-8080-exec-1] s.w.a.DelegatingAuthenticationEntryPoint : Trying to match using And [Not [RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]], MediaTypeRequestMatcher [contentNegotiationStrategy=org.springframework.web.accept.ContentNegotiationManager@20eaf588, matchingMediaTypes=[application/xhtml+xml, image/*, text/html, text/plain], useEquals=false, ignoredMediaTypes=[*/*]]]
2024-09-30T20:38:18.083+02:00 DEBUG 234589 --- [0.1-8080-exec-1] s.w.a.DelegatingAuthenticationEntryPoint : Match found! Executing org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint@4d4a6011
2024-09-30T20:38:18.083+02:00 DEBUG 234589 --- [0.1-8080-exec-1] o.s.s.web.DefaultRedirectStrategy        : Redirecting to http://localhost:8080/oauth2/authorization/my-app-ui
2024-09-30T20:38:18.102+02:00 DEBUG 234589 --- [0.1-8080-exec-2] o.s.security.web.FilterChainProxy        : Securing GET /oauth2/authorization/my-app-ui
2024-09-30T20:38:18.106+02:00 DEBUG 234589 --- [0.1-8080-exec-2] o.s.s.web.DefaultRedirectStrategy        : Redirecting to https://login.myoauth.com/realms/sample-time/protocol/openid-connect/auth?response_type=code&client_id=my-app-ui&scope=openid%20profile%20roles%20email%20my-app-roles&state=kv3hIlMhmqahG0Cz6_0CcSpc3PH6bWNBlWpXYJv_YrE%3D&redirect_uri=http://localhost:8080/login/oauth2/code/my-app-ui&nonce=BEGDM-kAk3VQytRR8298ZTNApIuTdn22CAi51OU2gug
2024-09-30T20:38:18.217+02:00 DEBUG 234589 --- [0.1-8080-exec-3] o.s.security.web.FilterChainProxy        : Securing GET /login/oauth2/code/my-app-ui?state=kv3hIlMhmqahG0Cz6_0CcSpc3PH6bWNBlWpXYJv_YrE%3D&session_state=8bac785f-65c3-4bd4-9d43-71a16ef98ef2&iss=https%3A%2F%2Flogin.myoauth.com%2Frealms%2Fsample-time&code=060a77c2-91b1-4463-b7fa-29047449888a.8bac785f-65c3-4bd4-9d43-71a16ef98ef2.db94b518-17e9-4c7e-86e4-ab55237c64dd
Assigned role: my-app-user
Assigned role: my-app-admin
Assigned role: my-app-manager
2024-09-30T20:38:28.174+02:00 DEBUG 234589 --- [0.1-8080-exec-5] o.s.security.web.FilterChainProxy        : Securing GET /sw-runtime-resources-precache.js
2024-09-30T20:38:28.174+02:00 DEBUG 234589 --- [0.1-8080-exec-4] o.s.security.web.FilterChainProxy        : Securing GET /sw.js
2024-09-30T20:38:28.174+02:00 DEBUG 234589 --- [0.1-8080-exec-5] o.s.s.w.a.AnonymousAuthenticationFilter  : Set SecurityContextHolder to anonymous SecurityContext
2024-09-30T20:38:28.174+02:00 DEBUG 234589 --- [0.1-8080-exec-4] o.s.s.w.a.AnonymousAuthenticationFilter  : Set SecurityContextHolder to anonymous SecurityContext
2024-09-30T20:38:28.175+02:00 DEBUG 234589 --- [0.1-8080-exec-5] o.s.security.web.FilterChainProxy        : Secured GET /sw-runtime-resources-precache.js
2024-09-30T20:38:28.175+02:00 DEBUG 234589 --- [0.1-8080-exec-4] o.s.security.web.FilterChainProxy        : Secured GET /sw.js
2024-09-30T20:38:28.176+02:00 DEBUG 234589 --- [0.1-8080-exec-3] .s.ChangeSessionIdAuthenticationStrategy : Changed session id from E5197EAC478DFCFBF4F7F7E4CD45FDAC
2024-09-30T20:38:28.177+02:00 DEBUG 234589 --- [0.1-8080-exec-3] w.c.HttpSessionSecurityContextRepository : Stored SecurityContextImpl [Authentication=OAuth2AuthenticationToken [Principal=Name: [bbce6fa1-4f69-420c-9803-4211cffef5fd], Granted Authorities: [[OIDC_USER, SCOPE_email, SCOPE_my-app-roles, SCOPE_openid, SCOPE_profile]], User Attributes: [{at_hash=J5jVwrXQvDh6HLyZtYNxRA, sub=bbce6fa1-4f69-420c-9803-4211cffef5fd, email_verified=true, my-app-roles=[my-app-user, my-app-admin, my-app-manager], iss=https://login.myoauth.com/realms/sample-time, typ=ID, preferred_username=samplenamee, given_name=John, nonce=BEGDM-kAk3VQytRR8298ZTNApIuTdn22CAi51OU2gug, sid=8bac785f-65c3-4bd4-9d43-71a16ef98ef2, aud=[my-app-ui], acr=0, azp=my-app-ui, auth_time=2024-09-30T18:36:45Z, name=John namee, exp=2024-09-30T18:43:18Z, family_name=namee, iat=2024-09-30T18:38:18Z, email=John@namee.net, jti=fd8ac200-7e6b-4587-8c79-4ad860709c14}], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=E5197EAC478DFCFBF4F7F7E4CD45FDAC], Granted Authorities=[my-app-user, my-app-admin, my-app-manager]]] to HttpSession [org.apache.catalina.session.StandardSessionFacade@353b1629]
2024-09-30T20:38:28.177+02:00 DEBUG 234589 --- [0.1-8080-exec-3] .s.o.c.w.OAuth2LoginAuthenticationFilter : Set SecurityContextHolder to OAuth2AuthenticationToken [Principal=Name: [bbce6fa1-4f69-420c-9803-4211cffef5fd], Granted Authorities: [[OIDC_USER, SCOPE_email, SCOPE_my-app-roles, SCOPE_openid, SCOPE_profile]], User Attributes: [{at_hash=J5jVwrXQvDh6HLyZtYNxRA, sub=bbce6fa1-4f69-420c-9803-4211cffef5fd, email_verified=true, my-app-roles=[my-app-user, my-app-admin, my-app-manager], iss=https://login.myoauth.com/realms/sample-time, typ=ID, preferred_username=samplenamee, given_name=John, nonce=BEGDM-kAk3VQytRR8298ZTNApIuTdn22CAi51OU2gug, sid=8bac785f-65c3-4bd4-9d43-71a16ef98ef2, aud=[my-app-ui], acr=0, azp=my-app-ui, auth_time=2024-09-30T18:36:45Z, name=John namee, exp=2024-09-30T18:43:18Z, family_name=namee, iat=2024-09-30T18:38:18Z, email=John@namee.net, jti=fd8ac200-7e6b-4587-8c79-4ad860709c14}], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=E5197EAC478DFCFBF4F7F7E4CD45FDAC], Granted Authorities=[my-app-user, my-app-admin, my-app-manager]]
2024-09-30T20:38:28.178+02:00 DEBUG 234589 --- [0.1-8080-exec-3] o.s.s.web.DefaultRedirectStrategy        : Redirecting to http://localhost:8080/?continue
2024-09-30T20:38:28.192+02:00 DEBUG 234589 --- [0.1-8080-exec-6] o.s.security.web.FilterChainProxy        : Securing GET /?continue
2024-09-30T20:38:28.193+02:00 DEBUG 234589 --- [0.1-8080-exec-6] w.c.HttpSessionSecurityContextRepository : Retrieved SecurityContextImpl [Authentication=OAuth2AuthenticationToken [Principal=Name: [bbce6fa1-4f69-420c-9803-4211cffef5fd], Granted Authorities: [[OIDC_USER, SCOPE_email, SCOPE_my-app-roles, SCOPE_openid, SCOPE_profile]], User Attributes: [{at_hash=J5jVwrXQvDh6HLyZtYNxRA, sub=bbce6fa1-4f69-420c-9803-4211cffef5fd, email_verified=true, my-app-roles=[my-app-user, my-app-admin, my-app-manager], iss=https://login.myoauth.com/realms/sample-time, typ=ID, preferred_username=samplenamee, given_name=John, nonce=BEGDM-kAk3VQytRR8298ZTNApIuTdn22CAi51OU2gug, sid=8bac785f-65c3-4bd4-9d43-71a16ef98ef2, aud=[my-app-ui], acr=0, azp=my-app-ui, auth_time=2024-09-30T18:36:45Z, name=John namee, exp=2024-09-30T18:43:18Z, family_name=namee, iat=2024-09-30T18:38:18Z, email=John@namee.net, jti=fd8ac200-7e6b-4587-8c79-4ad860709c14}], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=E5197EAC478DFCFBF4F7F7E4CD45FDAC], Granted Authorities=[my-app-user, my-app-admin, my-app-manager]]]
2024-09-30T20:38:28.193+02:00 DEBUG 234589 --- [0.1-8080-exec-6] o.s.s.w.s.HttpSessionRequestCache        : Loaded matching saved request http://localhost:8080/?continue
2024-09-30T20:38:28.195+02:00 DEBUG 

This is how my security config looks:

@EnableWebSecurity
@Configuration
public class SecurityConfiguration extends VaadinWebSecurity {

    private static final String OAUTH_URL = "/oauth2/authorization/my-app-ui";

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http.oauth2Login(c -> c.loginPage(OAUTH_URL).permitAll());
    }

    @Bean
    public GrantedAuthoritiesMapper userAuthoritiesMapper() {
        return (authorities) -> {
            Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

            authorities.forEach(authority -> {
                if (OidcUserAuthority.class.isInstance(authority)) {
                    OidcUserAuthority oidcUserAuthority = (OidcUserAuthority) authority;

                    OidcIdToken idToken = oidcUserAuthority.getIdToken();
                    OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();

                    if (idToken.getClaims().containsKey("my-appp-roles")) {
                        List<String> roles = (List<String>) idToken.getClaims().get("lk-watch-roles");
                        for (String role : roles) {
                            mappedAuthorities.add(new SimpleGrantedAuthority(role));
                            System.out.println("Assigned role: " + role);
                        }
                    }
                }
            });

            return mappedAuthorities;
        };
    }

}

Do you guys see what the problem is here?

I don't this that this should need a lot of hacking around, because this should be a pretty basic problem for spring security.

Thank you for you help


Solution

  • Roles are authorities with a prefix "ROLE_". You just need to prefix your custom authorities with "ROLE_" in order for Spring Security to recognize those as roles (and not just an authority).

    Spring Security Authorization Architecture

    By default, role-based authorization rules include ROLE_ as a prefix.

    @Bean
    public GrantedAuthoritiesMapper userAuthoritiesMapper() {
        return (authorities) -> {
            Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
    
            authorities.forEach(authority -> {
                if (OidcUserAuthority.class.isInstance(authority)) {
                    OidcUserAuthority oidcUserAuthority = (OidcUserAuthority) authority;
    
                    OidcIdToken idToken = oidcUserAuthority.getIdToken();
                    OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();
    
                    if (idToken.getClaims().containsKey("my-appp-roles")) {
                        List<String> roles = (List<String>) idToken.getClaims().get("lk-watch-roles");
                        for (String role : roles) {
                            // prefix with "ROLE_"
                            mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role));
                            System.out.println("Assigned role: " + role);
                        }
                    }
                }
            });
    
            return mappedAuthorities;
        };
    }