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>
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.