1
1
package io .quarkus .oidc .runtime ;
2
2
3
+ import static java .util .Objects .requireNonNull ;
4
+
3
5
import java .io .Closeable ;
4
6
import java .nio .charset .StandardCharsets ;
5
7
import java .security .Key ;
8
10
import java .util .Base64 ;
9
11
import java .util .List ;
10
12
import java .util .Map ;
13
+ import java .util .Set ;
11
14
import java .util .function .BiFunction ;
12
15
import java .util .function .Function ;
13
16
17
+ import jakarta .json .JsonArray ;
14
18
import jakarta .json .JsonObject ;
15
19
16
20
import org .eclipse .microprofile .jwt .Claims ;
@@ -76,7 +80,7 @@ public class OidcProvider implements Closeable {
76
80
final TokenCustomizer tokenCustomizer ;
77
81
final String issuer ;
78
82
final String [] audience ;
79
- final Map <String , String > requiredClaims ;
83
+ final Map <String , Set < String > > requiredClaims ;
80
84
final Key tokenDecryptionKey ;
81
85
final AlgorithmConstraints requiredAlgorithmConstraints ;
82
86
@@ -160,8 +164,9 @@ private String[] checkAudienceProp() {
160
164
return audienceProp != null ? audienceProp .toArray (new String [] {}) : null ;
161
165
}
162
166
163
- private Map <String , String > checkRequiredClaimsProp () {
164
- return oidcConfig != null ? oidcConfig .token ().requiredClaims () : null ;
167
+ private Map <String , Set <String >> checkRequiredClaimsProp () {
168
+ return oidcConfig != null && !oidcConfig .token ().requiredClaims ().isEmpty () ? oidcConfig .token ().requiredClaims ()
169
+ : null ;
165
170
}
166
171
167
172
public TokenVerificationResult verifySelfSignedJwtToken (String token , Key generatedInternalSignatureKey )
@@ -216,7 +221,7 @@ private TokenVerificationResult verifyJwtTokenInternal(String token,
216
221
}
217
222
218
223
if (nonce != null ) {
219
- builder .registerValidator (new CustomClaimsValidator (Map .of (OidcConstants .NONCE , nonce )));
224
+ builder .registerValidator (new CustomClaimsValidator (Map .of (OidcConstants .NONCE , Set . of ( nonce ) )));
220
225
}
221
226
222
227
for (Validator customValidator : customValidators ) {
@@ -241,7 +246,7 @@ private TokenVerificationResult verifyJwtTokenInternal(String token,
241
246
} else {
242
247
builder .setSkipDefaultAudienceValidation ();
243
248
}
244
- if (requiredClaims != null && ! requiredClaims . isEmpty () ) {
249
+ if (requiredClaims != null ) {
245
250
builder .registerValidator (new CustomClaimsValidator (requiredClaims ));
246
251
}
247
252
@@ -387,22 +392,46 @@ public TokenIntrospection apply(TokenIntrospection introspectionResult, Throwabl
387
392
throw new AuthenticationFailedException (ex , tokenMap (token , idToken ));
388
393
}
389
394
390
- if (requiredClaims != null && !requiredClaims .isEmpty ()) {
391
- for (Map .Entry <String , String > requiredClaim : requiredClaims .entrySet ()) {
392
- String introspectionClaimValue = null ;
393
- try {
394
- introspectionClaimValue = introspectionResult .getString (requiredClaim .getKey ());
395
- } catch (ClassCastException ex ) {
396
- LOG .debugf ("Introspection claim %s is not String" , requiredClaim .getKey ());
395
+ if (requiredClaims != null ) {
396
+ for (Map .Entry <String , Set <String >> requiredClaim : requiredClaims .entrySet ()) {
397
+ final String requiredClaimName = requiredClaim .getKey ();
398
+ if (!introspectionResult .contains (requiredClaimName )) {
399
+ LOG .debugf ("Introspection claim %s is missing" , requiredClaimName );
397
400
throw new AuthenticationFailedException (tokenMap (token , idToken ));
398
401
}
399
- if (introspectionClaimValue == null ) {
400
- LOG .debugf ("Introspection claim %s is missing" , requiredClaim .getKey ());
402
+ final Set <String > requiredClaimValues = requiredClaim .getValue ();
403
+ if (requiredClaimValues .size () == 1 ) {
404
+ String introspectionClaimValue = null ;
405
+ try {
406
+ introspectionClaimValue = introspectionResult .getString (requiredClaimName );
407
+ } catch (ClassCastException ex ) {
408
+ LOG .debugf ("Introspection claim %s is not String" , requiredClaimName );
409
+ }
410
+ String requiredClaimValue = requiredClaimValues .iterator ().next ();
411
+ if (requiredClaimValue .equals (introspectionClaimValue )) {
412
+ continue ;
413
+ }
414
+ }
415
+ final JsonArray actualClaimValueArray ;
416
+ try {
417
+ actualClaimValueArray = requireNonNull (introspectionResult .getArray (requiredClaimName ));
418
+ } catch (Exception ignored ) {
419
+ LOG .debugf ("Introspection claim %s is neither string or array" , requiredClaimName );
401
420
throw new AuthenticationFailedException (tokenMap (token , idToken ));
402
421
}
403
- if (!introspectionClaimValue .equals (requiredClaim .getValue ())) {
422
+ requiredClaimValuesLoop : for (String requiredClaimValue : requiredClaimValues ) {
423
+ for (int i = 0 ; i < actualClaimValueArray .size (); i ++) {
424
+ try {
425
+ String actualClaimValue = actualClaimValueArray .getString (i );
426
+ if (requiredClaimValue .equals (actualClaimValue )) {
427
+ continue requiredClaimValuesLoop ;
428
+ }
429
+ } catch (Exception ignored ) {
430
+ // try next actual claim value
431
+ }
432
+ }
404
433
LOG .debugf ("Value of the introspection claim %s does not match required value of %s" ,
405
- requiredClaim . getKey (), requiredClaim . getValue () );
434
+ requiredClaimName , requiredClaimValue );
406
435
throw new AuthenticationFailedException (tokenMap (token , idToken ));
407
436
}
408
437
}
@@ -416,8 +445,7 @@ public TokenIntrospection apply(TokenIntrospection introspectionResult, Throwabl
416
445
417
446
private void verifyTokenExpiry (String token , boolean idToken , Long exp ) {
418
447
if (isTokenExpired (exp )) {
419
- String error = String .format ("Token issued to client %s has expired" ,
420
- oidcConfig .clientId ().get ());
448
+ String error = String .format ("Token issued to client %s has expired" , oidcConfig .clientId ().get ());
421
449
LOG .debugf (error );
422
450
throw new AuthenticationFailedException (
423
451
new InvalidJwtException (error ,
@@ -436,7 +464,7 @@ private int getLifespanGrace() {
436
464
: 0 ;
437
465
}
438
466
439
- private static final long now () {
467
+ private static long now () {
440
468
return System .currentTimeMillis ();
441
469
}
442
470
@@ -624,7 +652,7 @@ public Key resolveKey(JsonWebSignature jws, List<JsonWebStructure> nestingContex
624
652
}
625
653
626
654
private Key initKey (Key generatedInternalSignatureKey ) {
627
- String clientSecret = OidcCommonUtils .getClientOrJwtSecret (oidcConfig .credentials );
655
+ String clientSecret = OidcCommonUtils .getClientOrJwtSecret (oidcConfig .credentials () );
628
656
if (clientSecret != null ) {
629
657
LOG .debug ("Verifying internal ID token with a configured client secret" );
630
658
return KeyUtils .createSecretKeyFromSecret (clientSecret );
@@ -642,11 +670,11 @@ public OidcConfigurationMetadata getMetadata() {
642
670
return client == null ? null : client .getMetadata ();
643
671
}
644
672
645
- private static class CustomClaimsValidator implements Validator {
673
+ private static final class CustomClaimsValidator implements Validator {
646
674
647
- private final Map <String , String > customClaims ;
675
+ private final Map <String , Set < String > > customClaims ;
648
676
649
- public CustomClaimsValidator (Map <String , String > customClaims ) {
677
+ private CustomClaimsValidator (Map <String , Set < String > > customClaims ) {
650
678
this .customClaims = customClaims ;
651
679
}
652
680
@@ -658,13 +686,29 @@ public String validate(JwtContext jwtContext) throws MalformedClaimException {
658
686
if (!claims .hasClaim (claimName )) {
659
687
return "claim " + claimName + " is missing" ;
660
688
}
661
- if (!claims .isClaimValueString (claimName )) {
662
- throw new MalformedClaimException ("expected claim " + claimName + " to be a string" );
663
- }
664
- var claimValue = claims .getStringClaimValue (claimName );
665
- var targetValue = targetClaim .getValue ();
666
- if (!claimValue .equals (targetValue )) {
667
- return "claim " + claimName + " does not match expected value of " + targetValue ;
689
+ Set <String > requiredClaimValues = targetClaim .getValue ();
690
+ if (claims .isClaimValueString (claimName )) {
691
+ if (requiredClaimValues .size () == 1 ) {
692
+ String actualClaimValue = claims .getStringClaimValue (claimName );
693
+ String requiredClaimValue = requiredClaimValues .iterator ().next ();
694
+ if (!requiredClaimValue .equals (actualClaimValue )) {
695
+ return "claim " + claimName + " does not match expected value of " + requiredClaimValues ;
696
+ }
697
+ } else {
698
+ throw new MalformedClaimException ("expected claim " + claimName + " must be a list of strings" );
699
+ }
700
+ } else {
701
+ if (claims .isClaimValueStringList (claimName )) {
702
+ List <String > actualClaimValues = claims .getStringListClaimValue (claimName );
703
+ for (String requiredClaimValue : requiredClaimValues ) {
704
+ if (!actualClaimValues .contains (requiredClaimValue )) {
705
+ return "claim " + claimName + " does not match expected value of " + requiredClaimValues ;
706
+ }
707
+ }
708
+ } else {
709
+ throw new MalformedClaimException (
710
+ "expected claim " + claimName + " must be a list of strings or a string" );
711
+ }
668
712
}
669
713
}
670
714
return null ;
0 commit comments