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}