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                try {
185                        // The optional "azp" claim is only checked for being
186                        // a string, extension-specific "azp" checks should be
187                        // subclassed or performed elsewhere.
188                        claimsSet.getStringClaim("azp");
189                } catch (java.text.ParseException e) {
190                        throw new BadJWTException("Invalid JWT authorized party (azp) claim: " + e.getMessage());
191                }
192
193                final Date exp = claimsSet.getExpirationTime();
194
195                if (exp == null) {
196                        throw BadJWTExceptions.MISSING_EXP_CLAIM_EXCEPTION;
197                }
198
199                final Date iat = claimsSet.getIssueTime();
200
201                if (iat == null) {
202                        throw BadJWTExceptions.MISSING_IAT_CLAIM_EXCEPTION;
203                }
204
205
206                final Date nowRef = new Date();
207
208                // Expiration must be after current time, given acceptable clock skew
209                if (! DateUtils.isAfter(exp, nowRef, maxClockSkew)) {
210                        throw BadJWTExceptions.EXPIRED_EXCEPTION;
211                }
212
213                // Issue time must be before current time, given acceptable clock skew, or equal to current time
214                if (! (iat.equals(nowRef) || DateUtils.isBefore(iat, nowRef, maxClockSkew))) {
215                        throw BadJWTExceptions.IAT_CLAIM_AHEAD_EXCEPTION;
216                }
217
218
219                if (expectedNonce != null) {
220
221                        final String tokenNonce;
222
223                        try {
224                                tokenNonce = claimsSet.getStringClaim("nonce");
225                        } catch (java.text.ParseException e) {
226                                throw new BadJWTException("Invalid JWT nonce (nonce) claim: " + e.getMessage());
227                        }
228
229                        if (tokenNonce == null) {
230                                throw BadJWTExceptions.MISSING_NONCE_CLAIM_EXCEPTION;
231                        }
232
233                        if (! expectedNonce.getValue().equals(tokenNonce)) {
234                                throw new BadJWTException("Unexpected JWT nonce (nonce) claim: " + tokenNonce);
235                        }
236                }
237        }
238}