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