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.util.Date; 022import java.util.List; 023 024import com.nimbusds.jose.util.DateUtils; 025import com.nimbusds.jwt.JWTClaimsSet; 026import com.nimbusds.jwt.proc.BadJWTException; 027import com.nimbusds.jwt.proc.ClockSkewAware; 028import com.nimbusds.jwt.proc.JWTClaimsVerifier; 029import com.nimbusds.oauth2.sdk.id.ClientID; 030import com.nimbusds.oauth2.sdk.id.Issuer; 031import com.nimbusds.openid.connect.sdk.Nonce; 032import net.jcip.annotations.ThreadSafe; 033 034 035/** 036 * ID token claims verifier. 037 * 038 * <p>Related specifications: 039 * 040 * <ul> 041 * <li>OpenID Connect Core 1.0, section 3.1.3.7 for code flow. 042 * <li>OpenID Connect Core 1.0, section 3.2.2.11 for implicit flow. 043 * <li>OpenID Connect Core 1.0, sections 3.3.2.12 and 3.3.3.7 for hybrid 044 * flow. 045 * </ul> 046 */ 047@ThreadSafe 048public class IDTokenClaimsVerifier implements JWTClaimsVerifier, ClockSkewAware { 049 050 051 // Cache general exceptions 052 /** 053 * Missing {@code exp} claim exception. 054 */ 055 private static final BadJWTException MISSING_EXP_CLAIM_EXCEPTION = 056 new BadJWTException("Missing JWT expiration (exp) claim"); 057 058 059 /** 060 * Missing {@code iat} claim exception. 061 */ 062 private static final BadJWTException MISSING_IAT_CLAIM_EXCEPTION = 063 new BadJWTException("Missing JWT issue time (iat) claim"); 064 065 066 /** 067 * Missing {@code iss} claim exception. 068 */ 069 private static final BadJWTException MISSING_ISS_CLAIM_EXCEPTION = 070 new BadJWTException("Missing JWT issuer (iss) claim"); 071 072 073 /** 074 * Missing {@code sub} claim exception. 075 */ 076 private static final BadJWTException MISSING_SUB_CLAIM_EXCEPTION = 077 new BadJWTException("Missing JWT subject (sub) claim"); 078 079 080 /** 081 * Missing {@code aud} claim exception. 082 */ 083 private static final BadJWTException MISSING_AUD_CLAIM_EXCEPTION = 084 new BadJWTException("Missing JWT audience (aud) claim"); 085 086 087 /** 088 * Missing {@code nonce} claim exception. 089 */ 090 private static final BadJWTException MISSING_NONCE_CLAIM_EXCEPTION = 091 new BadJWTException("Missing JWT nonce (nonce) claim"); 092 093 094 /** 095 * Expired ID token exception. 096 */ 097 private static final BadJWTException EXPIRED_EXCEPTION = 098 new BadJWTException("Expired JWT"); 099 100 101 /** 102 * ID token issue time ahead of current time exception. 103 */ 104 private static final BadJWTException IAT_CLAIM_AHEAD_EXCEPTION = 105 new BadJWTException("JWT issue time ahead of current time"); 106 107 108 /** 109 * The expected ID token issuer. 110 */ 111 private final Issuer expectedIssuer; 112 113 114 /** 115 * The requesting client. 116 */ 117 private final ClientID expectedClientID; 118 119 120 /** 121 * The expected nonce, {@code null} if not required or specified. 122 */ 123 private final Nonce expectedNonce; 124 125 126 /** 127 * The maximum acceptable clock skew, in seconds. 128 */ 129 private int maxClockSkew; 130 131 132 /** 133 * Creates a new ID token claims verifier. 134 * 135 * @param issuer The expected ID token issuer. Must not be 136 * {@code null}. 137 * @param clientID The client ID. Must not be {@code null}. 138 * @param nonce The nonce, required in the implicit flow or for 139 * ID tokens returned by the authorisation endpoint 140 * int the hybrid flow. {@code null} if not 141 * required or specified. 142 * @param maxClockSkew The maximum acceptable clock skew (absolute 143 * value), in seconds. Must be zero (no clock skew) 144 * or positive integer. 145 */ 146 public IDTokenClaimsVerifier(final Issuer issuer, 147 final ClientID clientID, 148 final Nonce nonce, 149 final int maxClockSkew) { 150 151 if (issuer == null) { 152 throw new IllegalArgumentException("The expected ID token issuer must not be null"); 153 } 154 this.expectedIssuer = issuer; 155 156 if (clientID == null) { 157 throw new IllegalArgumentException("The client ID must not be null"); 158 } 159 this.expectedClientID = clientID; 160 161 this.expectedNonce = nonce; 162 163 setMaxClockSkew(maxClockSkew); 164 } 165 166 167 /** 168 * Returns the expected ID token issuer. 169 * 170 * @return The ID token issuer. 171 */ 172 public Issuer getExpectedIssuer() { 173 174 return expectedIssuer; 175 } 176 177 178 /** 179 * Returns the client ID for verifying the ID token audience. 180 * 181 * @return The client ID. 182 */ 183 public ClientID getClientID() { 184 185 return expectedClientID; 186 } 187 188 189 /** 190 * Returns the expected nonce. 191 * 192 * @return The nonce, {@code null} if not required or specified. 193 */ 194 public Nonce getExpectedNonce() { 195 196 return expectedNonce; 197 } 198 199 200 @Override 201 public int getMaxClockSkew() { 202 203 return maxClockSkew; 204 } 205 206 207 @Override 208 public void setMaxClockSkew(final int maxClockSkew) { 209 if (maxClockSkew < 0) { 210 throw new IllegalArgumentException("The max clock skew must be zero or positive"); 211 } 212 this.maxClockSkew = maxClockSkew; 213 } 214 215 216 @Override 217 public void verify(final JWTClaimsSet claimsSet) 218 throws BadJWTException { 219 220 // See http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation 221 222 final String tokenIssuer = claimsSet.getIssuer(); 223 224 if (tokenIssuer == null) { 225 throw MISSING_ISS_CLAIM_EXCEPTION; 226 } 227 228 if (! expectedIssuer.getValue().equals(tokenIssuer)) { 229 throw new BadJWTException("Unexpected JWT issuer: " + tokenIssuer); 230 } 231 232 if (claimsSet.getSubject() == null) { 233 throw MISSING_SUB_CLAIM_EXCEPTION; 234 } 235 236 final List<String> tokenAudience = claimsSet.getAudience(); 237 238 if (tokenAudience == null || tokenAudience.isEmpty()) { 239 throw MISSING_AUD_CLAIM_EXCEPTION; 240 } 241 242 if (! tokenAudience.contains(expectedClientID.getValue())) { 243 throw new BadJWTException("Unexpected JWT audience: " + tokenAudience); 244 } 245 246 247 if (tokenAudience.size() > 1) { 248 249 final String tokenAzp; 250 251 try { 252 tokenAzp = claimsSet.getStringClaim("azp"); 253 } catch (java.text.ParseException e) { 254 throw new BadJWTException("Invalid JWT authorized party (azp) claim: " + e.getMessage()); 255 } 256 257 if (tokenAzp != null) { 258 if (! expectedClientID.getValue().equals(tokenAzp)) { 259 throw new BadJWTException("Unexpected JWT authorized party (azp) claim: " + tokenAzp); 260 } 261 } 262 } 263 264 final Date exp = claimsSet.getExpirationTime(); 265 266 if (exp == null) { 267 throw MISSING_EXP_CLAIM_EXCEPTION; 268 } 269 270 final Date iat = claimsSet.getIssueTime(); 271 272 if (iat == null) { 273 throw MISSING_IAT_CLAIM_EXCEPTION; 274 } 275 276 277 final Date nowRef = new Date(); 278 279 // Expiration must be after current time, given acceptable clock skew 280 if (! DateUtils.isAfter(exp, nowRef, maxClockSkew)) { 281 throw EXPIRED_EXCEPTION; 282 } 283 284 // Issue time must be after current time, given acceptable clock skew 285 if (! DateUtils.isBefore(iat, nowRef, maxClockSkew)) { 286 throw IAT_CLAIM_AHEAD_EXCEPTION; 287 } 288 289 290 if (expectedNonce != null) { 291 292 final String tokenNonce; 293 294 try { 295 tokenNonce = claimsSet.getStringClaim("nonce"); 296 } catch (java.text.ParseException e) { 297 throw new BadJWTException("Invalid JWT nonce (nonce) claim: " + e.getMessage()); 298 } 299 300 if (tokenNonce == null) { 301 throw MISSING_NONCE_CLAIM_EXCEPTION; 302 } 303 304 if (! expectedNonce.getValue().equals(tokenNonce)) { 305 throw new BadJWTException("Unexpected JWT nonce (nonce) claim: " + tokenNonce); 306 } 307 } 308 } 309}