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