Skip to content

Commit dc85baf

Browse files
gpascucciclaude
andcommitted
fix(security): audit user = custom:idp_username, not the sub UUID (PR #318 #4)
authentication.getName() drives the ENTRY_/UPDATE_USERID VARCHAR2(30) audit columns on every admin write. The converter returned the token with no name, so getName() defaulted to the Cognito sub (~36-char UUID), which overflows the column (ORA-12899) in production. Set the principal name to custom:idp_username (sized for it per the FAM identity contract), falling back to the sub only if the claim is absent. App-wide fix (also code-tables / 24.1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a5c3d9f commit dc85baf

2 files changed

Lines changed: 33 additions & 1 deletion

File tree

backend/src/main/java/ca/bc/gov/nrs/ilcr/security/CognitoGroupsJwtAuthenticationConverter.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,19 @@ public AbstractAuthenticationToken convert(Jwt jwt) {
3737
authorities.add(new SimpleGrantedAuthority(role.name()));
3838
}
3939
}
40-
return new JwtAuthenticationToken(jwt, authorities);
40+
return new JwtAuthenticationToken(jwt, authorities, auditUsername(jwt));
41+
}
42+
43+
/**
44+
* The principal name — i.e. {@code authentication.getName()}, which every admin write stamps into
45+
* the {@code ENTRY_/UPDATE_USERID VARCHAR2(30)} audit columns. FAM's {@code custom:idp_username} is
46+
* sized for that (per the identity contract); without this the token name defaults to the {@code sub}
47+
* (a ~36-char Cognito UUID), which overflows the column (ORA-12899) on every write in production.
48+
* Falls back to the {@code sub} only when the claim is absent (defensive; never expected with FAM).
49+
*/
50+
private static String auditUsername(Jwt jwt) {
51+
String username = jwt.getClaimAsString("custom:idp_username");
52+
return (username != null && !username.isBlank()) ? username : jwt.getSubject();
4153
}
4254

4355
private static List<String> extractGroups(Jwt jwt) {

backend/src/test/java/ca/bc/gov/nrs/ilcr/security/CognitoGroupsJwtAuthenticationConverterTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,24 @@ void bothGroups_mapBothRoles() {
8181
Set.of("SUBMITTER", "ADMIN"),
8282
authorities(converter.convert(jwtWithGroups(List.of("ILCR_SUBMITTER", "ILCR_ADMIN")))));
8383
}
84+
85+
@Test
86+
void principalName_isIdpUsername_notTheSubUuid() {
87+
// The audit user (authentication.getName()) must be custom:idp_username, which fits
88+
// UPDATE_USERID VARCHAR2(30) — not the ~36-char sub UUID that would overflow it.
89+
Jwt jwt = Jwt.withTokenValue("token")
90+
.header("alg", "none")
91+
.issuedAt(Instant.now())
92+
.expiresAt(Instant.now().plusSeconds(60))
93+
.subject("11111111-2222-3333-4444-555555555555")
94+
.claim("cognito:groups", List.of("ILCR_ADMIN"))
95+
.claim("custom:idp_username", "GPASCUCCI")
96+
.build();
97+
assertEquals("GPASCUCCI", converter.convert(jwt).getName());
98+
}
99+
100+
@Test
101+
void principalName_fallsBackToSubjectWhenNoIdpUsername() {
102+
assertEquals("user", converter.convert(jwtWithGroups(List.of("ILCR_ADMIN"))).getName());
103+
}
84104
}

0 commit comments

Comments
 (0)