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}