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}