diff --git a/readme.md b/readme.md index 77986ed..a4a0970 100644 --- a/readme.md +++ b/readme.md @@ -39,8 +39,9 @@ To Be Played is split into three repositories: 10 minutes, `typ=public-profile`) that Core accepts for the public library data only. The profile has its own look (accent, corners, banner and background from own game art) and a **tile layout** (`profile_tiles`: the shown tiles in order, each with a size S, M or L), apart from the app appearance; the public answer sends both as - `style`. The ticket's claims come from the layout: `sections` names the shown parts (`STATS` when any `STATS_` - tile is shown), `profileStats` the stat groups of the `STATS_` tiles. The user's dashboard layout + `style`. The ticket also names the user's `role`, so Core can show the free looks of an admin. The ticket's + claims come from the layout: `sections` names the shown parts (`STATS` when any `STATS_` tile is shown), + `profileStats` the stat groups of the `STATS_` tiles. The user's dashboard layout (`dashboard_tiles`, NULL = never changed) is stored the same way. The ticket is not a login: every token names its kind in `aud` (`tbp-login`, `tbp-public-profile`, `tbp-internal`, `iss=tbp-auth`), and both services check it. A login's `sub` is the user id; the email is in no token. A token without `aud` is refused. The claims are pinned in diff --git a/src/main/java/auth_service/configuration/security/JwtService.java b/src/main/java/auth_service/configuration/security/JwtService.java index f394763..e6f01d6 100644 --- a/src/main/java/auth_service/configuration/security/JwtService.java +++ b/src/main/java/auth_service/configuration/security/JwtService.java @@ -85,13 +85,14 @@ public class JwtService implements TokenGenerator, ProfileTicketIssuer, ServiceT } @Override - public String generateProfileTicket(long userId, List profileStats, List sections) { + public String generateProfileTicket(long userId, Role role, List profileStats, List sections) { return Jwts.builder() .header().keyId(jwtConfig.getKid()).and() .issuer(ISSUER) .audience().add(PROFILE_TICKET_AUDIENCE).and() .subject(PROFILE_TICKET_SUBJECT) .claim("profileUserId", String.valueOf(userId)) + .claim(ROLE_CLAIM, role.name()) .claim("profileStats", profileStats) .claim("sections", sections) .issuedAt(new Date(System.currentTimeMillis())) diff --git a/src/main/java/auth_service/modules/profile/model/PublicProfile.java b/src/main/java/auth_service/modules/profile/model/PublicProfile.java index 3151fe6..ed24f66 100644 --- a/src/main/java/auth_service/modules/profile/model/PublicProfile.java +++ b/src/main/java/auth_service/modules/profile/model/PublicProfile.java @@ -1,9 +1,11 @@ package auth_service.modules.profile.model; +import auth_service.shared.security.Role; + import java.time.LocalDate; import java.util.List; -public record PublicProfile(long userId, String displayName, LocalDate memberSince, Style style, List tiles) { +public record PublicProfile(long userId, Role role, String displayName, LocalDate memberSince, Style style, List tiles) { public record Style(String accent, String corners, Long bannerAppid, Long backgroundAppid) {} diff --git a/src/main/java/auth_service/modules/profile/repo/PublicProfileRepository.java b/src/main/java/auth_service/modules/profile/repo/PublicProfileRepository.java index 8ff36b5..5890951 100644 --- a/src/main/java/auth_service/modules/profile/repo/PublicProfileRepository.java +++ b/src/main/java/auth_service/modules/profile/repo/PublicProfileRepository.java @@ -2,6 +2,7 @@ package auth_service.modules.profile.repo; import auth_service.modules.profile.model.PublicProfile; import auth_service.shared.account.ActiveUser; +import auth_service.shared.security.Role; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; @@ -30,12 +31,12 @@ public class PublicProfileRepository { */ public Optional findPublicByDisplayName(String displayName) { return jdbc.query(""" - SELECT u.id, u.display_name, u.registration_date, s.profile_accent, s.profile_corners, + SELECT u.id, u.role, u.display_name, u.registration_date, s.profile_accent, s.profile_corners, s.profile_banner_appid, s.profile_background_appid, s.profile_tiles FROM users u JOIN user_settings s ON s.user_id = u.id WHERE LOWER(u.display_name) = LOWER(?) AND %s AND s.public_profile """.formatted(ActiveUser.of("u")), - (rs, row) -> new PublicProfile(rs.getLong("id"), rs.getString("display_name"), + (rs, row) -> new PublicProfile(rs.getLong("id"), Role.parse(rs.getString("role")), rs.getString("display_name"), rs.getTimestamp("registration_date") != null ? rs.getTimestamp("registration_date").toLocalDateTime().toLocalDate() : null, new PublicProfile.Style(rs.getString("profile_accent"), rs.getString("profile_corners"), diff --git a/src/main/java/auth_service/modules/profile/service/PublicProfileService.java b/src/main/java/auth_service/modules/profile/service/PublicProfileService.java index e96b862..2889add 100644 --- a/src/main/java/auth_service/modules/profile/service/PublicProfileService.java +++ b/src/main/java/auth_service/modules/profile/service/PublicProfileService.java @@ -42,6 +42,6 @@ public class PublicProfileService { if (!statGroups.isEmpty()) { sections.add("STATS"); } - return profileTicketIssuer.generateProfileTicket(profile.userId(), statGroups, sections); + return profileTicketIssuer.generateProfileTicket(profile.userId(), profile.role(), statGroups, sections); } } diff --git a/src/main/java/auth_service/shared/security/ProfileTicketIssuer.java b/src/main/java/auth_service/shared/security/ProfileTicketIssuer.java index c5aabb4..1eb6322 100644 --- a/src/main/java/auth_service/shared/security/ProfileTicketIssuer.java +++ b/src/main/java/auth_service/shared/security/ProfileTicketIssuer.java @@ -11,5 +11,5 @@ public interface ProfileTicketIssuer { * @param profileStats the stat groups of Core the profile shows * @param sections the parts of the profile Core sends data for */ - String generateProfileTicket(long userId, List profileStats, List sections); + String generateProfileTicket(long userId, Role role, List profileStats, List sections); } diff --git a/src/test/java/auth_service/configuration/security/JwtServiceTest.java b/src/test/java/auth_service/configuration/security/JwtServiceTest.java index 223719f..62928c6 100644 --- a/src/test/java/auth_service/configuration/security/JwtServiceTest.java +++ b/src/test/java/auth_service/configuration/security/JwtServiceTest.java @@ -113,7 +113,7 @@ class JwtServiceTest extends AbstractUnitTest { @Test void generateProfileTicket_NamesTheUserAndExpiresSoon() { - var ticket = jwtService.generateProfileTicket(42L, List.of(), List.of()); + var ticket = jwtService.generateProfileTicket(42L, Role.USER, List.of(), List.of()); var claims = Jwts.parser().verifyWith(publicKey).build().parseSignedClaims(ticket).getPayload(); assertEquals(List.of("tbp-public-profile"), List.copyOf(claims.getAudience())); @@ -123,9 +123,17 @@ class JwtServiceTest extends AbstractUnitTest { assertTrue(secondsLeft > 0 && secondsLeft <= 600, "expires in " + secondsLeft + "s"); } + @Test + void generateProfileTicket_CarriesTheRole() { + String ticket = jwtService.generateProfileTicket(42L, Role.PLATFORM_ADMIN, List.of(), List.of()); + + Claims claims = Jwts.parser().verifyWith(publicKey).build().parseSignedClaims(ticket).getPayload(); + assertEquals("PLATFORM_ADMIN", claims.get("role", String.class)); + } + @Test void generateProfileTicket_CarriesTheSharedStatGroups() { - String ticket = jwtService.generateProfileTicket(42L, List.of("PILE", "TREND"), List.of("PLAYING", "STATS")); + String ticket = jwtService.generateProfileTicket(42L, Role.USER, List.of("PILE", "TREND"), List.of("PLAYING", "STATS")); Claims claims = Jwts.parser().verifyWith(publicKey).build().parseSignedClaims(ticket).getPayload(); assertEquals(List.of("PILE", "TREND"), claims.get("profileStats", List.class)); @@ -134,7 +142,7 @@ class JwtServiceTest extends AbstractUnitTest { @Test void parseLoginToken_ProfileTicketIsNoLoginToken() { - assertEquals(Optional.empty(), jwtService.parseLoginToken(jwtService.generateProfileTicket(42L, List.of(), List.of()))); + assertEquals(Optional.empty(), jwtService.parseLoginToken(jwtService.generateProfileTicket(42L, Role.USER, List.of(), List.of()))); } @Test @@ -218,7 +226,7 @@ class JwtServiceTest extends AbstractUnitTest { assertEquals(ContractFixture.token("login"), payload(jwtService.generateToken("42", 76561198000000001L, Role.USER))); assertEquals(ContractFixture.token("publicProfile"), - payload(jwtService.generateProfileTicket(42L, List.of("OVERVIEW"), List.of("SUMMARY")))); + payload(jwtService.generateProfileTicket(42L, Role.USER, List.of("OVERVIEW"), List.of("SUMMARY")))); assertEquals(ContractFixture.token("service"), payload(jwtService.generateServiceToken())); } diff --git a/src/test/java/auth_service/modules/profile/api/PublicProfileControllerTest.java b/src/test/java/auth_service/modules/profile/api/PublicProfileControllerTest.java index 15c0ecf..18ba74a 100644 --- a/src/test/java/auth_service/modules/profile/api/PublicProfileControllerTest.java +++ b/src/test/java/auth_service/modules/profile/api/PublicProfileControllerTest.java @@ -68,6 +68,7 @@ class PublicProfileControllerTest extends AbstractIntegrationTest { var claims = Jwts.parser().verifyWith(key).build().parseSignedClaims(ticket).getPayload(); assertEquals(String.valueOf(publicUserId), claims.get("profileUserId", String.class)); assertEquals(List.of(), claims.get("profileStats", List.class)); + assertEquals("USER", claims.get("role", String.class)); } @Test @@ -120,6 +121,7 @@ class PublicProfileControllerTest extends AbstractIntegrationTest { Claims claims = Jwts.parser().verifyWith(key).build().parseSignedClaims(ticket).getPayload(); assertEquals(List.of("PLAYING"), claims.get("sections", List.class)); assertEquals(List.of(), claims.get("profileStats", List.class)); + assertEquals("USER", claims.get("role", String.class)); } @Test diff --git a/src/test/resources/contract/auth-core-contract.json b/src/test/resources/contract/auth-core-contract.json index 4cffd65..a296e79 100644 --- a/src/test/resources/contract/auth-core-contract.json +++ b/src/test/resources/contract/auth-core-contract.json @@ -13,6 +13,7 @@ "aud": ["tbp-public-profile"], "sub": "public-profile", "profileUserId": "42", + "role": "USER", "profileStats": ["OVERVIEW"], "sections": ["SUMMARY"] },