001package com.nimbusds.openid.connect.sdk.validators;
002
003
004import java.net.MalformedURLException;
005import java.net.URL;
006
007import com.nimbusds.jose.*;
008import com.nimbusds.jose.jwk.JWKSet;
009import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
010import com.nimbusds.jose.jwk.source.ImmutableSecret;
011import com.nimbusds.jose.jwk.source.JWKSource;
012import com.nimbusds.jose.jwk.source.RemoteJWKSet;
013import com.nimbusds.jose.proc.*;
014import com.nimbusds.jose.util.ResourceRetriever;
015import com.nimbusds.jwt.*;
016import com.nimbusds.jwt.proc.*;
017import com.nimbusds.oauth2.sdk.GeneralException;
018import com.nimbusds.oauth2.sdk.ParseException;
019import com.nimbusds.oauth2.sdk.auth.Secret;
020import com.nimbusds.oauth2.sdk.id.ClientID;
021import com.nimbusds.oauth2.sdk.id.Issuer;
022import com.nimbusds.openid.connect.sdk.Nonce;
023import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
024import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
025import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
026import net.jcip.annotations.ThreadSafe;
027
028
029/**
030 * Validator of ID tokens issued by an OpenID Provider (OP).
031 *
032 * <p>Supports processing of ID tokens with the following protection:
033 *
034 * <ul>
035 *     <li>ID tokens signed (JWS) with the OP's RSA or EC key, require the
036 *         OP public JWK set (provided by value or URL) to verify them.
037 *     <li>ID tokens authenticated with a JWS HMAC, require the client's secret
038 *         to verify them.
039 *     <li>Unsecured (plain) ID tokens received at the token endpoint.
040 * </ul>
041 *
042 * <p>Related specifications:
043 *
044 * <ul>
045 *     <li>OpenID Connect Core 1.0, sections 3.1.3.7, 3.2.2.11 and 3.3.2.12.
046 * </ul>
047 */
048@ThreadSafe
049public class IDTokenValidator implements ClockSkewAware {
050
051
052        /**
053         * The default maximum acceptable clock skew for verifying ID token
054         * timestamps, in seconds.
055         */
056        public static final int DEFAULT_MAX_CLOCK_SKEW = 60;
057
058
059        /**
060         * The expected ID token issuer.
061         */
062        private final Issuer expectedIssuer;
063
064
065        /**
066         * The requesting client.
067         */
068        private final ClientID clientID;
069
070
071        /**
072         * The JWS key selector.
073         */
074        private final JWSKeySelector jwsKeySelector;
075
076
077        /**
078         * The JWE key selector.
079         */
080        private final JWEKeySelector jweKeySelector;
081
082
083        /**
084         * The maximum acceptable clock skew, in seconds.
085         */
086        private int maxClockSkew = DEFAULT_MAX_CLOCK_SKEW;
087
088
089        /**
090         * Creates a new validator for unsecured (plain) ID tokens.
091         *
092         * @param expectedIssuer The expected ID token issuer (OpenID
093         *                       Provider). Must not be {@code null}.
094         * @param clientID       The client ID. Must not be {@code null}.
095         */
096        public IDTokenValidator(final Issuer expectedIssuer,
097                                final ClientID clientID) {
098
099                this(expectedIssuer, clientID, (JWSKeySelector)null, (JWEKeySelector)null);
100        }
101
102
103        /**
104         * Creates a new validator for RSA or EC signed ID tokens where the
105         * OpenID Provider's JWK set is specified by value.
106         *
107         * @param expectedIssuer The expected ID token issuer (OpenID
108         *                       Provider). Must not be {@code null}.
109         * @param clientID       The client ID. Must not be {@code null}.
110         * @param expectedJWSAlg The expected RSA or EC JWS algorithm. Must not
111         *                       be {@code null}.
112         * @param jwkSet         The OpenID Provider JWK set. Must not be
113         *                       {@code null}.
114         */
115        public IDTokenValidator(final Issuer expectedIssuer,
116                                final ClientID clientID,
117                                final JWSAlgorithm expectedJWSAlg,
118                                final JWKSet jwkSet) {
119
120                this(expectedIssuer, clientID, new JWSVerificationKeySelector(expectedJWSAlg, new ImmutableJWKSet(jwkSet)),  null);
121        }
122
123
124        /**
125         * Creates a new validator for RSA or EC signed ID tokens where the
126         * OpenID Provider's JWK set is specified by URL.
127         *
128         * @param expectedIssuer The expected ID token issuer (OpenID
129         *                       Provider). Must not be {@code null}.
130         * @param clientID       The client ID. Must not be {@code null}.
131         * @param expectedJWSAlg The expected RSA or EC JWS algorithm. Must not
132         *                       be {@code null}.
133         * @param jwkSetURI      The OpenID Provider JWK set URL. Must not be
134         *                       {@code null}.
135         */
136        public IDTokenValidator(final Issuer expectedIssuer,
137                                final ClientID clientID,
138                                final JWSAlgorithm expectedJWSAlg,
139                                final URL jwkSetURI) {
140
141                this(expectedIssuer, clientID, expectedJWSAlg, jwkSetURI, null);
142        }
143
144
145        /**
146         * Creates a new validator for RSA or EC signed ID tokens where the
147         * OpenID Provider's JWK set is specified by URL. Permits setting of a
148         * specific resource retriever (HTTP client) for the JWK set.
149         *
150         * @param expectedIssuer    The expected ID token issuer (OpenID
151         *                          Provider). Must not be {@code null}.
152         * @param clientID          The client ID. Must not be {@code null}.
153         * @param expectedJWSAlg    The expected RSA or EC JWS algorithm. Must
154         *                          not be {@code null}.
155         * @param jwkSetURI         The OpenID Provider JWK set URL. Must not
156         *                          be {@code null}.
157         * @param resourceRetriever For retrieving the OpenID Connect Provider
158         *                          JWK set from the specified URL. If
159         *                          {@code null} the
160         *                          {@link com.nimbusds.jose.util.DefaultResourceRetriever
161         *                          default retriever} will be used, with
162         *                          preset HTTP connect timeout, HTTP read
163         *                          timeout and entity size limit.
164         */
165        public IDTokenValidator(final Issuer expectedIssuer,
166                                final ClientID clientID,
167                                final JWSAlgorithm expectedJWSAlg,
168                                final URL jwkSetURI,
169                                final ResourceRetriever resourceRetriever) {
170
171                this(expectedIssuer, clientID, new JWSVerificationKeySelector(expectedJWSAlg, new RemoteJWKSet(jwkSetURI, resourceRetriever)),  null);
172        }
173
174
175        /**
176         * Creates a new validator for HMAC protected ID tokens.
177         *
178         * @param expectedIssuer The expected ID token issuer (OpenID
179         *                       Provider). Must not be {@code null}.
180         * @param clientID       The client ID. Must not be {@code null}.
181         * @param expectedJWSAlg The expected HMAC JWS algorithm. Must not be
182         *                       {@code null}.
183         * @param clientSecret   The client secret. Must not be {@code null}.
184         */
185        public IDTokenValidator(final Issuer expectedIssuer,
186                                final ClientID clientID,
187                                final JWSAlgorithm expectedJWSAlg,
188                                final Secret clientSecret) {
189
190                this(expectedIssuer, clientID, new JWSVerificationKeySelector(expectedJWSAlg, new ImmutableSecret(clientSecret.getValueBytes())), null);
191        }
192
193
194        /**
195         * Creates a new ID token validator.
196         *
197         * @param expectedIssuer The expected ID token issuer (OpenID
198         *                       Provider). Must not be {@code null}.
199         * @param clientID       The client ID. Must not be {@code null}.
200         * @param jwsKeySelector The key selector for JWS verification,
201         *                       {@code null} if unsecured (plain) ID tokens
202         *                       are expected.
203         * @param jweKeySelector The key selector for JWE decryption,
204         *                       {@code null} if encrypted ID tokens are not
205         *                       expected.
206         */
207        public IDTokenValidator(final Issuer expectedIssuer,
208                                final ClientID clientID,
209                                final JWSKeySelector jwsKeySelector,
210                                final JWEKeySelector jweKeySelector) {
211                if (expectedIssuer == null) {
212                        throw new IllegalArgumentException("The expected ID token issuer must not be null");
213                }
214                this.expectedIssuer = expectedIssuer;
215                if (clientID == null) {
216                        throw new IllegalArgumentException("The client ID must not be null");
217                }
218                this.clientID = clientID;
219                this.jwsKeySelector = jwsKeySelector;
220                this.jweKeySelector = jweKeySelector;
221        }
222
223
224        /**
225         * Returns the expected ID token issuer.
226         *
227         * @return The ID token issuer.
228         */
229        public Issuer getExpectedIssuer() {
230                return expectedIssuer;
231        }
232
233
234        /**
235         * Returns the client ID (the expected ID token audience).
236         *
237         * @return The client ID.
238         */
239        public ClientID getClientID() {
240                return clientID;
241        }
242
243
244        /**
245         * Returns the configured JWS key selector for signed ID token
246         * verification.
247         *
248         * @return The JWS key selector, {@code null} if none.
249         */
250        public JWSKeySelector getJWSKeySelector() {
251                return jwsKeySelector;
252        }
253
254
255        /**
256         * Returns the configured JWE key selector for encrypted ID token
257         * decryption.
258         *
259         * @return The JWE key selector, {@code null}.
260         */
261        public JWEKeySelector getJWEKeySelector() {
262                return jweKeySelector;
263        }
264
265
266        /**
267         * Gets the maximum acceptable clock skew for verifying the ID token
268         * timestamps.
269         *
270         * @return The maximum acceptable clock skew, in seconds. Zero
271         *         indicates none.
272         */
273        @Override
274        public int getMaxClockSkew() {
275
276                return maxClockSkew;
277        }
278
279
280        /**
281         * Sets the maximum acceptable clock skew for verifying the ID token
282         * timestamps.
283         *
284         * @param maxClockSkew The maximum acceptable clock skew, in seconds.
285         *                     Zero indicates none. Must not be negative.
286         */
287        @Override
288        public void setMaxClockSkew(final int maxClockSkew) {
289
290                this.maxClockSkew = maxClockSkew;
291        }
292
293
294        /**
295         * Validates the specified ID token.
296         *
297         * @param idToken       The ID token. Must not be {@code null}.
298         * @param expectedNonce The expected nonce, {@code null} if none.
299         *
300         * @return The claims set of the verified ID token.
301         *
302         * @throws BadJOSEException If the ID token is invalid or expired.
303         * @throws JOSEException    If an internal JOSE exception was
304         *                          encountered.
305         */
306        public IDTokenClaimsSet validate(final JWT idToken, final Nonce expectedNonce)
307                throws BadJOSEException, JOSEException {
308
309                if (idToken instanceof PlainJWT) {
310                        return validate((PlainJWT)idToken, expectedNonce);
311                } else if (idToken instanceof SignedJWT) {
312                        return validate((SignedJWT) idToken, expectedNonce);
313                } else if (idToken instanceof EncryptedJWT) {
314                        return validate((EncryptedJWT) idToken, expectedNonce);
315                } else {
316                        throw new JOSEException("Unexpected JWT type: " + idToken.getClass());
317                }
318        }
319
320
321        /**
322         * Verifies the specified unsecured (plain) ID token.
323         *
324         * @param idToken       The ID token. Must not be {@code null}.
325         * @param expectedNonce The expected nonce, {@code null} if none.
326         *
327         * @return The claims set of the verified ID token.
328         *
329         * @throws BadJOSEException If the ID token is invalid or expired.
330         * @throws JOSEException    If an internal JOSE exception was
331         *                          encountered.
332         */
333        private IDTokenClaimsSet validate(final PlainJWT idToken, final Nonce expectedNonce)
334                throws BadJOSEException, JOSEException {
335
336                if (jwsKeySelector != null) {
337                        throw new BadJWTException("Signed ID token expected");
338                }
339
340                JWTClaimsSet jwtClaimsSet;
341
342                try {
343                        jwtClaimsSet = idToken.getJWTClaimsSet();
344                } catch (java.text.ParseException e) {
345                        throw new BadJWTException(e.getMessage(), e);
346                }
347
348                JWTClaimsVerifier claimsVerifier = new IDTokenClaimsVerifier(expectedIssuer, clientID, expectedNonce, maxClockSkew);
349                claimsVerifier.verify(jwtClaimsSet);
350                return toIDTokenClaimsSet(jwtClaimsSet);
351        }
352
353
354        /**
355         * Verifies the specified signed ID token.
356         *
357         * @param idToken       The ID token. Must not be {@code null}.
358         * @param expectedNonce The expected nonce, {@code null} if none.
359         *
360         * @return The claims set of the verified ID token.
361         *
362         * @throws BadJOSEException If the ID token is invalid or expired.
363         * @throws JOSEException    If an internal JOSE exception was
364         *                          encountered.
365         */
366        private IDTokenClaimsSet validate(final SignedJWT idToken, final Nonce expectedNonce)
367                throws BadJOSEException, JOSEException {
368
369                if (jwsKeySelector == null) {
370                        throw new BadJWTException("Verification of signed JWTs not configured");
371                }
372
373                ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor();
374                jwtProcessor.setJWSKeySelector(jwsKeySelector);
375                jwtProcessor.setJWTClaimsVerifier(new IDTokenClaimsVerifier(expectedIssuer, clientID, expectedNonce, maxClockSkew));
376                JWTClaimsSet jwtClaimsSet = jwtProcessor.process(idToken, null);
377                return toIDTokenClaimsSet(jwtClaimsSet);
378        }
379
380
381        /**
382         * Verifies the specified signed and encrypted ID token.
383         *
384         * @param idToken       The ID token. Must not be {@code null}.
385         * @param expectedNonce The expected nonce, {@code null} if none.
386         *
387         * @return The claims set of the verified ID token.
388         *
389         * @throws BadJOSEException If the ID token is invalid or expired.
390         * @throws JOSEException    If an internal JOSE exception was
391         *                          encountered.
392         */
393        private IDTokenClaimsSet validate(final EncryptedJWT idToken, final Nonce expectedNonce)
394                throws BadJOSEException, JOSEException {
395
396                if (jweKeySelector == null) {
397                        throw new BadJWTException("Decryption of JWTs not configured");
398                }
399                if (jwsKeySelector == null) {
400                        throw new BadJWTException("Verification of signed JWTs not configured");
401                }
402
403                ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor();
404                jwtProcessor.setJWSKeySelector(jwsKeySelector);
405                jwtProcessor.setJWEKeySelector(jweKeySelector);
406                jwtProcessor.setJWTClaimsVerifier(new IDTokenClaimsVerifier(expectedIssuer, clientID, expectedNonce, maxClockSkew));
407
408                JWTClaimsSet jwtClaimsSet = jwtProcessor.process(idToken, null);
409
410                return toIDTokenClaimsSet(jwtClaimsSet);
411        }
412
413
414        /**
415         * Converts a JWT claims set to ID token claims set.
416         *
417         * @param jwtClaimsSet The JWT claims set. Must not be {@code null}.
418         *
419         * @return The ID token claims set.
420         *
421         * @throws JOSEException If conversion failed.
422         */
423        private static IDTokenClaimsSet toIDTokenClaimsSet(final JWTClaimsSet jwtClaimsSet)
424                throws JOSEException {
425
426                try {
427                        return new IDTokenClaimsSet(jwtClaimsSet);
428                } catch (ParseException e) {
429                        // Claims set must be verified at this point
430                        throw new JOSEException(e.getMessage(), e);
431                }
432        }
433
434
435        /**
436         * Creates a key selector for JWS verification.
437         *
438         * @param opMetadata The OpenID Provider metadata. Must not be
439         *                   {@code null}.
440         * @param clientInfo The Relying Party metadata. Must not be
441         *                   {@code null}.
442         *
443         * @return The JWS key selector.
444         *
445         * @throws GeneralException If the supplied OpenID Provider metadata or
446         *                          Relying Party metadata are missing a
447         *                          required parameter or inconsistent.
448         */
449        private static JWSKeySelector createJWSKeySelector(final OIDCProviderMetadata opMetadata,
450                                                           final OIDCClientInformation clientInfo)
451                throws GeneralException {
452
453                final JWSAlgorithm expectedJWSAlg = clientInfo.getOIDCMetadata().getIDTokenJWSAlg();
454
455                if (opMetadata.getIDTokenJWSAlgs() == null) {
456                        throw new GeneralException("Missing OpenID Provider id_token_signing_alg_values_supported parameter");
457                }
458
459                if (! opMetadata.getIDTokenJWSAlgs().contains(expectedJWSAlg)) {
460                        throw new GeneralException("The OpenID Provider doesn't support " + expectedJWSAlg + " ID tokens");
461                }
462
463                if (Algorithm.NONE.equals(expectedJWSAlg)) {
464                        // Skip creation of JWS key selector, plain ID tokens expected
465                        return null;
466
467                } else if (JWSAlgorithm.Family.RSA.contains(expectedJWSAlg) || JWSAlgorithm.Family.EC.contains(expectedJWSAlg)) {
468
469                        URL jwkSetURL;
470                        try {
471                                jwkSetURL = opMetadata.getJWKSetURI().toURL();
472                        } catch (MalformedURLException e) {
473                                throw new GeneralException("Invalid jwk set URI: " + e.getMessage(), e);
474                        }
475                        JWKSource jwkSource = new RemoteJWKSet(jwkSetURL); // TODO specify HTTP response limits
476
477                        return new JWSVerificationKeySelector(expectedJWSAlg, jwkSource);
478
479                } else if (JWSAlgorithm.Family.HMAC_SHA.contains(expectedJWSAlg)) {
480
481                        Secret clientSecret = clientInfo.getSecret();
482                        if (clientSecret == null) {
483                                throw new GeneralException("Missing client secret");
484                        }
485                        return new JWSVerificationKeySelector(expectedJWSAlg, new ImmutableSecret(clientSecret.getValueBytes()));
486
487                } else {
488                        throw new GeneralException("Unsupported JWS algorithm: " + expectedJWSAlg);
489                }
490        }
491
492
493        /**
494         * Creates a key selector for JWE decryption.
495         *
496         * @param opMetadata      The OpenID Provider metadata. Must not be
497         *                        {@code null}.
498         * @param clientInfo      The Relying Party metadata. Must not be
499         *                        {@code null}.
500         * @param clientJWKSource The client private JWK source, {@code null}
501         *                        if encrypted ID tokens are not expected.
502         *
503         * @return The JWE key selector.
504         *
505         * @throws GeneralException If the supplied OpenID Provider metadata or
506         *                          Relying Party metadata are missing a
507         *                          required parameter or inconsistent.
508         */
509        private static JWEKeySelector createJWEKeySelector(final OIDCProviderMetadata opMetadata,
510                                                           final OIDCClientInformation clientInfo,
511                                                           final JWKSource clientJWKSource)
512                throws GeneralException {
513
514                final JWEAlgorithm expectedJWEAlg = clientInfo.getOIDCMetadata().getIDTokenJWEAlg();
515                final EncryptionMethod expectedJWEEnc = clientInfo.getOIDCMetadata().getIDTokenJWEEnc();
516
517                if (expectedJWEAlg == null) {
518                        // Encrypted ID tokens not expected
519                        return null;
520                }
521
522                if (expectedJWEEnc == null) {
523                        throw new GeneralException("Missing required ID token JWE encryption method for " + expectedJWEAlg);
524                }
525
526                if (opMetadata.getIDTokenJWEAlgs() == null || ! opMetadata.getIDTokenJWEAlgs().contains(expectedJWEAlg)) {
527                        throw new GeneralException("The OpenID Provider doesn't support " + expectedJWEAlg + " ID tokens");
528                }
529
530                if (opMetadata.getIDTokenJWEEncs() == null || ! opMetadata.getIDTokenJWEEncs().contains(expectedJWEEnc)) {
531                        throw new GeneralException("The OpenID Provider doesn't support " + expectedJWEAlg + " / " + expectedJWEEnc + " ID tokens");
532                }
533
534                return new JWEDecryptionKeySelector(expectedJWEAlg, expectedJWEEnc, clientJWKSource);
535        }
536
537
538        /**
539         * Creates a new ID token validator for the specified OpenID Provider
540         * metadata and OpenID Relying Party registration.
541         *
542         * @param opMetadata      The OpenID Provider metadata. Must not be
543         *                        {@code null}.
544         * @param clientInfo      The OpenID Relying Party registration. Must
545         *                        not be {@code null}.
546         * @param clientJWKSource The client private JWK source, {@code null}
547         *                        if encrypted ID tokens are not expected.
548         *
549         * @return The ID token validator.
550         *
551         * @throws GeneralException If the supplied OpenID Provider metadata or
552         *                          Relying Party metadata are missing a
553         *                          required parameter or inconsistent.
554         */
555        public static IDTokenValidator create(final OIDCProviderMetadata opMetadata,
556                                              final OIDCClientInformation clientInfo,
557                                              final JWKSource clientJWKSource)
558                throws GeneralException {
559
560                // Create JWS key selector, unless id_token alg = none
561                final JWSKeySelector jwsKeySelector = createJWSKeySelector(opMetadata, clientInfo);
562
563                // Create JWE key selector if encrypted ID tokens are expected
564                final JWEKeySelector jweKeySelector = createJWEKeySelector(opMetadata, clientInfo, clientJWKSource);
565
566                return new IDTokenValidator(opMetadata.getIssuer(), clientInfo.getID(), jwsKeySelector, jweKeySelector);
567        }
568}