Search code examples
spring-bootspring-securitythymeleafspring-security-oauth2

OAuth2 Client Principal do not have GrantedAuthorities when authenticated by Other Custom Authorization Server (SpringBoot2 & OAuth2)


i'm using Spring Boot2 as Framework and Thymeleaf as template engine.

in my authorization server, i added user 'admin' as 'ROLE_ADMIN'.

but in Client Application, when i loged in as 'admin' and print Authentication Object from SecurityContextHolder.getContext().getAuthentication(), Granted Authorities property has only 'ROLE_USER'.

following is my authorization server config.

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .inMemoryAuthentication()
                .withUser("admin").password(passwordEncoder().encode("123")).roles("USER", "ADMIN");
        auth
                .inMemoryAuthentication()
                .withUser("user").password(passwordEncoder().encode("123")).roles("USER");

    }

and following is Authentication Object from SecurityContextHolder.getContext().getAuthentication()'s logging code.

Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        System.out.println(auth.isAuthenticated());
        System.out.println(auth.getAuthorities());
        System.out.println(auth.getPrincipal());

and result is

//  isAuthenticated()
true

// getAuthorites()
[ROLE_USER] 

// getPrincipal()
Name: [admin], Granted Authorities: [ROLE_USER], User Attributes: [authorities=[{authority=ROLE_ADMIN}, {authority=ROLE_USER}], ...

following is my thymeleaf code.

            <div sec:authorize="isAuthenticated()">
                Text visible only to authenticated users.

                <!-- Principal name -->
                Authenticated username:
                <div sec:authentication="name"></div>

                <div sec:authorize="hasRole('USER')">Text visible to user.</div>
                <!-- i cant see this message -->
                <div sec:authorize="hasRole('ADMIN')">Text visible to admin.</div>

                Authenticated user roles:
                <!-- print '[ROLE_USER]' only -->
                <div sec:authentication="principal.authorities"></div>
            </div>

            <div sec:authorize="!isAuthenticated()">Text visible only to
                unauthenticated users.
            </div>

so, i want to access Principal.UserAttributes.authorities in thymeleaf.

i'm refering OAuth2AuthenticationToken, OAuth2User.getAttributes() and DefaultOAuth2User.toString()

how can i do this?


Solution

  • I solved.

    In Authorization Server, i configed like this.

    • AuthorizationServer WebSecurityConfigurerAdapter config
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        ...
            @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth
                    .inMemoryAuthentication()
                    .withUser("admin").password(passwordEncoder().encode("123")).roles("USER", "ADMIN").authorities("USER", "ADMIN");
            auth
                    .inMemoryAuthentication()
                    .withUser("user").password(passwordEncoder().encode("123")).roles("USER");
    
        }
        ...
    }
    

    and following is my Resource Server's /me mapping controller

    • ResourceServer /me mapped Controller
    @RestController
    public class UserController {
    
        @RequestMapping("/me")
        public Principal user(Principal principal) {
            return principal;
        }
    }
    

    and following is my Client's WebSecurityConfigurerAdapter config

    • Client WebSecurityConfigurerAdapter config
    @Configuration
    @EnableOAuth2Client
    public class WebSecurityConfigurerAdapterImpl extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .csrf().disable()
                    .authorizeRequests()
                    .antMatchers("/", "/home", "/error", "/webjars/**", "/resources/**", "/login**").permitAll()
                    .anyRequest().authenticated()
                    .and().oauth2Login();
        }
    

    and in Client's Controller, i logged like this.

    • logging Principal in Client Controller
        @GetMapping("")
        public String git1() {
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            System.out.println(auth.getPrincipal());
    
            /** Thymeleaf using this **/
            Object authenticationProperty = AuthUtils.getAuthenticationProperty(auth, "principal.attributes['authorities']");
            System.out.println(authenticationProperty.toString());
    
            return VIEW_PATH + "git1";
        }
    

    and following is the result

    Name: [admin], Granted Authorities: [ROLE_USER], User Attributes: [authorities=[{authority=USER}, {authority=ADMIN}], details={remoteAddress=127.0.0.1, sessionId=null, tokenValue=82a7a532-a31e-4d0a-bd83-f15a9cbea3bc, tokenType=Bearer, decodedDetails=null}, authenticated=true, userAuthentication={authorities=[{authority=USER}, {authority=ADMIN}], details=null, authenticated=true, principal=admin, credentials=N/A, name=admin}, oauth2Request={clientId=foo, scope=[read], requestParameters={client_id=foo}, resourceIds=[], authorities=[], approved=true, refresh=false, redirectUri=null, responseTypes=[], extensions={}, refreshTokenRequest=null, grantType=null}, clientOnly=false, principal=admin, credentials=, name=admin]
    [{authority=USER}, {authority=ADMIN}]
    

    as you can see, i added 'ROLE_USER' and 'ROLE_ADMIN' Authorities in Authorization Server.

    in Resource Server's Principal Object granted both 'ROLE_ADMIN' and 'ROLE_USER'.

    but in Client's Principal Object doesn't granted 'ROLE_ADMIN'. there is 'ROLE_USER' Only.

    and Principal.atttibutes['authorities'] has 'USER', 'ADMIN'.

    as @Rahil Husain said, there is DefaultOAuth2UserService and this service grant 'ROLE_USER' only to OAuth2User Object.

    first, i added CustomAuthoritiesExtractor via @Componenet annotation (@Bean too.) to Client.

    but this doesn't working in my projects.

    so, i implemented CustomOAuth2User and CustomOAuth2UserService.

    like this.

    • CustomOAuth2User
    public class CustomOAuth2User implements OAuth2User {
        private List<GrantedAuthority> authorities;
        private Map<String, Object> attributes;
        private String name;
    
    
        public CustomOAuth2User(List<GrantedAuthority> authorities, Map<String, Object> attributes) {
            this.authorities = authorities;
            this.attributes = attributes;
        }
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return this.authorities;
        }
    
        @Override
        public Map<String, Object> getAttributes() {
            if (this.attributes == null) {
                this.attributes = new HashMap<>();
                this.attributes.put("name", this.getName());
            }
            return attributes;
        }
    
        @Override
        public String getName() {
            return this.name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
    }
    

    and following is CustomOAuth2UserService

    • CustomOAuth2UserService
    public class CustomOAuth2UserService extends DefaultOAuth2UserService {
    
        @Override
        public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
            OAuth2User oAuth2User = super.loadUser(userRequest);
    
            AuthoritiesExtractor authoritiesExtractor = new CustomAuthoritiesExtractor();
            List<GrantedAuthority> grantedAuthorityList = authoritiesExtractor.extractAuthorities(oAuth2User.getAttributes());
            CustomOAuth2User customOAuth2User = new CustomOAuth2User(grantedAuthorityList, oAuth2User.getAttributes());
            customOAuth2User.setName(oAuth2User.getName());
    
            return customOAuth2User;
        }
    }
    

    and following is my CustomAuthoritiesExtractor. this class not used as @Bean or @Component. directly used in CustomOAuth2Service for mapping CustomOAuth2User object's authorities

    • CustomAuthoritiesExtractor
    public class CustomAuthoritiesExtractor implements AuthoritiesExtractor {
    
        @Override
        public List<GrantedAuthority> extractAuthorities(Map<String, Object> map) {
            return AuthorityUtils.commaSeparatedStringToAuthorityList(asAuthorities(map));
        }
    
        private String asAuthorities(Map<String, Object> map) {
            List<String> authorities = new ArrayList<>();
            List<LinkedHashMap<String, String>> authz =
                    (List<LinkedHashMap<String, String>>) map.get("authorities");
            for (LinkedHashMap<String, String> entry : authz) {
                authorities.add(entry.get("authority"));
            }
            return String.join(",", authorities);
        }
    }
    

    and final, i changed Client's endpoint to using my CustomOAuth2User and CustomOAuth2UserService.

    so, i changed Client's WebSecurityConfigurerAdapter config like this.

    @Configuration
    @EnableOAuth2Client
    public class WebSecurityConfigurerAdapterImpl extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .csrf().disable()
                    .authorizeRequests()
                    .antMatchers("/", "/home", "/error", "/webjars/**", "/resources/**", "/login**").permitAll()
                    .anyRequest().authenticated()
                    .and().oauth2Login()
    
    
                    /** add this config**/
                                .userInfoEndpoint()
                                        .customUserType(CustomOAuth2User.class, "teemo")
                                        .userService(this.oauth2UserService());
        }
    
        private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
            return new CustomOAuth2UserService();
        }
    

    and following is my thymeleaf.

    • thymeleaf
        <div sec:authorize="isAuthenticated()">
            Text visible only to authenticated users.
    
            Authenticated username:
            <div sec:authentication="name"></div>
    
            <div sec:authorize="hasRole('USER')">hasRole('USER')</div>
            <div sec:authorize="hasRole('ROLE_USER')">hasRole('ROLE_USER')</div>
            <div sec:authorize="hasRole('ADMIN')">hasRole('ADMIN')</div>
            <div sec:authorize="hasRole('ROLE_ADMIN')">hasRole('ROLE_ADMIN')</div>
            <!-- TRUE -->
            <div sec:authorize="hasAuthority('USER')">hasAuthority('USER')</div>
            <div sec:authorize="hasAuthority('ROLE_USER')">hasAuthority('ROLE_USER')</div>
            <!-- TRUE -->
            <div sec:authorize="hasAuthority('ADMIN')">hasAuthority('ADMIN')</div>
            <div sec:authorize="hasAuthority('ROLE_ADMIN')">hasAuthority('ROLE_ADMIN')</div>
        </div>
    
        <div sec:authorize="!isAuthenticated()">Text visible only to
                    unauthenticated users.
        </div>
    

    and following is the result.

    Text visible only to authenticated users. Authenticated username:
    admin
    hasAuthority('USER')
    hasAuthority('ADMIN')
    

    anyone who digging like me, i hope help this question and answers.

    but i don't know this is de facto-standard way.

    just.. working now.