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 com.nimbusds.jose.JOSEException;
024import com.nimbusds.jose.JWSAlgorithm;
025import com.nimbusds.jose.jwk.JWKSet;
026import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
027import com.nimbusds.jose.jwk.source.ImmutableSecret;
028import com.nimbusds.jose.jwk.source.JWKSource;
029import com.nimbusds.jose.jwk.source.RemoteJWKSet;
030import com.nimbusds.jose.proc.BadJOSEException;
031import com.nimbusds.jose.proc.JWEKeySelector;
032import com.nimbusds.jose.proc.JWSKeySelector;
033import com.nimbusds.jose.proc.JWSVerificationKeySelector;
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;
047import net.jcip.annotations.ThreadSafe;
048
049
050/**
051 * Validator of logout tokens issued by an OpenID Provider (OP).
052 *
053 * <p>Supports processing of logout tokens with the following protection:
054 *
055 * <ul>
056 *     <li>Logout tokens signed (JWS) with the OP's RSA or EC key, require the
057 *         OP public JWK set (provided by value or URL) to verify them.
058 *     <li>Logout tokens authenticated with a JWS HMAC, require the client's
059 *         secret to verify them.
060 * </ul>
061 *
062 * <p>Related specifications:
063 *
064 * <ul>
065 *     <li>OpenID Connect Back-Channel Logout 1.0, section 2.4 (draft 04).
066 * </ul>
067 */
068@ThreadSafe
069public class LogoutTokenValidator extends AbstractJWTValidator {
070
071
072        /**
073         * Creates a new validator for RSA or EC signed logout tokens where the
074         * OpenID Provider's JWK set is specified by value.
075         *
076         * @param expectedIssuer The expected logout token issuer (OpenID
077         *                       Provider). Must not be {@code null}.
078         * @param clientID       The client ID. Must not be {@code null}.
079         * @param expectedJWSAlg The expected RSA or EC JWS algorithm. Must not
080         *                       be {@code null}.
081         * @param jwkSet         The OpenID Provider JWK set. Must not be
082         *                       {@code null}.
083         */
084        public LogoutTokenValidator(final Issuer expectedIssuer,
085                                    final ClientID clientID,
086                                    final JWSAlgorithm expectedJWSAlg,
087                                    final JWKSet jwkSet) {
088
089                this(expectedIssuer, clientID, new JWSVerificationKeySelector(expectedJWSAlg, new ImmutableJWKSet(jwkSet)),  null);
090        }
091
092
093        /**
094         * Creates a new validator for RSA or EC signed logout tokens where the
095         * OpenID Provider's JWK set is specified by URL.
096         *
097         * @param expectedIssuer The expected logout token issuer (OpenID
098         *                       Provider). Must not be {@code null}.
099         * @param clientID       The client ID. Must not be {@code null}.
100         * @param expectedJWSAlg The expected RSA or EC JWS algorithm. Must not
101         *                       be {@code null}.
102         * @param jwkSetURI      The OpenID Provider JWK set URL. Must not be
103         *                       {@code null}.
104         */
105        public LogoutTokenValidator(final Issuer expectedIssuer,
106                                    final ClientID clientID,
107                                    final JWSAlgorithm expectedJWSAlg,
108                                    final URL jwkSetURI) {
109
110                this(expectedIssuer, clientID, expectedJWSAlg, jwkSetURI, null);
111        }
112
113
114        /**
115         * Creates a new validator for RSA or EC signed logout tokens where the
116         * OpenID Provider's JWK set is specified by URL. Permits setting of a
117         * specific resource retriever (HTTP client) for the JWK set.
118         *
119         * @param expectedIssuer    The expected logout token issuer (OpenID
120         *                          Provider). Must not be {@code null}.
121         * @param clientID          The client ID. Must not be {@code null}.
122         * @param expectedJWSAlg    The expected RSA or EC JWS algorithm. Must
123         *                          not be {@code null}.
124         * @param jwkSetURI         The OpenID Provider JWK set URL. Must not
125         *                          be {@code null}.
126         * @param resourceRetriever For retrieving the OpenID Connect Provider
127         *                          JWK set from the specified URL. If
128         *                          {@code null} the
129         *                          {@link com.nimbusds.jose.util.DefaultResourceRetriever
130         *                          default retriever} will be used, with
131         *                          preset HTTP connect timeout, HTTP read
132         *                          timeout and entity size limit.
133         */
134        public LogoutTokenValidator(final Issuer expectedIssuer,
135                                    final ClientID clientID,
136                                    final JWSAlgorithm expectedJWSAlg,
137                                    final URL jwkSetURI,
138                                    final ResourceRetriever resourceRetriever) {
139
140                this(expectedIssuer, clientID, new JWSVerificationKeySelector(expectedJWSAlg, new RemoteJWKSet(jwkSetURI, resourceRetriever)),  null);
141        }
142
143
144        /**
145         * Creates a new validator for HMAC protected logout tokens.
146         *
147         * @param expectedIssuer The expected logout token issuer (OpenID
148         *                       Provider). Must not be {@code null}.
149         * @param clientID       The client ID. Must not be {@code null}.
150         * @param expectedJWSAlg The expected HMAC JWS algorithm. Must not be
151         *                       {@code null}.
152         * @param clientSecret   The client secret. Must not be {@code null}.
153         */
154        public LogoutTokenValidator(final Issuer expectedIssuer,
155                                    final ClientID clientID,
156                                    final JWSAlgorithm expectedJWSAlg,
157                                    final Secret clientSecret) {
158
159                this(expectedIssuer, clientID, new JWSVerificationKeySelector(expectedJWSAlg, new ImmutableSecret(clientSecret.getValueBytes())), null);
160        }
161
162
163        /**
164         * Creates a new logout token validator.
165         *
166         * @param expectedIssuer The expected logout token issuer (OpenID
167         *                       Provider). Must not be {@code null}.
168         * @param clientID       The client ID. Must not be {@code null}.
169         * @param jwsKeySelector The key selector for JWS verification,
170         *                       {@code null} if unsecured (plain) logout tokens
171         *                       are expected.
172         * @param jweKeySelector The key selector for JWE decryption,
173         *                       {@code null} if encrypted logout tokens are
174         *                       not expected.
175         */
176        public LogoutTokenValidator(final Issuer expectedIssuer,
177                                    final ClientID clientID,
178                                    final JWSKeySelector jwsKeySelector,
179                                    final JWEKeySelector jweKeySelector) {
180                
181                super(expectedIssuer, clientID, jwsKeySelector, jweKeySelector);
182        }
183
184
185        /**
186         * Validates the specified logout token.
187         *
188         * @param logoutToken The logout token. Must not be {@code null}.
189         *
190         * @return The claims set of the verified logout token.
191         *
192         * @throws BadJOSEException If the logout token is invalid or expired.
193         * @throws JOSEException    If an internal JOSE exception was
194         *                          encountered.
195         */
196        public LogoutTokenClaimsSet validate(final JWT logoutToken)
197                throws BadJOSEException, JOSEException {
198
199                if (logoutToken instanceof PlainJWT) {
200                        throw new BadJWTException("Unsecured (plain) logout tokens are illegal");
201                } else if (logoutToken instanceof SignedJWT) {
202                        return validate((SignedJWT) logoutToken);
203                } else if (logoutToken instanceof EncryptedJWT) {
204                        return validate((EncryptedJWT) logoutToken);
205                } else {
206                        throw new JOSEException("Unexpected JWT type: " + logoutToken.getClass());
207                }
208        }
209
210
211        /**
212         * Verifies the specified signed logout token.
213         *
214         * @param logoutToken The logout token. Must not be {@code null}.
215         *
216         * @return The claims set of the verified logout token.
217         *
218         * @throws BadJOSEException If the logout token is invalid or expired.
219         * @throws JOSEException    If an internal JOSE exception was
220         *                          encountered.
221         */
222        private LogoutTokenClaimsSet validate(final SignedJWT logoutToken)
223                throws BadJOSEException, JOSEException {
224
225                if (getJWSKeySelector() == null) {
226                        throw new BadJWTException("Verification of signed JWTs not configured");
227                }
228
229                ConfigurableJWTProcessor<?> jwtProcessor = new DefaultJWTProcessor();
230                jwtProcessor.setJWSKeySelector(getJWSKeySelector());
231                jwtProcessor.setJWTClaimsSetVerifier(new LogoutTokenClaimsVerifier(getExpectedIssuer(), getClientID()));
232                JWTClaimsSet jwtClaimsSet = jwtProcessor.process(logoutToken, null);
233                return toLogoutTokenClaimsSet(jwtClaimsSet);
234        }
235
236
237        /**
238         * Verifies the specified signed and encrypted logout token.
239         *
240         * @param logoutToken The logout token. Must not be {@code null}.
241         *
242         * @return The claims set of the verified logout token.
243         *
244         * @throws BadJOSEException If the logout token is invalid or expired.
245         * @throws JOSEException    If an internal JOSE exception was
246         *                          encountered.
247         */
248        private LogoutTokenClaimsSet validate(final EncryptedJWT logoutToken)
249                throws BadJOSEException, JOSEException {
250
251                if (getJWEKeySelector() == null) {
252                        throw new BadJWTException("Decryption of JWTs not configured");
253                }
254                if (getJWSKeySelector() == null) {
255                        throw new BadJWTException("Verification of signed JWTs not configured");
256                }
257
258                ConfigurableJWTProcessor<?> jwtProcessor = new DefaultJWTProcessor();
259                jwtProcessor.setJWSKeySelector(getJWSKeySelector());
260                jwtProcessor.setJWEKeySelector(getJWEKeySelector());
261                jwtProcessor.setJWTClaimsSetVerifier(new LogoutTokenClaimsVerifier(getExpectedIssuer(), getClientID()));
262
263                JWTClaimsSet jwtClaimsSet = jwtProcessor.process(logoutToken, null);
264
265                return toLogoutTokenClaimsSet(jwtClaimsSet);
266        }
267
268
269        /**
270         * Converts a JWT claims set to a logout token claims set.
271         *
272         * @param jwtClaimsSet The JWT claims set. Must not be {@code null}.
273         *
274         * @return The logout token claims set.
275         *
276         * @throws JOSEException If conversion failed.
277         */
278        private static LogoutTokenClaimsSet toLogoutTokenClaimsSet(final JWTClaimsSet jwtClaimsSet)
279                throws JOSEException {
280
281                try {
282                        return new LogoutTokenClaimsSet(jwtClaimsSet);
283                } catch (ParseException e) {
284                        // Claims set must be verified at this point
285                        throw new JOSEException(e.getMessage(), e);
286                }
287        }
288
289
290        /**
291         * Creates a new logout token validator for the specified OpenID
292         * Provider metadata and OpenID Relying Party registration.
293         *
294         * @param opMetadata      The OpenID Provider metadata. Must not be
295         *                        {@code null}.
296         * @param clientInfo      The OpenID Relying Party registration. Must
297         *                        not be {@code null}.
298         * @param clientJWKSource The client private JWK source, {@code null}
299         *                        if encrypted logout tokens are not expected.
300         *
301         * @return The logout token validator.
302         *
303         * @throws GeneralException If the supplied OpenID Provider metadata or
304         *                          Relying Party metadata are missing a
305         *                          required parameter or inconsistent.
306         */
307        public static LogoutTokenValidator create(final OIDCProviderMetadata opMetadata,
308                                                  final OIDCClientInformation clientInfo,
309                                                  final JWKSource clientJWKSource)
310                throws GeneralException {
311                
312                // Logout tokens verified according to registered ID token algorithms!
313                // http://openid.net/specs/openid-connect-backchannel-1_0-ID1.html#Validation
314
315                // Create JWS key selector, unless id_token alg = none
316                final JWSKeySelector jwsKeySelector = IDTokenValidator.createJWSKeySelector(opMetadata, clientInfo);
317
318                // Create JWE key selector if encrypted logout tokens are expected
319                final JWEKeySelector jweKeySelector = IDTokenValidator.createJWEKeySelector(opMetadata, clientInfo, clientJWKSource);
320
321                return new LogoutTokenValidator(opMetadata.getIssuer(), clientInfo.getID(), jwsKeySelector, jweKeySelector);
322        }
323}