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.openid.connect.sdk.validators;
019
020
021import java.net.URL;
022
023import net.jcip.annotations.ThreadSafe;
024
025import com.nimbusds.jose.JOSEException;
026import com.nimbusds.jose.JOSEObjectType;
027import com.nimbusds.jose.JWSAlgorithm;
028import com.nimbusds.jose.jwk.JWKSet;
029import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
030import com.nimbusds.jose.jwk.source.ImmutableSecret;
031import com.nimbusds.jose.jwk.source.JWKSource;
032import com.nimbusds.jose.jwk.source.RemoteJWKSet;
033import com.nimbusds.jose.proc.*;
034import com.nimbusds.jose.util.ResourceRetriever;
035import com.nimbusds.jwt.*;
036import com.nimbusds.jwt.proc.BadJWTException;
037import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
038import com.nimbusds.jwt.proc.DefaultJWTProcessor;
039import com.nimbusds.oauth2.sdk.GeneralException;
040import com.nimbusds.oauth2.sdk.ParseException;
041import com.nimbusds.oauth2.sdk.auth.Secret;
042import com.nimbusds.oauth2.sdk.id.ClientID;
043import com.nimbusds.oauth2.sdk.id.Issuer;
044import com.nimbusds.openid.connect.sdk.claims.LogoutTokenClaimsSet;
045import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
046import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
047
048
049/**
050 * Validator of logout tokens issued by an OpenID Provider (OP).
051 *
052 * <p>Supports processing of logout tokens with the following protection:
053 *
054 * <ul>
055 *     <li>Logout tokens signed (JWS) with the OP's RSA or EC key, require the
056 *         OP public JWK set (provided by value or URL) to verify them.
057 *     <li>Logout tokens authenticated with a JWS HMAC, require the client's
058 *         secret to verify them.
059 * </ul>
060 *
061 * <p>The logout types may be {@linkplain #TYPE explicitly typed} with
062 * {@code logout+jwt}.
063 *
064 * <p>Related specifications:
065 *
066 * <ul>
067 *     <li>OpenID Connect Back-Channel Logout 1.0, section 2.4 (draft 07).
068 * </ul>
069 */
070@ThreadSafe
071public class LogoutTokenValidator extends AbstractJWTValidator {
072        
073        
074        /**
075         * The recommended logout token JWT (typ) type.
076         */
077        public static final JOSEObjectType TYPE = new JOSEObjectType("logout+jwt");
078        
079        
080        /**
081         * {@code true} to require logout tokens to be explicitly
082         * {@link #TYPE typed}, {@code false} to accept untyped tokens.
083         */
084        private final boolean requireTypedTokens;
085
086
087        /**
088         * Creates a new validator for RSA or EC signed logout tokens where the
089         * OpenID Provider's JWK set is specified by value. Explicit typing of
090         * the logout tokens is not required but wil be checked if present.
091         *
092         * @param expectedIssuer The expected logout token issuer (OpenID
093         *                       Provider). Must not be {@code null}.
094         * @param clientID       The client ID. Must not be {@code null}.
095         * @param expectedJWSAlg The expected RSA or EC JWS algorithm. Must not
096         *                       be {@code null}.
097         * @param jwkSet         The OpenID Provider JWK set. Must not be
098         *                       {@code null}.
099         */
100        public LogoutTokenValidator(final Issuer expectedIssuer,
101                                    final ClientID clientID,
102                                    final JWSAlgorithm expectedJWSAlg,
103                                    final JWKSet jwkSet) {
104
105                this(expectedIssuer, clientID, new JWSVerificationKeySelector<>(expectedJWSAlg, new ImmutableJWKSet<>(jwkSet)),  null);
106        }
107
108
109        /**
110         * Creates a new validator for RSA or EC signed logout tokens where the
111         * OpenID Provider's JWK set is specified by URL. Explicit typing of
112         * the logout tokens is not required but wil be checked if present.
113         *
114         * @param expectedIssuer The expected logout token issuer (OpenID
115         *                       Provider). Must not be {@code null}.
116         * @param clientID       The client ID. Must not be {@code null}.
117         * @param expectedJWSAlg The expected RSA or EC JWS algorithm. Must not
118         *                       be {@code null}.
119         * @param jwkSetURI      The OpenID Provider JWK set URL. Must not be
120         *                       {@code null}.
121         */
122        public LogoutTokenValidator(final Issuer expectedIssuer,
123                                    final ClientID clientID,
124                                    final JWSAlgorithm expectedJWSAlg,
125                                    final URL jwkSetURI) {
126
127                this(expectedIssuer, clientID, expectedJWSAlg, jwkSetURI, null);
128        }
129
130
131        /**
132         * Creates a new validator for RSA or EC signed logout tokens where the
133         * OpenID Provider's JWK set is specified by URL. Permits setting of a
134         * specific resource retriever (HTTP client) for the JWK set. Explicit
135         * typing of the logout tokens is not required but wil be checked if
136         * present.
137         *
138         * @param expectedIssuer    The expected logout token issuer (OpenID
139         *                          Provider). Must not be {@code null}.
140         * @param clientID          The client ID. Must not be {@code null}.
141         * @param expectedJWSAlg    The expected RSA or EC JWS algorithm. Must
142         *                          not be {@code null}.
143         * @param jwkSetURI         The OpenID Provider JWK set URL. Must not
144         *                          be {@code null}.
145         * @param resourceRetriever For retrieving the OpenID Connect Provider
146         *                          JWK set from the specified URL. If
147         *                          {@code null} the
148         *                          {@link com.nimbusds.jose.util.DefaultResourceRetriever
149         *                          default retriever} will be used, with
150         *                          preset HTTP connect timeout, HTTP read
151         *                          timeout and entity size limit.
152         */
153        public LogoutTokenValidator(final Issuer expectedIssuer,
154                                    final ClientID clientID,
155                                    final JWSAlgorithm expectedJWSAlg,
156                                    final URL jwkSetURI,
157                                    final ResourceRetriever resourceRetriever) {
158
159                this(expectedIssuer, clientID, new JWSVerificationKeySelector<>(expectedJWSAlg, new RemoteJWKSet<>(jwkSetURI, resourceRetriever)),  null);
160        }
161
162
163        /**
164         * Creates a new validator for HMAC protected logout tokens. Explicit
165         * typing of the logout tokens is not required but wil be checked if
166         * present.
167         *
168         * @param expectedIssuer The expected logout token issuer (OpenID
169         *                       Provider). Must not be {@code null}.
170         * @param clientID       The client ID. Must not be {@code null}.
171         * @param expectedJWSAlg The expected HMAC JWS algorithm. Must not be
172         *                       {@code null}.
173         * @param clientSecret   The client secret. Must not be {@code null}.
174         */
175        public LogoutTokenValidator(final Issuer expectedIssuer,
176                                    final ClientID clientID,
177                                    final JWSAlgorithm expectedJWSAlg,
178                                    final Secret clientSecret) {
179
180                this(expectedIssuer, clientID, new JWSVerificationKeySelector<>(expectedJWSAlg, new ImmutableSecret<>(clientSecret.getValueBytes())), null);
181        }
182
183
184        /**
185         * Creates a new logout token validator.
186         *
187         * @param expectedIssuer The expected logout token issuer (OpenID
188         *                       Provider). Must not be {@code null}.
189         * @param clientID       The client ID. Must not be {@code null}.
190         * @param jwsKeySelector The key selector for JWS verification,
191         *                       {@code null} if unsecured (plain) logout tokens
192         *                       are expected.
193         * @param jweKeySelector The key selector for JWE decryption,
194         *                       {@code null} if encrypted logout tokens are
195         *                       not expected.
196         */
197        @Deprecated
198        public LogoutTokenValidator(final Issuer expectedIssuer,
199                                    final ClientID clientID,
200                                    final JWSKeySelector<?> jwsKeySelector,
201                                    final JWEKeySelector<?> jweKeySelector) {
202                
203                this(expectedIssuer, clientID, false, jwsKeySelector, jweKeySelector);
204        }
205
206
207        /**
208         * Creates a new logout token validator.
209         *
210         * @param expectedIssuer    The expected logout token issuer (OpenID
211         *                          Provider). Must not be {@code null}.
212         * @param clientID          The client ID. Must not be {@code null}.
213         * @param requireTypedToken {@code true} to require logout tokens to be
214         *                          explicitly {@link #TYPE typed},
215         *                          {@code false} to accept untyped tokens.
216         * @param jwsKeySelector    The key selector for JWS verification,
217         *                          {@code null} if unsecured (plain) logout
218         *                          tokens are expected.
219         * @param jweKeySelector    The key selector for JWE decryption,
220         *                          {@code null} if encrypted logout tokens are
221         *                          not expected.
222         */
223        public LogoutTokenValidator(final Issuer expectedIssuer,
224                                    final ClientID clientID,
225                                    final boolean requireTypedToken,
226                                    final JWSKeySelector<?> jwsKeySelector,
227                                    final JWEKeySelector<?> jweKeySelector) {
228                
229                super(TYPE, expectedIssuer, clientID, jwsKeySelector, jweKeySelector);
230                this.requireTypedTokens = requireTypedToken;
231        }
232
233
234        /**
235         * Validates the specified logout token.
236         *
237         * @param logoutToken The logout token. Must not be {@code null}.
238         *
239         * @return The claims set of the verified logout token.
240         *
241         * @throws BadJOSEException If the logout token is invalid or expired.
242         * @throws JOSEException    If an internal JOSE exception was
243         *                          encountered.
244         */
245        public LogoutTokenClaimsSet validate(final JWT logoutToken)
246                throws BadJOSEException, JOSEException {
247
248                if (logoutToken instanceof PlainJWT) {
249                        throw new BadJWTException("Unsecured (plain) logout tokens are illegal");
250                } else if (logoutToken instanceof SignedJWT) {
251                        return validate((SignedJWT) logoutToken);
252                } else if (logoutToken instanceof EncryptedJWT) {
253                        return validate((EncryptedJWT) logoutToken);
254                } else {
255                        throw new JOSEException("Unexpected JWT type: " + logoutToken.getClass());
256                }
257        }
258
259
260        /**
261         * Verifies the specified signed logout token.
262         *
263         * @param logoutToken The logout token. Must not be {@code null}.
264         *
265         * @return The claims set of the verified logout token.
266         *
267         * @throws BadJOSEException If the logout token is invalid or expired.
268         * @throws JOSEException    If an internal JOSE exception was
269         *                          encountered.
270         */
271        private LogoutTokenClaimsSet validate(final SignedJWT logoutToken)
272                throws BadJOSEException, JOSEException {
273
274                if (getJWSKeySelector() == null) {
275                        throw new BadJWTException("Verification of signed JWTs not configured");
276                }
277
278                ConfigurableJWTProcessor<?> jwtProcessor = new DefaultJWTProcessor<>();
279                jwtProcessor.setJWSTypeVerifier(new TypeVerifier(requireTypedTokens));
280                jwtProcessor.setJWSKeySelector(getJWSKeySelector());
281                jwtProcessor.setJWTClaimsSetVerifier(new LogoutTokenClaimsVerifier(getExpectedIssuer(), getClientID()));
282                JWTClaimsSet jwtClaimsSet = jwtProcessor.process(logoutToken, null);
283                return toLogoutTokenClaimsSet(jwtClaimsSet);
284        }
285
286
287        /**
288         * Verifies the specified signed and encrypted logout token.
289         *
290         * @param logoutToken The logout token. Must not be {@code null}.
291         *
292         * @return The claims set of the verified logout token.
293         *
294         * @throws BadJOSEException If the logout token is invalid or expired.
295         * @throws JOSEException    If an internal JOSE exception was
296         *                          encountered.
297         */
298        private LogoutTokenClaimsSet validate(final EncryptedJWT logoutToken)
299                throws BadJOSEException, JOSEException {
300
301                if (getJWEKeySelector() == null) {
302                        throw new BadJWTException("Decryption of JWTs not configured");
303                }
304                if (getJWSKeySelector() == null) {
305                        throw new BadJWTException("Verification of signed JWTs not configured");
306                }
307
308                ConfigurableJWTProcessor<?> jwtProcessor = new DefaultJWTProcessor<>();
309                jwtProcessor.setJWETypeVerifier(new TypeVerifier(requireTypedTokens));
310                jwtProcessor.setJWSKeySelector(getJWSKeySelector());
311                jwtProcessor.setJWEKeySelector(getJWEKeySelector());
312                jwtProcessor.setJWTClaimsSetVerifier(new LogoutTokenClaimsVerifier(getExpectedIssuer(), getClientID()));
313                JWTClaimsSet jwtClaimsSet = jwtProcessor.process(logoutToken, null);
314
315                return toLogoutTokenClaimsSet(jwtClaimsSet);
316        }
317        
318        
319        private static class TypeVerifier implements JOSEObjectTypeVerifier {
320                
321                
322                private final boolean requireTypedTokens;
323                
324                
325                public TypeVerifier(final boolean requireTypedTokens) {
326                        this.requireTypedTokens = requireTypedTokens;
327                }
328                
329                
330                @Override
331                public void verify(final JOSEObjectType type, final SecurityContext context)
332                        throws BadJOSEException {
333                
334                        if (requireTypedTokens) {
335                                if (! TYPE.equals(type)) {
336                                        throw new BadJOSEException("Invalid / missing logout token typ (type) header, must be " + TYPE);
337                                }
338                        } else {
339                                if (type != null && ! TYPE.equals(type)) {
340                                        throw new BadJOSEException("If set the logout token typ (type) header must be " + TYPE);
341                                }
342                        }
343                }
344        }
345
346
347        /**
348         * Converts a JWT claims set to a logout token claims set.
349         *
350         * @param jwtClaimsSet The JWT claims set. Must not be {@code null}.
351         *
352         * @return The logout token claims set.
353         *
354         * @throws JOSEException If conversion failed.
355         */
356        private static LogoutTokenClaimsSet toLogoutTokenClaimsSet(final JWTClaimsSet jwtClaimsSet)
357                throws JOSEException {
358
359                try {
360                        return new LogoutTokenClaimsSet(jwtClaimsSet);
361                } catch (ParseException e) {
362                        // Claims set must be verified at this point
363                        throw new JOSEException(e.getMessage(), e);
364                }
365        }
366
367
368        /**
369         * Creates a new logout token validator for the specified OpenID
370         * Provider metadata and OpenID Relying Party registration. Explicit
371         * typing of the logout tokens is not required but wil be checked if
372         * present.
373         *
374         * @param opMetadata      The OpenID Provider metadata. Must not be
375         *                        {@code null}.
376         * @param clientInfo      The OpenID Relying Party registration. Must
377         *                        not be {@code null}.
378         * @param clientJWKSource The client private JWK source, {@code null}
379         *                        if encrypted logout tokens are not expected.
380         *
381         * @return The logout token validator.
382         *
383         * @throws GeneralException If the supplied OpenID Provider metadata or
384         *                          Relying Party metadata are missing a
385         *                          required parameter or inconsistent.
386         */
387        public static LogoutTokenValidator create(final OIDCProviderMetadata opMetadata,
388                                                  final OIDCClientInformation clientInfo,
389                                                  final JWKSource<?> clientJWKSource)
390                throws GeneralException {
391                
392                // Logout tokens verified according to registered ID token algorithms!
393                // http://openid.net/specs/openid-connect-backchannel-1_0-ID1.html#Validation
394
395                // Create JWS key selector, unless id_token alg = none
396                final JWSKeySelector jwsKeySelector = IDTokenValidator.createJWSKeySelector(opMetadata, clientInfo);
397
398                // Create JWE key selector if encrypted logout tokens are expected
399                final JWEKeySelector jweKeySelector = IDTokenValidator.createJWEKeySelector(opMetadata, clientInfo, clientJWKSource);
400
401                return new LogoutTokenValidator(opMetadata.getIssuer(), clientInfo.getID(), jwsKeySelector, jweKeySelector);
402        }
403}