Search code examples
javaspring-bootspring-securityoauth-2.0

Spring Boot Oauth login based authorization


I'm creating a Spring boot application using oauth, right now I have the github login working, but i want to have a role based application, so some routes can only be accessed by a role_admin while other can be accessed by role_user, I've been trying some stuff and my code look a bit messy, so I appreciate any help! Right now in the CustomOAuth2User class in the getAuthorities method, it shows the Set roles = getUserRoles(); as empty, so it defaults all users role to role_user, so I can access routes that need such role, but not admin ones, even tho I create the user with the role_admin.

SecurityConfig

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    
    @Autowired
    private CustomOAuth2UserService oauthUserService;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private UserService userService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
        .authorizeHttpRequests(auth -> {
            auth.requestMatchers(HttpMethod.GET, "/userinfo").hasRole("ADMIN");
            auth.requestMatchers(HttpMethod.GET, "/").hasRole("USER");
            auth.anyRequest().authenticated();
        })
         .oauth2Login(oauth2 -> oauth2
                        .userInfoEndpoint(userInfo -> userInfo.userService(oauthUserService)).successHandler(new AuthenticationSuccessHandler() {

                            @Override
                            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                                                Authentication authentication) throws IOException, ServletException {

                                CustomOAuth2User oauthUser = (CustomOAuth2User) authentication.getPrincipal();

                                String github_id = oauthUser.getAttributes().get("id").toString();

                                Optional<User> userExistOp = userRepository.findByIdentifier(github_id);

                                if(!userExistOp.isPresent()) {
                                    //all user are saves with role User
                                    UserDTO userDTO = new UserDTO(oauthUser.getName(),"username", oauthUser.getAttributes().get("id").toString(), oauthUser.getAttributes().get("avatar_url").toString(), Role.ADMIN);

                                    userService.create(userDTO);
                                }

                                response.sendRedirect("/");
                            }
                        })
                )
        .build();
    }
}

CustomOAuth2UserService

@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User user =  super.loadUser(userRequest);
        return new CustomOAuth2User(user);
    }
}

CustomOauth2User

public class CustomOAuth2User implements OAuth2User {

    private OAuth2User oauth2User;

    public CustomOAuth2User(OAuth2User oauth2User) {
        this.oauth2User = oauth2User;
    }

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

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Set<Role> roles = getUserRoles();

        System.out.println("User roles:");
        for (Role role : roles) {
            System.out.println(role.name());
        }

        if (roles != null && !roles.isEmpty()) {
            // Convert roles to GrantedAuthority objects
            return roles.stream()
                    .map(role -> new SimpleGrantedAuthority("ROLE_" + role.name()))
                    .collect(Collectors.toList());
        } else {
            // Provide a default role if user roles are not available
            return Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"));
        }
    }

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

    // Helper method to retrieve user roles from OAuth2User attributes
    private Set<Role> getUserRoles() {
        // Assuming roles are stored as an attribute with key "roles" in OAuth2User attributes
        Collection<String> roleStrings = (Collection<String>) oauth2User.getAttribute("roles");
        return roleStrings != null ?
                roleStrings.stream()
                        .map(Role::valueOf) // Convert role string to Role enum
                        .collect(Collectors.toSet()) :
                Collections.emptySet();
    }

    public String getEmail() {
        return oauth2User.<String>getAttribute("email");
    }
}

User model

@Entity
@Table(name="users")
@Data
@Setter
public class User implements UserDetails {

    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String username;

    private String identifier;

    private String avatar;

    private Role role;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (this.role == Role.ADMIN) {
            return List.of(new SimpleGrantedAuthority("ROLE_ADMIN"), new SimpleGrantedAuthority("ROLE_USER"));
        } 
        return List.of(new SimpleGrantedAuthority("ROLE_USER"));
    }

    @Override
    public String getPassword() {
        return null;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

AuthService

@Service
public class AuthService implements UserDetailsService{

    UserRepository userRepository;

    public AuthService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String identifier) throws UsernameNotFoundException {
        Optional<User> userOp = userRepository.findByIdentifier(identifier);
        if (!userOp.isPresent()) {
            throw new UsernameNotFoundException("User not found with GitHub user ID: " + identifier);
        }

        User user = userOp.get();
        // Construct UserDetails object with user roles
        return org.springframework.security.core.userdetails.User
                .withUsername(String.valueOf(user.getIdentifier())) // GitHub user ID
                .password(user.getPassword())
                .authorities(user.getAuthorities())
                .accountExpired(true)
                .accountLocked(true)
                .credentialsExpired(true)
                .disabled(true)
                .build();
    }
}

Pom.xml

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ecommerce</groupId>
    <artifactId>e-commerce</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>e-commerce</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-core</artifactId>
        </dependency>

        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>1.5.5.Final</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
             <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>1.5.5.Final</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>

Solution

  • What you should do is using an authorization server of your own (Keycloak? Auth0? Amazon cognito? There are plenty of options). This new authorization server will be in charge of keeping roles and adding it to tokens private claims.

    Then all you have to configure manually in Spring clients and resource servers is authorities mapping from the private claim(s) your authorization server puts roles into.