diff --git a/src/main/java/com/ase/userservice/security/UserInformationJWT.java b/src/main/java/com/ase/userservice/security/UserInformationJWT.java new file mode 100644 index 00000000..a2ff7fb7 --- /dev/null +++ b/src/main/java/com/ase/userservice/security/UserInformationJWT.java @@ -0,0 +1,176 @@ +package com.ase.userservice.security; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Helper class to extract user information from JWT tokens. + * Provides convenient static methods to access authenticated user data. + */ +public class UserInformationJWT { + + /** + * Get the current JWT token from the security context + * @return JWT token or null if not authenticated + */ + private static Jwt getCurrentJwt() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication instanceof JwtAuthenticationToken) { + return ((JwtAuthenticationToken) authentication).getToken(); + } + + return null; + } + + /** + * Get the user ID + * @return User ID or null if not available + */ + public static String getUserId() { + Jwt jwt = getCurrentJwt(); + return jwt != null ? jwt.getSubject() : null; + } + + /** + * Get the user's email address + * @return Email or null if not available + */ + public static String getEmail() { + Jwt jwt = getCurrentJwt(); + return jwt != null ? jwt.getClaimAsString("email") : null; + } + + /** + * Get the username + * @return Username or null if not available + */ + public static String getUsername() { + Jwt jwt = getCurrentJwt(); + return jwt != null ? jwt.getClaimAsString("preferred_username") : null; + } + + /** + * Get the user's first name + * @return First name or null if not available + */ + public static String getFirstName() { + Jwt jwt = getCurrentJwt(); + return jwt != null ? jwt.getClaimAsString("given_name") : null; + } + + /** + * Get the user's last name + * @return Last name or null if not available + */ + public static String getLastName() { + Jwt jwt = getCurrentJwt(); + return jwt != null ? jwt.getClaimAsString("family_name") : null; + } + + /** + * Get all roles/groups of the user from multiple sources. + * @return List of all unique roles or empty list if not available + */ + public static List getRoles() { + Jwt jwt = getCurrentJwt(); + if (jwt == null) { + return List.of(); + } + + List allRoles = new ArrayList<>(); + + // combine all group fields + // groups + List groups = jwt.getClaimAsStringList("groups"); + if (groups != null) { + allRoles.addAll(groups); + } + + // realm_access.roles + try { + Map realmAccess = jwt.getClaim("realm_access"); + if (realmAccess != null && realmAccess.get("roles") instanceof List) { + @SuppressWarnings("unchecked") + List realmRoles = (List) realmAccess.get("roles"); + if (realmRoles != null) { + allRoles.addAll(realmRoles); + } + } + } catch (Exception e) { + // Ignore parsing errors + } + + // resource_access.account.roles + try { + Map resourceAccess = jwt.getClaim("resource_access"); + if (resourceAccess != null && resourceAccess.get("account") instanceof Map) { + @SuppressWarnings("unchecked") + Map accountAccess = (Map) resourceAccess.get("account"); + if (accountAccess != null && accountAccess.get("roles") instanceof List) { + @SuppressWarnings("unchecked") + List accountRoles = (List) accountAccess.get("roles"); + if (accountRoles != null) { + allRoles.addAll(accountRoles); + } + } + } + } catch (Exception e) { + } + + // remove duplicates + return allRoles.stream().distinct().toList(); + } + + /** + * Check if the user has a specific role (case-insensitive). + * Searches in groups, realm_access.roles, and resource_access.account.roles + * + * @param role The role to check + * @return true if user has the role, false otherwise + */ + public static boolean hasRole(String role) { + if (role == null) { + return false; + } + + List roles = getRoles(); + return roles.stream() + .anyMatch(r -> r.equalsIgnoreCase(role)); + } + + + /** + * Get a custom claim from the JWT + * @param claimName Name of the claim + * @return Claim value or null if not available + */ + public static Object getClaim(String claimName) { + Jwt jwt = getCurrentJwt(); + return jwt != null ? jwt.getClaim(claimName) : null; + } + + /** + * Get a custom claim as String + * @param claimName Name of the claim + * @return Claim value as String or null if not available + */ + public static String getClaimAsString(String claimName) { + Jwt jwt = getCurrentJwt(); + return jwt != null ? jwt.getClaimAsString(claimName) : null; + } + + /** + * Check if a user is currently authenticated + * @return true if authenticated, false otherwise + */ + public static boolean isAuthenticated() { + return getCurrentJwt() != null; + } +} diff --git a/src/test/java/com/ase/userservice/security/UserInformationJWTTest.java b/src/test/java/com/ase/userservice/security/UserInformationJWTTest.java new file mode 100644 index 00000000..2cc13230 --- /dev/null +++ b/src/test/java/com/ase/userservice/security/UserInformationJWTTest.java @@ -0,0 +1,215 @@ +package com.ase.userservice.security; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +import java.time.Instant; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for UserInformationJWT helper class + */ +class UserInformationJWTTest { + + private Jwt testJwt; + + @BeforeEach + void setUp() { + // Create a mock JWT with test data matching the real Keycloak token structure + Map claims = new HashMap<>(); + claims.put("sub", "0b540a6e-988d-484a-9247-9e3a2f237438"); + claims.put("preferred_username", "david"); + claims.put("email", "dave@fave.com"); + claims.put("given_name", "david"); + claims.put("family_name", "daivd"); + claims.put("name", "david daivd"); + + // Groups claim + claims.put("groups", Arrays.asList( + "default-roles-sau", + "manage-users", + "offline_access", + "lecturer", + "uma_authorization" + )); + + // realm_access with roles + Map realmAccess = new HashMap<>(); + realmAccess.put("roles", Arrays.asList( + "default-roles-sau", + "manage-users", + "offline_access", + "lecturer", + "uma_authorization" + )); + claims.put("realm_access", realmAccess); + + // resource_access with account roles + Map accountAccess = new HashMap<>(); + accountAccess.put("roles", Arrays.asList( + "manage-account", + "manage-account-links", + "view-profile" + )); + Map resourceAccess = new HashMap<>(); + resourceAccess.put("account", accountAccess); + claims.put("resource_access", resourceAccess); + + Map headers = new HashMap<>(); + headers.put("alg", "RS256"); + + testJwt = new Jwt( + "test-token-value", + Instant.now(), + Instant.now().plusSeconds(3600), + headers, + claims + ); + + // Set the JWT in the security context + JwtAuthenticationToken auth = new JwtAuthenticationToken(testJwt); + SecurityContextHolder.getContext().setAuthentication(auth); + } + + @AfterEach + void tearDown() { + // Clear security context after each test + SecurityContextHolder.clearContext(); + } + + @Test + void testGetUserId() { + String userId = UserInformationJWT.getUserId(); + assertEquals("0b540a6e-988d-484a-9247-9e3a2f237438", userId); + } + + @Test + void testGetUsername() { + String username = UserInformationJWT.getUsername(); + assertEquals("david", username); + } + + @Test + void testGetEmail() { + String email = UserInformationJWT.getEmail(); + assertEquals("dave@fave.com", email); + } + + @Test + void testGetFirstName() { + String firstName = UserInformationJWT.getFirstName(); + assertEquals("david", firstName); + } + + @Test + void testGetLastName() { + String lastName = UserInformationJWT.getLastName(); + assertEquals("daivd", lastName); + } + + @Test + void testGetRoles() { + List roles = UserInformationJWT.getRoles(); + + // Should contain roles from groups + assertTrue(roles.contains("lecturer")); + assertTrue(roles.contains("manage-users")); + + // Should contain roles from resource_access.account.roles + assertTrue(roles.contains("manage-account")); + assertTrue(roles.contains("view-profile")); + + // Should have at least 8 unique roles (5 from groups + 3 from account) + assertTrue(roles.size() >= 8); + } + + @Test + void testHasRoleFromGroups() { + assertTrue(UserInformationJWT.hasRole("lecturer")); + assertTrue(UserInformationJWT.hasRole("manage-users")); + } + + @Test + void testHasRoleFromRealmAccess() { + assertTrue(UserInformationJWT.hasRole("default-roles-sau")); + assertTrue(UserInformationJWT.hasRole("offline_access")); + } + + @Test + void testHasRoleFromResourceAccess() { + assertTrue(UserInformationJWT.hasRole("manage-account")); + assertTrue(UserInformationJWT.hasRole("view-profile")); + assertTrue(UserInformationJWT.hasRole("manage-account-links")); + } + + @Test + void testHasRoleCaseInsensitive() { + assertTrue(UserInformationJWT.hasRole("LECTURER")); + assertTrue(UserInformationJWT.hasRole("Manage-Account")); + assertTrue(UserInformationJWT.hasRole("lecturer")); + } + + @Test + void testHasRoleNotExists() { + assertFalse(UserInformationJWT.hasRole("admin")); + assertFalse(UserInformationJWT.hasRole("superuser")); + } + + @Test + void testHasRoleWithNull() { + assertFalse(UserInformationJWT.hasRole(null)); + } + + @Test + void testGetClaim() { + Object email = UserInformationJWT.getClaim("email"); + assertEquals("dave@fave.com", email); + } + + @Test + void testGetClaimAsString() { + String email = UserInformationJWT.getClaimAsString("email"); + assertEquals("dave@fave.com", email); + } + + @Test + void testIsAuthenticated() { + assertTrue(UserInformationJWT.isAuthenticated()); + } + + @Test + void testIsAuthenticatedWithoutContext() { + SecurityContextHolder.clearContext(); + assertFalse(UserInformationJWT.isAuthenticated()); + } + + @Test + void testGetUserIdWithoutAuthentication() { + SecurityContextHolder.clearContext(); + assertNull(UserInformationJWT.getUserId()); + } + + @Test + void testGetRolesWithoutAuthentication() { + SecurityContextHolder.clearContext(); + List roles = UserInformationJWT.getRoles(); + assertTrue(roles.isEmpty()); + } + + @Test + void testGetRolesNoDuplicates() { + List roles = UserInformationJWT.getRoles(); + // Check that there are no duplicates + long uniqueCount = roles.stream().distinct().count(); + assertEquals(roles.size(), uniqueCount, "Roles list should not contain duplicates"); + } +}