Skip to content

Commit 05f9dd4

Browse files
ihraskoclaude
authored andcommitted
Make user and role claim names configurable
Add user-claim and role-claim properties to the org.opendaylight.aaa.shiro.bearerjwtrealm configuration PID so that operators can adapt the realm to identity providers that use different JWT claim names (e.g. sub/groups instead of preferred_username/roles). JIRA: AAA-299 Change-Id: Iccc9e481cfc852d10551747aa7721e6e39845cad Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Ivan Hrasko <ivan.hrasko@pantheon.tech> Signed-off-by: Samuel Schneider <samuel.schneider@pantheon.tech>
1 parent 233a20b commit 05f9dd4

5 files changed

Lines changed: 92 additions & 19 deletions

File tree

aaa-shiro/impl/src/main/java/org/opendaylight/aaa/shiro/realm/BearerJwtRealm.java

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.apache.shiro.authz.SimpleAuthorizationInfo;
2727
import org.apache.shiro.realm.AuthorizingRealm;
2828
import org.apache.shiro.subject.PrincipalCollection;
29+
import org.eclipse.jdt.annotation.NonNull;
2930
import org.eclipse.jdt.annotation.Nullable;
3031
import org.opendaylight.aaa.api.shiro.principal.ODLPrincipal;
3132
import org.opendaylight.aaa.shiro.principal.ODLPrincipalImpl;
@@ -49,16 +50,21 @@
4950
*/
5051
public final class BearerJwtRealm extends AuthorizingRealm {
5152
private static final Logger LOG = LoggerFactory.getLogger(BearerJwtRealm.class);
52-
// TODO make this configurable for different identity providers
53-
private static final String USER_CLAIM = "preferred_username";
54-
private static final String ROLE_CLAIM = "groups";
55-
private static final ThreadLocal<JWTProcessor<SecurityContext>> PROCESSOR_TL = new ThreadLocal<>();
53+
private static final ThreadLocal<BearerJwtRealmConfig> CONFIG_TL = new ThreadLocal<>();
54+
55+
static final String DEFAULT_USER_CLAIM = "preferred_username";
56+
static final String DEFAULT_ROLE_CLAIM = "groups";
5657

5758
private final @Nullable JWTProcessor<SecurityContext> jwtProcessor;
59+
private final @NonNull String userClaim;
60+
private final @NonNull String roleClaim;
5861

5962
public BearerJwtRealm() {
6063
setAuthenticationTokenClass(BearerToken.class);
61-
this.jwtProcessor = PROCESSOR_TL.get();
64+
final var config = CONFIG_TL.get();
65+
jwtProcessor = config != null ? config.jwtProcessor() : null;
66+
userClaim = config != null ? config.userClaim() : DEFAULT_USER_CLAIM;
67+
roleClaim = config != null ? config.roleClaim() : DEFAULT_ROLE_CLAIM;
6268
if (jwtProcessor == null) {
6369
LOG.warn("No JWT verification configured — BearerJwtRealm accepting unverified Bearer tokens");
6470
}
@@ -74,8 +80,8 @@ public BearerJwtRealm() {
7480
* @return a {@link Registration} that cleans up the thread-local when closed
7581
*/
7682
public static Registration prepareForLoad(final BearerJwtRealmConfig config) {
77-
PROCESSOR_TL.set(config != null ? config.jwtProcessor() : null);
78-
return PROCESSOR_TL::remove;
83+
CONFIG_TL.set(config);
84+
return CONFIG_TL::remove;
7985
}
8086

8187
@Override
@@ -89,7 +95,7 @@ protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken t
8995

9096
final String username;
9197
try {
92-
username = claims.getStringClaim(USER_CLAIM);
98+
username = claims.getStringClaim(userClaim);
9399
} catch (ParseException e) {
94100
throw new AuthenticationException("Invalid JWT user claim data", e);
95101
}
@@ -99,9 +105,9 @@ protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken t
99105

100106
final Set<String> roles;
101107
try {
102-
roles = parseRoles(claims);
108+
roles = parseRoles(claims, roleClaim);
103109
} catch (ParseException e) {
104-
throw new AuthenticationException("Invalid JWT groups claim data", e);
110+
throw new AuthenticationException("Invalid JWT role claim data", e);
105111
}
106112

107113
final var odlPrincipal = ODLPrincipalImpl.createODLPrincipal(username, null, username, roles);
@@ -138,8 +144,8 @@ private JWTClaimsSet parseClaims(final String token) {
138144
}
139145
}
140146

141-
private static Set<String> parseRoles(final JWTClaimsSet claims) throws ParseException {
142-
final var groups = claims.getStringListClaim(ROLE_CLAIM);
147+
private static Set<String> parseRoles(final JWTClaimsSet claims, final String roleClaim) throws ParseException {
148+
final var groups = claims.getStringListClaim(roleClaim);
143149
if (groups == null) {
144150
LOG.warn("JWT has no roles claim; granting no roles");
145151
return Set.of();

aaa-shiro/impl/src/main/java/org/opendaylight/aaa/shiro/realm/BearerJwtRealmConfig.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import com.nimbusds.jose.proc.SecurityContext;
1111
import com.nimbusds.jwt.proc.JWTProcessor;
12+
import org.eclipse.jdt.annotation.NonNull;
1213
import org.eclipse.jdt.annotation.Nullable;
1314

1415
/**
@@ -20,4 +21,14 @@ public interface BearerJwtRealmConfig {
2021
* {@return JWTProcessor if configured}
2122
*/
2223
@Nullable JWTProcessor<SecurityContext> jwtProcessor();
24+
25+
/**
26+
* {@return configured JWT claim name used to extract the username}
27+
*/
28+
@NonNull String userClaim();
29+
30+
/**
31+
* {@return configured claim name used to extract the list of roles}
32+
*/
33+
@NonNull String roleClaim();
2334
}

aaa-shiro/impl/src/main/java/org/opendaylight/aaa/shiro/realm/BearerJwtRealmConfigImpl.java

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
*/
88
package org.opendaylight.aaa.shiro.realm;
99

10+
import static java.util.Objects.requireNonNull;
11+
1012
import com.google.common.annotations.VisibleForTesting;
1113
import com.nimbusds.jose.JWSAlgorithm;
1214
import com.nimbusds.jose.jwk.source.JWKSource;
@@ -23,6 +25,7 @@
2325
import java.util.Arrays;
2426
import java.util.Set;
2527
import java.util.stream.Collectors;
28+
import org.eclipse.jdt.annotation.NonNull;
2629
import org.eclipse.jdt.annotation.Nullable;
2730
import org.osgi.service.component.annotations.Activate;
2831
import org.osgi.service.component.annotations.Component;
@@ -41,10 +44,12 @@
4144
*
4245
* <p>Configuration is supplied via {@code etc/org.opendaylight.aaa.shiro.bearerjwtrealm.cfg}:
4346
* <pre>{@code
44-
* jwks-uri=http(s)://keycloak.local:8080/realms/odl-realm/protocol/openid-connect/certs
45-
* expected-issuer=http(s)://keycloak.local:8080/realms/odl-realm
46-
* expected-audience=odl-application
47-
* allowed-algorithms=RS256
47+
* jwks-uri="http(s)://keycloak.local:8080/realms/odl-realm/protocol/openid-connect/certs"
48+
* expected-issuer="http(s)://keycloak.local:8080/realms/odl-realm"
49+
* expected-audience="odl-application"
50+
* allowed-algorithms=["RS256", "RS384", "RS512", "ES256"]
51+
* user-claim="preferred_username"
52+
* role-claim="groups"
4853
* }</pre>
4954
*/
5055
@Component(service = BearerJwtRealmConfig.class, configurationPid = "org.opendaylight.aaa.shiro.bearerjwtrealm")
@@ -73,16 +78,29 @@ Expected value(s) of the 'aud' JWT claim (comma-separated).
7378
@AttributeDefinition(description = """
7479
Allowed JWS signing algorithms (comma-separated, e.g. RS256, RS384, RS512, ES256).""")
7580
String[] allowed$_$algorithms() default {"RS256", "RS384", "RS512", "ES256"};
81+
82+
@AttributeDefinition(description = """
83+
JWT claim name used to extract the username.""")
84+
String user$_$claim() default BearerJwtRealm.DEFAULT_USER_CLAIM;
85+
86+
@AttributeDefinition(description = """
87+
JWT claim name used to extract the list of roles.""")
88+
String role$_$claim() default BearerJwtRealm.DEFAULT_ROLE_CLAIM;
7689
}
7790

7891
private final @Nullable JWTProcessor<SecurityContext> jwtProcessor;
92+
private final @NonNull String userClaim;
93+
private final @NonNull String roleClaim;
7994

8095
/**
8196
* Package-private constructor for use in unit tests, accepting a pre-built processor.
8297
*/
8398
@VisibleForTesting
84-
BearerJwtRealmConfigImpl(final @Nullable JWTProcessor<SecurityContext> jwtProcessor) {
99+
BearerJwtRealmConfigImpl(final @Nullable JWTProcessor<SecurityContext> jwtProcessor,
100+
final @NonNull String userClaim, final @NonNull String roleClaim) {
85101
this.jwtProcessor = jwtProcessor;
102+
this.userClaim = requireNonNull(userClaim);
103+
this.roleClaim = requireNonNull(roleClaim);
86104
}
87105

88106
/**
@@ -94,6 +112,8 @@ Allowed JWS signing algorithms (comma-separated, e.g. RS256, RS384, RS512, ES256
94112
*/
95113
@Activate
96114
public BearerJwtRealmConfigImpl(final Configuration configuration) {
115+
userClaim = configuration.user$_$claim();
116+
roleClaim = configuration.role$_$claim();
97117
if (configuration.jwks$_$uri().isBlank()) {
98118
jwtProcessor = null;
99119
return;
@@ -144,7 +164,17 @@ private static DefaultJWTClaimsVerifier<SecurityContext> verifier(
144164
}
145165

146166
@Override
147-
public @Nullable JWTProcessor<SecurityContext> jwtProcessor() {
167+
public JWTProcessor<SecurityContext> jwtProcessor() {
148168
return jwtProcessor;
149169
}
170+
171+
@Override
172+
public String userClaim() {
173+
return userClaim;
174+
}
175+
176+
@Override
177+
public String roleClaim() {
178+
return roleClaim;
179+
}
150180
}

aaa-shiro/impl/src/main/resources/org.opendaylight.aaa.shiro.bearerjwtrealm.cfg

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,9 @@ expected-audience=""
1313

1414
# Allowed JWS signing algorithms (comma-separated, e.g. RS256, RS384, RS512, ES256).
1515
allowed-algorithms=["RS256", "RS384", "RS512", "ES256"]
16+
17+
# JWT claim name used to extract the username.
18+
user-claim="preferred_username"
19+
20+
# JWT claim name used to extract the list of roles.
21+
role-claim="groups"

aaa-shiro/impl/src/test/java/org/opendaylight/aaa/shiro/realm/BearerJwtRealmTest.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,26 @@ void testAuthorizationUnknownPrincipal() {
176176
assertNull(info.getRoles());
177177
}
178178

179+
/**
180+
* Tests that custom user and role claim names are honored when configured.
181+
*/
182+
@Test
183+
void testCustomClaimNames() {
184+
final var config = new BearerJwtRealmConfigImpl(null, "sub", "auth_roles");
185+
try (var ignored = BearerJwtRealm.prepareForLoad(config)) {
186+
final var customRealm = new BearerJwtRealm();
187+
final var jwt = buildPlainJwt(new JWTClaimsSet.Builder()
188+
.claim("sub", "custom-user")
189+
.claim("auth_roles", List.of("admin", "viewer"))
190+
.build());
191+
final var info = customRealm.doGetAuthenticationInfo(new BearerToken(jwt));
192+
assertNotNull(info);
193+
final var principal = assertInstanceOf(ODLPrincipal.class, info.getPrincipals().getPrimaryPrincipal());
194+
assertEquals("custom-user", principal.getUsername());
195+
assertEquals(Set.of("admin", "viewer"), principal.getRoles());
196+
}
197+
}
198+
179199
/**
180200
* Tests that a correctly signed JWT with valid claims passes verification.
181201
*/
@@ -417,7 +437,7 @@ private static BearerJwtRealmConfigImpl buildConfig(final RSAKey rsaKey, final S
417437
final var processor = new DefaultJWTProcessor<>();
418438
processor.setJWSKeySelector(keySelector);
419439
processor.setJWTClaimsSetVerifier(claimsVerifier);
420-
return new BearerJwtRealmConfigImpl(processor);
440+
return new BearerJwtRealmConfigImpl(processor, USER_CLAIM, ROLE_CLAIM);
421441
}
422442

423443
private static Date futureDate() {

0 commit comments

Comments
 (0)