001/*
002 * oauth2-oidc-sdk
003 *
004 * Copyright 2012-2016, Connect2id Ltd and contributors.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
007 * this file except in compliance with the License. You may obtain a copy of the
008 * License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software distributed
013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
015 * specific language governing permissions and limitations under the License.
016 */
017
018package com.nimbusds.oauth2.sdk.jarm;
019
020
021import java.io.IOException;
022import java.net.MalformedURLException;
023import java.net.URL;
024import java.text.ParseException;
025
026import com.nimbusds.jose.*;
027import com.nimbusds.jose.jwk.JWKSet;
028import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
029import com.nimbusds.jose.jwk.source.ImmutableSecret;
030import com.nimbusds.jose.jwk.source.JWKSource;
031import com.nimbusds.jose.jwk.source.RemoteJWKSet;
032import com.nimbusds.jose.proc.*;
033import com.nimbusds.jose.util.ResourceRetriever;
034import com.nimbusds.jwt.*;
035import com.nimbusds.jwt.proc.BadJWTException;
036import com.nimbusds.jwt.proc.ClockSkewAware;
037import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
038import com.nimbusds.jwt.proc.DefaultJWTProcessor;
039import com.nimbusds.oauth2.sdk.GeneralException;
040import com.nimbusds.oauth2.sdk.as.AuthorizationServerMetadata;
041import com.nimbusds.oauth2.sdk.auth.Secret;
042import com.nimbusds.oauth2.sdk.client.ClientInformation;
043import com.nimbusds.oauth2.sdk.id.ClientID;
044import com.nimbusds.oauth2.sdk.id.Issuer;
045import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
046import com.nimbusds.openid.connect.sdk.validators.AbstractJWTValidator;
047import net.jcip.annotations.ThreadSafe;
048
049
050/**
051 * Validator of JSON Web Token (JWT) secured authorisation responses (JARM).
052 *
053 * <p>Supports processing of JWT responses with the following protection:
054 *
055 * <ul>
056 *     <li>JWTs signed (JWS) with the Authorisation Server's RSA or EC key,
057 *         require the Authorisation Server's public JWK set (provided by value
058 *         or URL) to verify them.
059 *     <li>JWTs authenticated with a JWS HMAC, require the client's secret
060 *         to verify them.
061 * </ul>
062 *
063 * <p>Convenience static methods for creating a validator from Authorisation
064 * Server  metadata or issuer URL, and the registered OAuth 2.0 client
065 * information:
066 *
067 * <ul>
068 *     <li>{@link #create(AuthorizationServerMetadata, ClientInformation)}
069 *     <li>{@link #create(Issuer, ClientInformation)}
070 * </ul>
071 *
072 * <p>Related specifications:
073 *
074 * <ul>
075 *     <li>Financial-grade API: JWT Secured Authorization Response Mode for
076 *         OAuth 2.0 (JARM).
077 * </ul>
078 */
079@ThreadSafe
080public class JARMValidator extends AbstractJWTValidator implements ClockSkewAware {
081        
082        
083        /**
084         * Creates a new JARM validator for RSA or EC signed authorisation
085         * responses where the Authorisation Server's JWK set is specified by
086         * value.
087         *
088         * @param expectedIssuer The expected issuer (Authorisation Server).
089         *                       Must not be {@code null}.
090         * @param clientID       The client ID. Must not be {@code null}.
091         * @param expectedJWSAlg The expected RSA or EC JWS algorithm. Must not
092         *                       be {@code null}.
093         * @param jwkSet         The Authorisation Server JWK set. Must not be
094         *                       {@code null}.
095         */
096        public JARMValidator(final Issuer expectedIssuer,
097                             final ClientID clientID,
098                             final JWSAlgorithm expectedJWSAlg,
099                             final JWKSet jwkSet) {
100                
101                this(expectedIssuer, clientID, new JWSVerificationKeySelector(expectedJWSAlg, new ImmutableJWKSet(jwkSet)),  null);
102        }
103        
104        
105        /**
106         * Creates a new JARM validator for RSA or EC signed authorisation
107         * responses where the Authorisation Server's JWK set is specified by
108         * URL.
109         *
110         * @param expectedIssuer The expected issuer (Authorisation Server).
111         *                       Must not be {@code null}.
112         * @param clientID       The client ID. Must not be {@code null}.
113         * @param expectedJWSAlg The expected RSA or EC JWS algorithm. Must not
114         *                       be {@code null}.
115         * @param jwkSetURI      The OpenID Provider JWK set URL. Must not be
116         *                       {@code null}.
117         */
118        public JARMValidator(final Issuer expectedIssuer,
119                             final ClientID clientID,
120                             final JWSAlgorithm expectedJWSAlg,
121                             final URL jwkSetURI) {
122                
123                this(expectedIssuer, clientID, expectedJWSAlg, jwkSetURI, null);
124        }
125        
126        
127        /**
128         * Creates a new JARM validator for RSA or EC signed authorisation
129         * responses where the Authorisation Server's JWK set is specified by
130         * URL. Permits setting of a specific resource retriever (HTTP client)
131         * for the JWK set.
132         *
133         * @param expectedIssuer    The expected issuer (Authorisation Server).
134         *                          Must not be {@code null}.
135         * @param clientID          The client ID. Must not be {@code null}.
136         * @param expectedJWSAlg    The expected RSA or EC JWS algorithm. Must
137         *                          not be {@code null}.
138         * @param jwkSetURI         The Authorisation Server JWK set URL. Must
139         *                          not be {@code null}.
140         * @param resourceRetriever For retrieving the Authorisation Server JWK
141         *                          from the specified URL. If {@code null} the
142         *                          {@link com.nimbusds.jose.util.DefaultResourceRetriever
143         *                          default retriever} will be used, with
144         *                          preset HTTP connect timeout, HTTP read
145         *                          timeout and entity size limit.
146         */
147        public JARMValidator(final Issuer expectedIssuer,
148                             final ClientID clientID,
149                             final JWSAlgorithm expectedJWSAlg,
150                             final URL jwkSetURI,
151                             final ResourceRetriever resourceRetriever) {
152                
153                this(expectedIssuer, clientID, new JWSVerificationKeySelector(expectedJWSAlg, new RemoteJWKSet(jwkSetURI, resourceRetriever)),  null);
154        }
155        
156        
157        /**
158         * Creates a new JARM validator for HMAC protected authorisation
159         * responses.
160         *
161         * @param expectedIssuer The expected issuer (Authorisation Server).
162         *                       Must not be {@code null}.
163         * @param clientID       The client ID. Must not be {@code null}.
164         * @param expectedJWSAlg The expected HMAC JWS algorithm. Must not be
165         *                       {@code null}.
166         * @param clientSecret   The client secret. Must not be {@code null}.
167         */
168        public JARMValidator(final Issuer expectedIssuer,
169                             final ClientID clientID,
170                             final JWSAlgorithm expectedJWSAlg,
171                             final Secret clientSecret) {
172                
173                this(expectedIssuer, clientID, new JWSVerificationKeySelector(expectedJWSAlg, new ImmutableSecret(clientSecret.getValueBytes())), null);
174        }
175        
176        
177        /**
178         * Creates a new JARM validator.
179         *
180         * @param expectedIssuer The expected issuer (Authorisation Server).
181         *                       Must not be {@code null}.
182         * @param clientID       The client ID. Must not be {@code null}.
183         * @param jwsKeySelector The key selector for JWS verification, must
184         *                       not be {@code null}.
185         * @param jweKeySelector The key selector for JWE decryption,
186         *                       {@code null} if encrypted authorisation
187         *                       responses are not expected.
188         */
189        public JARMValidator(final Issuer expectedIssuer,
190                             final ClientID clientID,
191                             final JWSKeySelector jwsKeySelector,
192                             final JWEKeySelector jweKeySelector) {
193                
194                super(expectedIssuer, clientID, jwsKeySelector, jweKeySelector);
195        }
196        
197        
198        /**
199         * Validates the specified JWT-secured authorisation response.
200         *
201         * @param jwtResponseString The JWT-secured authorisation response
202         *                          string. Must not be {@code null}.
203         *
204         * @return The claims set of the verified JWT.
205         *
206         * @throws BadJOSEException If the JWT is invalid or expired.
207         * @throws JOSEException    If an internal JOSE exception was
208         *                          encountered.
209         */
210        public JWTClaimsSet validate(final String jwtResponseString)
211                throws BadJOSEException, JOSEException {
212                
213                try {
214                        return validate(JWTParser.parse(jwtResponseString));
215                } catch (ParseException e) {
216                        throw new BadJOSEException("Invalid JWT: " + e.getMessage(), e);
217                }
218        }
219        
220        
221        /**
222         * Validates the specified JWT-secured authorisation response.
223         *
224         * @param jwtResponse The JWT-secured authorisation response. Must not
225         *                    be {@code null}.
226         *
227         * @return The claims set of the verified JWT.
228         *
229         * @throws BadJOSEException If the JWT is invalid or expired.
230         * @throws JOSEException    If an internal JOSE exception was
231         *                          encountered.
232         */
233        public JWTClaimsSet validate(final JWT jwtResponse)
234                throws BadJOSEException, JOSEException {
235                
236                if (jwtResponse instanceof SignedJWT) {
237                        return validate((SignedJWT) jwtResponse);
238                } else if (jwtResponse instanceof EncryptedJWT) {
239                        return validate((EncryptedJWT) jwtResponse);
240                } else if (jwtResponse instanceof PlainJWT) {
241                        throw new BadJWTException("The JWT must not be plain (unsecured)");
242                } else {
243                        throw new BadJOSEException("Unexpected JWT type: " + jwtResponse.getClass());
244                }
245        }
246        
247        
248        /**
249         * Verifies the specified signed authorisation response.
250         *
251         * @param jwtResponse The JWT-secured authorisation response. Must not
252         *                    be {@code null}.
253         *
254         * @return The claims set of the verified JWT.
255         *
256         * @throws BadJOSEException If the JWT is invalid or expired.
257         * @throws JOSEException    If an internal JOSE exception was
258         *                          encountered.
259         */
260        private JWTClaimsSet validate(final SignedJWT jwtResponse)
261                throws BadJOSEException, JOSEException {
262                
263                if (getJWSKeySelector() == null) {
264                        throw new BadJWTException("Verification of signed JWTs not configured");
265                }
266                
267                ConfigurableJWTProcessor<?> jwtProcessor = new DefaultJWTProcessor();
268                jwtProcessor.setJWSKeySelector(getJWSKeySelector());
269                jwtProcessor.setJWTClaimsSetVerifier(new JARMClaimsVerifier(getExpectedIssuer(), getClientID(), getMaxClockSkew()));
270                return jwtProcessor.process(jwtResponse, null);
271        }
272        
273        
274        /**
275         * Verifies the specified signed and encrypted authorisation response.
276         *
277         * @param jwtResponse The JWT-secured authorisation response. Must not
278         *                    be {@code null}.
279         *
280         * @return The claims set of the verified JWT.
281         *
282         * @throws BadJOSEException If the JWT is invalid or expired.
283         * @throws JOSEException    If an internal JOSE exception was
284         *                          encountered.
285         */
286        private JWTClaimsSet validate(final EncryptedJWT jwtResponse)
287                throws BadJOSEException, JOSEException {
288                
289                if (getJWEKeySelector() == null) {
290                        throw new BadJWTException("Decryption of JWTs not configured");
291                }
292                if (getJWSKeySelector() == null) {
293                        throw new BadJWTException("Verification of signed JWTs not configured");
294                }
295                
296                ConfigurableJWTProcessor<?> jwtProcessor = new DefaultJWTProcessor();
297                jwtProcessor.setJWSKeySelector(getJWSKeySelector());
298                jwtProcessor.setJWEKeySelector(getJWEKeySelector());
299                jwtProcessor.setJWTClaimsSetVerifier(new JARMClaimsVerifier(getExpectedIssuer(), getClientID(), getMaxClockSkew()));
300                
301                return jwtProcessor.process(jwtResponse, null);
302        }
303        
304        
305        /**
306         * Creates a key selector for JWS verification.
307         *
308         * @param asMetadata The Authorisation Server metadata. Must not be
309         *                   {@code null}.
310         * @param clientInfo The OAuth 2.0 client information. Must not be
311         *                   {@code null}.
312         *
313         * @return The JWS key selector.
314         *
315         * @throws GeneralException If the supplied Authorisation Server
316         *                          metadata or OAuth 2.0 client information
317         *                          are missing a required parameter or
318         *                          inconsistent.
319         */
320        protected static JWSKeySelector createJWSKeySelector(final com.nimbusds.oauth2.sdk.as.AuthorizationServerMetadata asMetadata,
321                                                             final ClientInformation clientInfo)
322                throws GeneralException {
323                
324                final JWSAlgorithm expectedJWSAlg = clientInfo.getMetadata().getAuthorizationJWSAlg();
325                
326                if (asMetadata.getAuthorizationJWSAlgs() == null) {
327                        throw new GeneralException("Missing Authorization Server authorization_signing_alg_values_supported parameter");
328                }
329                
330                if (! asMetadata.getAuthorizationJWSAlgs().contains(expectedJWSAlg)) {
331                        throw new GeneralException("The Authorization Server doesn't support " + expectedJWSAlg + " authorization responses");
332                }
333                
334                if (Algorithm.NONE.equals(expectedJWSAlg)) {
335                        // Skip creation of JWS key selector, plain ID tokens expected
336                        return null;
337                        
338                } else if (JWSAlgorithm.Family.RSA.contains(expectedJWSAlg) || JWSAlgorithm.Family.EC.contains(expectedJWSAlg)) {
339                        
340                        URL jwkSetURL;
341                        try {
342                                jwkSetURL = asMetadata.getJWKSetURI().toURL();
343                        } catch (MalformedURLException e) {
344                                throw new GeneralException("Invalid jwk set URI: " + e.getMessage(), e);
345                        }
346                        JWKSource jwkSource = new RemoteJWKSet(jwkSetURL); // TODO specify HTTP response limits
347                        
348                        return new JWSVerificationKeySelector(expectedJWSAlg, jwkSource);
349                        
350                } else if (JWSAlgorithm.Family.HMAC_SHA.contains(expectedJWSAlg)) {
351                        
352                        Secret clientSecret = clientInfo.getSecret();
353                        if (clientSecret == null) {
354                                throw new GeneralException("Missing client secret");
355                        }
356                        return new JWSVerificationKeySelector(expectedJWSAlg, new ImmutableSecret(clientSecret.getValueBytes()));
357                        
358                } else {
359                        throw new GeneralException("Unsupported JWS algorithm: " + expectedJWSAlg);
360                }
361        }
362        
363        
364        /**
365         * Creates a key selector for JWE decryption.
366         *
367         * @param asMetadata      The Authorisation Server metadata. Must not
368         *                        be {@code null}.
369         * @param clientInfo      The OAuth 2.0 client information. Must not be
370         *                        {@code null}.
371         * @param clientJWKSource The client private JWK source, {@code null}
372         *                        if encrypted JWT-secured authorisation
373         *                        responses are not expected.
374         *
375         * @return The JWE key selector.
376         *
377         * @throws GeneralException If the supplied Authorisation Server
378         *                          metadata or OAuth 2.0 client information
379         *                          are missing a required parameter or
380         *                          inconsistent.
381         */
382        protected static JWEKeySelector createJWEKeySelector(final AuthorizationServerMetadata asMetadata,
383                                                             final ClientInformation clientInfo,
384                                                             final JWKSource clientJWKSource)
385                throws GeneralException {
386                
387                final JWEAlgorithm expectedJWEAlg = clientInfo.getMetadata().getAuthorizationJWEAlg();
388                final EncryptionMethod expectedJWEEnc = clientInfo.getMetadata().getAuthorizationJWEEnc();
389                
390                if (expectedJWEAlg == null) {
391                        // Encrypted JWTs not expected
392                        return null;
393                }
394                
395                if (expectedJWEEnc == null) {
396                        throw new GeneralException("Missing required authorization response JWE encryption method for " + expectedJWEAlg);
397                }
398                
399                if (asMetadata.getAuthorizationJWEAlgs() == null || ! asMetadata.getAuthorizationJWEAlgs().contains(expectedJWEAlg)) {
400                        throw new GeneralException("The Authorization Server doesn't support " + expectedJWEAlg + " authorization responses");
401                }
402                
403                if (asMetadata.getAuthorizationJWEEncs() == null || ! asMetadata.getAuthorizationJWEEncs().contains(expectedJWEEnc)) {
404                        throw new GeneralException("The Authorization Server doesn't support " + expectedJWEAlg + " / " + expectedJWEEnc + " authorization responses");
405                }
406                
407                return new JWEDecryptionKeySelector(expectedJWEAlg, expectedJWEEnc, clientJWKSource);
408        }
409        
410        
411        /**
412         * Creates a new JARM validator for the specified Authorisation Server
413         * metadata and OAuth 2.0 client registration.
414         *
415         * @param asMetadata      The Authorisation Server metadata. Must not
416         *                        be {@code null}.
417         * @param clientInfo      The OAuth 2.0 client registration. Must not
418         *                        be {@code null}.
419         * @param clientJWKSource The client private JWK source, {@code null}
420         *                        if encrypted authorisation responses are not
421         *                        expected.
422         *
423         * @return The JARM validator.
424         *
425         * @throws GeneralException If the supplied Authorisation Server
426         *                          metadata or OAuth 2.0 client information
427         *                          are missing a required parameter or
428         *                          inconsistent.
429         */
430        public static JARMValidator create(final AuthorizationServerMetadata asMetadata,
431                                           final ClientInformation clientInfo,
432                                           final JWKSource clientJWKSource)
433                throws GeneralException {
434                
435                // Create JWS key selector, unless id_token alg = none
436                final JWSKeySelector jwsKeySelector = createJWSKeySelector(asMetadata, clientInfo);
437                
438                // Create JWE key selector if encrypted ID tokens are expected
439                final JWEKeySelector jweKeySelector = createJWEKeySelector(asMetadata, clientInfo, clientJWKSource);
440                
441                return new JARMValidator(asMetadata.getIssuer(), clientInfo.getID(), jwsKeySelector, jweKeySelector);
442        }
443        
444        
445        /**
446         * Creates a new JARM validator for the specified Authorisation Server
447         * metadata and OAuth 2.0 client registration.
448         *
449         * @param asMetadata The Authorisation Server metadata. Must not be
450         *                   {@code null}.
451         * @param clientInfo The OAuth 2.0 client registration. Must not be
452         *                   {@code null}.
453         *
454         * @return The JARM validator.
455         *
456         * @throws GeneralException If the supplied Authorisation Server
457         *                          metadata or OAuth 2.0 client information
458         *                          are missing a required parameter or
459         *                          inconsistent.
460         */
461        public static JARMValidator create(final AuthorizationServerMetadata asMetadata,
462                                           final ClientInformation clientInfo)
463                throws GeneralException {
464                
465                return create(asMetadata, clientInfo, null);
466        }
467        
468        
469        /**
470         * Creates a new JARM validator for the specified Authorisation Server
471         * or OpenID Provider, which must publish its metadata at
472         * {@code [issuer-url]/.well-known/oauth-authorization-server} resp.
473         * {@code [issuer-url]/.well-known/openid-configuration}.
474         *
475         * @param issuer     The Authorisation Server / OpenID Provider issuer 
476         *                   identifier. Must not be {@code null}.
477         * @param clientInfo The OAuth 2.0 client registration. Must not be
478         *                   {@code null}.
479         *
480         * @return The JARM validator.
481         *
482         * @throws GeneralException If the resolved Authorisation Server / 
483         *                          OpenID Provider metadata is invalid.
484         * @throws IOException      On a HTTP exception.
485         */
486        public static JARMValidator create(final Issuer issuer,
487                                           final ClientInformation clientInfo)
488                throws GeneralException, IOException {
489                
490                return create(issuer, clientInfo, null, 0, 0);
491        }
492        
493        
494        /**
495         * Creates a new JARM validator for the specified Authorisation Server
496         * or OpenID Provider, which must publish its metadata at
497         * {@code [issuer-url]/.well-known/oauth-authorization-server} resp.
498         * {@code [issuer-url]/.well-known/openid-configuration}.
499         *
500         * @param issuer          The Authorisation Server / OpenID Provider 
501         *                        issuer identifier. Must not be {@code null}.
502         * @param clientInfo      The OAuth 2.0 client registration. Must not 
503         *                        be {@code null}.
504         * @param clientJWKSource The client private JWK source, {@code null}
505         *                        if encrypted authorisation responses are not 
506         *                        expected.
507         * @param connectTimeout  The HTTP connect timeout, in milliseconds.
508         *                        Zero implies no timeout. Must not be
509         *                        negative.
510         * @param readTimeout     The HTTP response read timeout, in
511         *                        milliseconds. Zero implies no timeout. Must
512         *                        not be negative.
513         *
514         * @return The JARM validator.
515         *
516         * @throws GeneralException If the resolved Authorisation Server / 
517         *                          OpenID Provider metadata is invalid.
518         * @throws IOException      On a HTTP exception.
519         */
520        public static JARMValidator create(final Issuer issuer,
521                                           final ClientInformation clientInfo,
522                                           final JWKSource clientJWKSource,
523                                           final int connectTimeout,
524                                           final int readTimeout)
525                throws GeneralException, IOException {
526                
527                AuthorizationServerMetadata asMetadata;
528                
529                try {
530                        // Try OP well-known URL first
531                        asMetadata = OIDCProviderMetadata.resolve(issuer, connectTimeout, readTimeout);
532                        
533                } catch (Exception e) {
534                        
535                        // Retry with AS well-known URL
536                        asMetadata = AuthorizationServerMetadata.resolve(issuer, connectTimeout, readTimeout); 
537                }
538                
539                return create(asMetadata, clientInfo, clientJWKSource);
540        }
541}