001package com.nimbusds.openid.connect.sdk.validators;
002
003
004import java.util.Date;
005import java.util.List;
006
007import com.nimbusds.jwt.JWTClaimsSet;
008import com.nimbusds.jwt.proc.BadJWTException;
009import com.nimbusds.jwt.proc.ClockSkewAware;
010import com.nimbusds.jwt.proc.JWTClaimsVerifier;
011import com.nimbusds.jwt.util.DateUtils;
012import com.nimbusds.oauth2.sdk.id.ClientID;
013import com.nimbusds.oauth2.sdk.id.Issuer;
014import com.nimbusds.openid.connect.sdk.Nonce;
015import net.jcip.annotations.ThreadSafe;
016
017
018/**
019 * ID token claims verifier.
020 *
021 * <p>Related specifications:
022 *
023 * <ul>
024 *     <li>OpenID Connect Core 1.0, section 3.1.3.7 for code flow.
025 *     <li>OpenID Connect Core 1.0, section 3.2.2.11 for implicit flow.
026 *     <li>OpenID Connect Core 1.0, sections 3.3.2.12 and 3.3.3.7 for hybrid
027 *         flow.
028 * </ul>
029 */
030@ThreadSafe
031public class IDTokenClaimsVerifier implements JWTClaimsVerifier, ClockSkewAware {
032
033
034        // Cache general exceptions
035        /**
036         * Missing {@code exp} claim exception.
037         */
038        private static final BadJWTException MISSING_EXP_CLAIM_EXCEPTION =
039                new BadJWTException("Missing JWT expiration (exp) claim");
040
041
042        /**
043         * Missing {@code iat} claim exception.
044         */
045        private static final BadJWTException MISSING_IAT_CLAIM_EXCEPTION =
046                new BadJWTException("Missing JWT issue time (iat) claim");
047
048
049        /**
050         * Missing {@code iss} claim exception.
051         */
052        private static final BadJWTException MISSING_ISS_CLAIM_EXCEPTION =
053                new BadJWTException("Missing JWT issuer (iss) claim");
054
055
056        /**
057         * Missing {@code sub} claim exception.
058         */
059        private static final BadJWTException MISSING_SUB_CLAIM_EXCEPTION =
060                new BadJWTException("Missing JWT subject (sub) claim");
061
062
063        /**
064         * Missing {@code aud} claim exception.
065         */
066        private static final BadJWTException MISSING_AUD_CLAIM_EXCEPTION =
067                new BadJWTException("Missing JWT audience (aud) claim");
068
069
070        /**
071         * Missing {@code nonce} claim exception.
072         */
073        private static final BadJWTException MISSING_NONCE_CLAIM_EXCEPTION =
074                new BadJWTException("Missing JWT nonce (nonce) claim");
075
076
077        /**
078         * Expired ID token exception.
079         */
080        private static final BadJWTException EXPIRED_EXCEPTION =
081                new BadJWTException("Expired JWT");
082
083
084        /**
085         * ID token issue time ahead of current time exception.
086         */
087        private static final BadJWTException IAT_CLAIM_AHEAD_EXCEPTION =
088                new BadJWTException("JWT issue time ahead of current time");
089        
090
091        /**
092         * The expected ID token issuer.
093         */
094        private final Issuer expectedIssuer;
095
096
097        /**
098         * The requesting client.
099         */
100        private final ClientID expectedClientID;
101
102
103        /**
104         * The expected nonce, {@code null} if not required or specified.
105         */
106        private final Nonce expectedNonce;
107
108
109        /**
110         * The maximum acceptable clock skew, in seconds.
111         */
112        private int maxClockSkew;
113
114
115        /**
116         * Creates a new ID token claims verifier.
117         *
118         * @param issuer       The expected ID token issuer. Must not be
119         *                     {@code null}.
120         * @param clientID     The client ID. Must not be {@code null}.
121         * @param nonce        The nonce, required in the implicit flow or for
122         *                     ID tokens returned by the authorisation endpoint
123         *                     int the hybrid flow. {@code null} if not
124         *                     required or specified.
125         * @param maxClockSkew The maximum acceptable clock skew (absolute
126         *                     value), in seconds. Must be zero (no clock skew)
127         *                     or positive integer.
128         */
129        public IDTokenClaimsVerifier(final Issuer issuer,
130                                     final ClientID clientID,
131                                     final Nonce nonce,
132                                     final int maxClockSkew) {
133
134                if (issuer == null) {
135                        throw new IllegalArgumentException("The expected ID token issuer must not be null");
136                }
137                this.expectedIssuer = issuer;
138
139                if (clientID == null) {
140                        throw new IllegalArgumentException("The client ID must not be null");
141                }
142                this.expectedClientID = clientID;
143
144                this.expectedNonce = nonce;
145
146                setMaxClockSkew(maxClockSkew);
147        }
148
149
150        /**
151         * Returns the expected ID token issuer.
152         *
153         * @return The ID token issuer.
154         */
155        public Issuer getExpectedIssuer() {
156
157                return expectedIssuer;
158        }
159
160
161        /**
162         * Returns the client ID for verifying the ID token audience.
163         *
164         * @return The client ID.
165         */
166        public ClientID getClientID() {
167
168                return expectedClientID;
169        }
170
171
172        /**
173         * Returns the expected nonce.
174         *
175         * @return The nonce, {@code null} if not required or specified.
176         */
177        public Nonce getExpectedNonce() {
178
179                return expectedNonce;
180        }
181
182
183        @Override
184        public int getMaxClockSkew() {
185
186                return maxClockSkew;
187        }
188
189
190        @Override
191        public void setMaxClockSkew(final int maxClockSkew) {
192                if (maxClockSkew < 0) {
193                        throw new IllegalArgumentException("The max clock skew must be zero or positive");
194                }
195                this.maxClockSkew = maxClockSkew;
196        }
197
198
199        @Override
200        public void verify(final JWTClaimsSet claimsSet)
201                throws BadJWTException {
202
203                // See http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
204
205                final String tokenIssuer = claimsSet.getIssuer();
206
207                if (tokenIssuer == null) {
208                        throw MISSING_ISS_CLAIM_EXCEPTION;
209                }
210
211                if (! expectedIssuer.getValue().equals(tokenIssuer)) {
212                        throw new BadJWTException("Unexpected JWT issuer: " + tokenIssuer);
213                }
214
215                if (claimsSet.getSubject() == null) {
216                        throw MISSING_SUB_CLAIM_EXCEPTION;
217                }
218
219                final List<String> tokenAudience = claimsSet.getAudience();
220
221                if (tokenAudience == null || tokenAudience.isEmpty()) {
222                        throw MISSING_AUD_CLAIM_EXCEPTION;
223                }
224
225                if (! tokenAudience.contains(expectedClientID.getValue())) {
226                        throw new BadJWTException("Unexpected JWT audience: " + tokenAudience);
227                }
228
229
230                if (tokenAudience.size() > 1) {
231
232                        final String tokenAzp;
233
234                        try {
235                                tokenAzp = claimsSet.getStringClaim("azp");
236                        } catch (java.text.ParseException e) {
237                                throw new BadJWTException("Invalid JWT authorized party (azp) claim: " + e.getMessage());
238                        }
239
240                        if (tokenAzp != null) {
241                                if (! expectedClientID.getValue().equals(tokenAzp)) {
242                                        throw new BadJWTException("Unexpected JWT authorized party (azp) claim: " + tokenAzp);
243                                }
244                        }
245                }
246
247                final Date exp = claimsSet.getExpirationTime();
248
249                if (exp == null) {
250                        throw MISSING_EXP_CLAIM_EXCEPTION;
251                }
252
253                final Date iat = claimsSet.getIssueTime();
254
255                if (iat == null) {
256                        throw MISSING_IAT_CLAIM_EXCEPTION;
257                }
258
259
260                final Date nowRef = new Date();
261
262                // Expiration must be after current time, given acceptable clock skew
263                if (! DateUtils.isAfter(exp, nowRef, maxClockSkew)) {
264                        throw EXPIRED_EXCEPTION;
265                }
266
267                // Issue time must be after current time, given acceptable clock skew
268                if (! DateUtils.isBefore(iat, nowRef, maxClockSkew)) {
269                        throw IAT_CLAIM_AHEAD_EXCEPTION;
270                }
271
272
273                if (expectedNonce != null) {
274
275                        final String tokenNonce;
276
277                        try {
278                                tokenNonce = claimsSet.getStringClaim("nonce");
279                        } catch (java.text.ParseException e) {
280                                throw new BadJWTException("Invalid JWT nonce (nonce) claim: " + e.getMessage());
281                        }
282
283                        if (tokenNonce == null) {
284                                throw MISSING_NONCE_CLAIM_EXCEPTION;
285                        }
286
287                        if (! expectedNonce.getValue().equals(tokenNonce)) {
288                                throw new BadJWTException("Unexpected JWT nonce (nonce) claim: " + tokenNonce);
289                        }
290                }
291        }
292}