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}