1
1
/*
2
- * Copyright 2002-2018 the original author or authors.
2
+ * Copyright 2002-2019 the original author or authors.
3
3
*
4
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
5
* you may not use this file except in compliance with the License.
27
27
28
28
import java .net .URL ;
29
29
import java .time .Instant ;
30
+ import java .util .HashMap ;
30
31
import java .util .List ;
32
+ import java .util .Map ;
33
+ import java .util .stream .Collectors ;
31
34
32
35
/**
33
36
* An {@link OAuth2TokenValidator} responsible for
41
44
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation">ID Token Validation</a>
42
45
*/
43
46
public final class OidcIdTokenValidator implements OAuth2TokenValidator <Jwt > {
44
- private static final OAuth2Error INVALID_ID_TOKEN_ERROR = new OAuth2Error ("invalid_id_token" );
45
47
private final ClientRegistration clientRegistration ;
46
48
47
49
public OidcIdTokenValidator (ClientRegistration clientRegistration ) {
@@ -53,27 +55,10 @@ public OidcIdTokenValidator(ClientRegistration clientRegistration) {
53
55
public OAuth2TokenValidatorResult validate (Jwt idToken ) {
54
56
// 3.1.3.7 ID Token Validation
55
57
// http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
58
+ Map <String , Object > invalidClaims = validateRequiredClaims (idToken );
56
59
57
- // Validate REQUIRED Claims
58
- URL issuer = idToken .getIssuer ();
59
- if (issuer == null ) {
60
- return invalidIdToken ();
61
- }
62
- String subject = idToken .getSubject ();
63
- if (subject == null ) {
64
- return invalidIdToken ();
65
- }
66
- List <String > audience = idToken .getAudience ();
67
- if (CollectionUtils .isEmpty (audience )) {
68
- return invalidIdToken ();
69
- }
70
- Instant expiresAt = idToken .getExpiresAt ();
71
- if (expiresAt == null ) {
72
- return invalidIdToken ();
73
- }
74
- Instant issuedAt = idToken .getIssuedAt ();
75
- if (issuedAt == null ) {
76
- return invalidIdToken ();
60
+ if (!invalidClaims .isEmpty ()){
61
+ return OAuth2TokenValidatorResult .failure (invalidIdToken (invalidClaims ));
77
62
}
78
63
79
64
// 2. The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery)
@@ -85,21 +70,21 @@ public OAuth2TokenValidatorResult validate(Jwt idToken) {
85
70
// The aud (audience) Claim MAY contain an array with more than one element.
86
71
// The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience,
87
72
// or if it contains additional audiences not trusted by the Client.
88
- if (!audience .contains (this .clientRegistration .getClientId ())) {
89
- return invalidIdToken ( );
73
+ if (!idToken . getAudience () .contains (this .clientRegistration .getClientId ())) {
74
+ invalidClaims . put ( IdTokenClaimNames . AUD , idToken . getAudience () );
90
75
}
91
76
92
77
// 4. If the ID Token contains multiple audiences,
93
78
// the Client SHOULD verify that an azp Claim is present.
94
79
String authorizedParty = idToken .getClaimAsString (IdTokenClaimNames .AZP );
95
- if (audience .size () > 1 && authorizedParty == null ) {
96
- return invalidIdToken ( );
80
+ if (idToken . getAudience () .size () > 1 && authorizedParty == null ) {
81
+ invalidClaims . put ( IdTokenClaimNames . AZP , authorizedParty );
97
82
}
98
83
99
84
// 5. If an azp (authorized party) Claim is present,
100
85
// the Client SHOULD verify that its client_id is the Claim Value.
101
86
if (authorizedParty != null && !authorizedParty .equals (this .clientRegistration .getClientId ())) {
102
- return invalidIdToken ( );
87
+ invalidClaims . put ( IdTokenClaimNames . AZP , authorizedParty );
103
88
}
104
89
105
90
// 7. The alg value SHOULD be the default of RS256 or the algorithm sent by the Client
@@ -108,16 +93,16 @@ public OAuth2TokenValidatorResult validate(Jwt idToken) {
108
93
109
94
// 9. The current time MUST be before the time represented by the exp Claim.
110
95
Instant now = Instant .now ();
111
- if (!now .isBefore (expiresAt )) {
112
- return invalidIdToken ( );
96
+ if (!now .isBefore (idToken . getExpiresAt () )) {
97
+ invalidClaims . put ( IdTokenClaimNames . EXP , idToken . getExpiresAt () );
113
98
}
114
99
115
100
// 10. The iat Claim can be used to reject tokens that were issued too far away from the current time,
116
101
// limiting the amount of time that nonces need to be stored to prevent attacks.
117
102
// The acceptable range is Client specific.
118
103
Instant maxIssuedAt = now .plusSeconds (30 );
119
- if (issuedAt .isAfter (maxIssuedAt )) {
120
- return invalidIdToken ( );
104
+ if (idToken . getIssuedAt () .isAfter (maxIssuedAt )) {
105
+ invalidClaims . put ( IdTokenClaimNames . IAT , idToken . getIssuedAt () );
121
106
}
122
107
123
108
// 11. If a nonce value was sent in the Authentication Request,
@@ -127,10 +112,45 @@ public OAuth2TokenValidatorResult validate(Jwt idToken) {
127
112
// The precise method for detecting replay attacks is Client specific.
128
113
// TODO Depends on gh-4442
129
114
115
+ if (!invalidClaims .isEmpty ()) {
116
+ return OAuth2TokenValidatorResult .failure (invalidIdToken (invalidClaims ));
117
+ }
118
+
130
119
return OAuth2TokenValidatorResult .success ();
131
120
}
132
121
133
- private static OAuth2TokenValidatorResult invalidIdToken () {
134
- return OAuth2TokenValidatorResult .failure (INVALID_ID_TOKEN_ERROR );
122
+ private static OAuth2Error invalidIdToken (Map <String , Object > invalidClaims ) {
123
+ String claimsDetail = invalidClaims .entrySet ().stream ()
124
+ .map (it -> it .getKey ()+ "(" +it .getValue ()+")" )
125
+ .collect (Collectors .joining (", " ));
126
+
127
+ return new OAuth2Error ("invalid_id_token" , "The ID Token contains invalid claims: " +claimsDetail , null );
128
+ }
129
+
130
+ private static Map <String , Object > validateRequiredClaims (Jwt idToken ){
131
+ Map <String , Object > requiredClaims = new HashMap <>();
132
+
133
+ URL issuer = idToken .getIssuer ();
134
+ if (issuer == null ) {
135
+ requiredClaims .put (IdTokenClaimNames .ISS , issuer );
136
+ }
137
+ String subject = idToken .getSubject ();
138
+ if (subject == null ) {
139
+ requiredClaims .put (IdTokenClaimNames .SUB , subject );
140
+ }
141
+ List <String > audience = idToken .getAudience ();
142
+ if (CollectionUtils .isEmpty (audience )) {
143
+ requiredClaims .put (IdTokenClaimNames .AUD , audience );
144
+ }
145
+ Instant expiresAt = idToken .getExpiresAt ();
146
+ if (expiresAt == null ) {
147
+ requiredClaims .put (IdTokenClaimNames .EXP , expiresAt );
148
+ }
149
+ Instant issuedAt = idToken .getIssuedAt ();
150
+ if (issuedAt == null ) {
151
+ requiredClaims .put (IdTokenClaimNames .IAT , issuedAt );
152
+ }
153
+
154
+ return requiredClaims ;
135
155
}
136
156
}
0 commit comments