-
Notifications
You must be signed in to change notification settings - Fork 12
feat: added get userdata from jwt helper #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+391
−0
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
176 changes: 176 additions & 0 deletions
176
src/main/java/com/ase/userservice/security/UserInformationJWT.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<String> getRoles() { | ||
| Jwt jwt = getCurrentJwt(); | ||
| if (jwt == null) { | ||
| return List.of(); | ||
| } | ||
|
|
||
| List<String> allRoles = new ArrayList<>(); | ||
|
|
||
| // combine all group fields | ||
| // groups | ||
| List<String> groups = jwt.getClaimAsStringList("groups"); | ||
| if (groups != null) { | ||
| allRoles.addAll(groups); | ||
| } | ||
|
|
||
| // realm_access.roles | ||
| try { | ||
| Map<String, Object> realmAccess = jwt.getClaim("realm_access"); | ||
| if (realmAccess != null && realmAccess.get("roles") instanceof List) { | ||
| @SuppressWarnings("unchecked") | ||
| List<String> realmRoles = (List<String>) realmAccess.get("roles"); | ||
| if (realmRoles != null) { | ||
| allRoles.addAll(realmRoles); | ||
| } | ||
| } | ||
| } catch (Exception e) { | ||
| // Ignore parsing errors | ||
| } | ||
|
|
||
| // resource_access.account.roles | ||
| try { | ||
| Map<String, Object> resourceAccess = jwt.getClaim("resource_access"); | ||
| if (resourceAccess != null && resourceAccess.get("account") instanceof Map) { | ||
| @SuppressWarnings("unchecked") | ||
| Map<String, Object> accountAccess = (Map<String, Object>) resourceAccess.get("account"); | ||
| if (accountAccess != null && accountAccess.get("roles") instanceof List) { | ||
| @SuppressWarnings("unchecked") | ||
| List<String> accountRoles = (List<String>) accountAccess.get("roles"); | ||
| if (accountRoles != null) { | ||
| allRoles.addAll(accountRoles); | ||
| } | ||
| } | ||
| } | ||
| } catch (Exception e) { | ||
Julius257 marked this conversation as resolved.
Show resolved
Hide resolved
Julius257 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // 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<String> 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; | ||
| } | ||
| } | ||
215 changes: 215 additions & 0 deletions
215
src/test/java/com/ase/userservice/security/UserInformationJWTTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<String, Object> 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<String, Object> 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<String, Object> accountAccess = new HashMap<>(); | ||
| accountAccess.put("roles", Arrays.asList( | ||
| "manage-account", | ||
| "manage-account-links", | ||
| "view-profile" | ||
| )); | ||
| Map<String, Object> resourceAccess = new HashMap<>(); | ||
| resourceAccess.put("account", accountAccess); | ||
| claims.put("resource_access", resourceAccess); | ||
|
|
||
| Map<String, Object> 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<String> 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<String> roles = UserInformationJWT.getRoles(); | ||
| assertTrue(roles.isEmpty()); | ||
| } | ||
|
|
||
| @Test | ||
| void testGetRolesNoDuplicates() { | ||
| List<String> 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"); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.