Skip to content

Configuration ‐ Authentication and Authorization

patrickmac3 edited this page Jan 15, 2025 · 1 revision

Overview

This wiki page explains the authentication and authorization used in the backend.

Authentication Flow

The flow of a request goes through the following steps:

  1. A request is sent from the front-end/client.
  2. The request arrives at the microservice.
  3. 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.
  4. The endpoint receives the request and processes it with the required authorization.

JWT Token Structure

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"
}

Important JWT Token fields

  1. 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.
  2. sub: The unique ID assigned to a user by Keycloak.
  3. 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")
  1. email: The email address of the user.
  2. preferred_username: The username of the user.
  3. name: The full name of the user sending the request. ensures that the requesting user is the owner of the resource.

Security Configuration

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.

Example SecurityConfig file

@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();
    }
}

Key Components

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

JWT Authentication Converter

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.

Authorization Flow

This project uses method-level security to ensure only permitted users can access specific endpoints.

Example: UserController

@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:

  1. The user_id accessed from authentication.name from the token matches the id of the user being updated.
  2. Alternatively, the user has the ADMIN role.

Extra resources