-
Notifications
You must be signed in to change notification settings - Fork 2
Configuration β Authentication and Authorization
This wiki page explains the authentication and authorization used in the backend.
The flow of a request goes through the following steps:
- A request is sent from the front-end/client.
- The request arrives at the microservice.
- The
SecurityConfig
checks whether the route accessed requires authentication or not:- If no authentication is required, the request is forwarded to the endpoint and executed.
- If authentication is required, the token is verified and converted to extract authentication information.
- The endpoint receives the request and processes it with the required authorization.
The JWT Token used for authentication and authorization contains a payload similar to the following:
{
"exp": 1736870566,
"iat": 1736870266,
"jti": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"iss": "https://keycloak-dev.sportahub.app/realms/spring-microservices-security-realm",
"aud": "xxxxxxxx",
"sub": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"typ": "Bearer",
"azp": "spring-boot-client",
"sid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"acr": "x",
"allowed-origins": ["/*"],
"realm_access": {
"roles": [
"offline_access",
"default-roles-spring-microservices-security-realm",
"uma_authorization",
"USER"
]
},
"resource_access": {
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"scope": "email profile",
"email_verified": true,
"user_id": "xxx",
"name": "Alex Smith",
"preferred_username": "alexsmith",
"given_name": "Alex",
"family_name": "Smith",
"email": "alex.smith@example.com"
}
-
realm_access.roles
: Contains the roles assigned to the user. Key roles include:-
USER
: General role assigned to all users upon registration. -
ADMIN
: Role created for admin users. Admins can access all endpoints and information. For example, while regular users can only edit their events or friend requests, admins can edit any user's data. This role must be manually assigned in the Keycloak console.
-
-
sub
: The unique ID assigned to a user by Keycloak. -
user_id
: A custom attribute in the user's Keycloak profile, linked to the user ID within the application. It is generated upon saving the user to MongoDB. This field is crucial for authorization. For example:
@PreAuthorize("#id == authentication.name")
-
email
: The email address of the user. -
preferred_username
: The username of the user. -
name
: The full name of the user sending the request. ensures that the requesting user is the owner of the resource.
Each microservice has its own SecurityConfig
file. This file defines which endpoints require authentication and handles the process of converting the JWT token into claims.
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
return KeycloakJwtAuthenticationConverter.jwtAuthenticationConverter();
}
@SneakyThrows
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) {
return http
.authorizeHttpRequests(authorizeRequests -> authorizeRequests
.requestMatchers(
"/auth/register",
"/auth/login",
"/auth/refresh",
"/auth/reset-password",
"/swagger-ui.html",
"/api-docs",
"/api-docs/**",
"/swagger-ui/**",
"/webjars/**")
.permitAll()
.anyRequest().authenticated())
.csrf(AbstractHttpConfigurer::disable)
.oauth2Login(Customizer.withDefaults())
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(
jwtAuthenticationConverter())))
.build();
}
}
-
requestMatcher
: Specifies endpoints that do not require authentication. -
oauth2ResourceeServer
: Configures the custom token converter to extract claims from the token. -
@EnableMethodSecurity(prePostEnable = true)
: Enables method-level security in controllers and services.
The following class is used to configure the authentication converter:
public class KeycloakJwtAuthenticationConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
@Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
Object realmAccess = jwt.getClaims().get("realm_access");
if (realmAccess instanceof Map) {
Map<String, Object> realmAccessMap = (Map<String, Object>) realmAccess;
Object roles = realmAccessMap.get("roles");
if (roles instanceof List) {
List<String> rolesList = (List<String>) roles;
return rolesList.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase()))
.collect(Collectors.toList());
}
}
return List.of();
}
public static JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(new KeycloakJwtAuthenticationConverter());
converter.setPrincipalClaimName("user_id");
return converter;
}
@Bean
public static JwtAuthenticationConverter jwtAuthenticationConverter(
Converter<Map<String, Object>, Collection<GrantedAuthority>> authoritiesConverter) {
var authenticationConverter = new JwtAuthenticationConverter();
authenticationConverter.setJwtGrantedAuthoritiesConverter(jwt -> {
return authoritiesConverter.convert(jwt.getClaims());
});
return authenticationConverter;
}
}
This class extracts the roles from realm_access.roles
and sets the user_id
as the principal claim.
This project uses method-level security to ensure only permitted users can access specific endpoints.
@PatchMapping("/{id}/profile")
@PreAuthorize("#id == authentication.name || hasRole('ROLE_ADMIN')")
@ResponseStatus(HttpStatus.OK)
public ProfileResponse patchProfile(@PathVariable String id, @Valid @RequestBody ProfileRequest profileRequest) {
return userService.patchUserProfile(id, profileRequest);
}
In this example, the @PreAuthorize
annotation ensures that:
- The
user_id
accessed fromauthentication.name
from the token matches theid
of the user being updated. - Alternatively, the user has the ADMIN role.
Β© 2024 Sporta Team. All rights reserved.