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