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