spring-security

How to define custom granted Authorities based on user roles in Auth0 using Spring Boot


Using Auth0 as an OAuth service provider we know that roles can be affected to users (Staying just at Auth0 level for now). However those roles aren't directly considered as granted authorities when the user logs in. I wanted to be able to fetch the user roles using Auth0 management api to get user roles and define authorities based on the roles that will be granted to my athenticated user. Is that a good practice overall and if yes how should i proceed ? I have tried some specific implementations but i still face the issue.

I implemented a Customer OAuthUser, a CustomOAuthUserService and added my service in the Spring Security Configuration file.

Here is my Custom OAuthUser:

package com.interco.reconciliation.model;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;

import java.util.Collection;
import java.util.List;
import java.util.Map;

public class CustomOAuth2User extends DefaultOAuth2User {

    private final OAuth2User delegate;
    private final Collection<GrantedAuthority> authorities;


    public CustomOAuth2User(OAuth2User delegate, Collection<GrantedAuthority> authorities) {
        super(authorities, delegate.getAttributes(), "name");
        this.delegate = delegate;
        this.authorities = authorities;
    }

    @Override
    public Map<String, Object> getAttributes() {
        return this.delegate.getAttributes();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getName() {
        return this.delegate.getAttribute("name");
    }

}

Here is my Custom OAuth User Service:

package com.interco.reconciliation.service;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

import com.interco.reconciliation.model.CustomOAuth2User;
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.JsonNode;
import com.mashape.unirest.http.exceptions.UnirestException;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;


@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    @Autowired
    private Auth0ApiManagementService auth0ApiManagementService;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) {
        OAuth2User oAuth2User = super.loadUser(userRequest);
        List<GrantedAuthority> authorities = new ArrayList<>(oAuth2User.getAuthorities());
        String userId = oAuth2User.getAttribute("sub");

        try {
            HttpResponse<JsonNode> roles = this.auth0ApiManagementService.getUserRoles(userId);
            JsonNode rolesBody = roles.getBody();
            for(int i = 0; i < rolesBody.getArray().length(); i++) {
                JSONObject role = rolesBody.getArray().getJSONObject(i);
                String roleName = role.getString("name");
                authorities.add(new SimpleGrantedAuthority("ROLE_" + roleName));
            }
        } catch (UnirestException | UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
        System.out.println("User retrieved and authorities assigned");
        return new CustomOAuth2User(oAuth2User, authorities);
    }

}

Here is my Security Configuration file:

package com.interco.reconciliation.config;

import static org.springframework.security.config.Customizer.withDefaults;

import com.interco.reconciliation.service.Auth0ApiManagementService;
import com.interco.reconciliation.service.CustomOAuth2UserService;
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.JsonNode;
import com.mashape.unirest.http.exceptions.UnirestException;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;

import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import java.io.IOException;


@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {

    @Value("${okta.oauth2.issuer}")
    private String issuer;
    @Value("${okta.oauth2.client-id}")
    private String clientId;
    @Autowired
    private CustomOAuth2UserService customOAuth2UserService;


    @Bean
    public SecurityFilterChain configure(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable) // remove later or configure appropriately in production
                .cors(withDefaults()) // remove later or configure appropriately in production
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/").permitAll()
                        .anyRequest().authenticated()
                )
                .oauth2Login(oauth2Login ->
                        oauth2Login
                                .userInfoEndpoint(userInfoEndpoint ->
                                        userInfoEndpoint.userService(customOAuth2UserService)))
                .logout(logout -> logout
                        .addLogoutHandler(logoutHandler()));
        return http.build();
    }
    

    private LogoutHandler logoutHandler() {
        return (request, response, authentication) -> {
            try {
                String baseUrl = ServletUriComponentsBuilder.fromCurrentContextPath().build().toUriString();
                response.sendRedirect(issuer + "v2/logout?client_id=" + clientId + "&returnTo=" + baseUrl);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        };
    }

}


Solution

  • You are configuring a Spring OAuth2 client (with oauth2Login). You need to:

    P.S. n°1

    If you were configuring an oauth2ResourceServer (a REST API authorized using bearer tokens) with a JWT decoder, then you'd have to:

    P.S. n°2

    I maintain an additional Spring Boot starter that can, among many aother things: