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}