001/*
002 * nimbus-jose-jwt
003 *
004 * Copyright 2012-2016, Connect2id Ltd.
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.jwt.proc;
019
020
021import java.util.*;
022
023import net.jcip.annotations.ThreadSafe;
024
025import com.nimbusds.jose.proc.SecurityContext;
026import com.nimbusds.jwt.JWTClaimsSet;
027import com.nimbusds.jwt.util.DateUtils;
028
029
030/**
031 * {@link JWTClaimsSetVerifier JWT claims verifier} implementation. This class
032 * is thread-safe.
033 *
034 * <p>Performs the following checks:
035 *
036 * <ol>
037 *     <li>If an expiration time (exp) claim is present, makes sure it is ahead
038 *         of the current time, else the JWT claims set is rejected.
039 *     <li>If a not-before-time (nbf) claim is present, makes sure it is
040 *         before the current time, else the JWT claims set is rejected.
041 * </ol>
042 *
043 * <p>This class may be extended to perform additional checks.
044 *
045 * @author Vladimir Dzhuvinov
046 * @version 2019-10-17
047 */
048@ThreadSafe
049public class DefaultJWTClaimsVerifier <C extends SecurityContext> implements JWTClaimsSetVerifier<C>, JWTClaimsVerifier, ClockSkewAware {
050
051
052        /**
053         * The default maximum acceptable clock skew, in seconds (60).
054         */
055        public static final int DEFAULT_MAX_CLOCK_SKEW_SECONDS = 60;
056
057
058        /**
059         * The maximum acceptable clock skew, in seconds.
060         */
061        private int maxClockSkew = DEFAULT_MAX_CLOCK_SKEW_SECONDS;
062        
063        
064        /**
065         * The accepted audience values, {@code null} if not specified. A
066         * {@code null} value present in the set allows JWTs with no audience.
067         */
068        private final Set<String> acceptedAudienceValues;
069        
070        
071        /**
072         * The JWT claims that must match exactly, empty set if none.
073         */
074        private final JWTClaimsSet exactMatchClaims;
075        
076        
077        /**
078         * The names of the JWT claims that must be present, empty set if none.
079         */
080        private final Set<String> requiredClaims;
081        
082        
083        /**
084         * The names of the JWT claims that must not be present, empty set if
085         * none.
086         */
087        private final Set<String> prohibitedClaims;
088        
089        
090        /**
091         * Creates a new JWT claims verifier. No audience ("aud"), required and
092         * prohibited claims are specified. Will check the expiration ("exp")
093         * and not-before ("nbf") times if present.
094         */
095        public DefaultJWTClaimsVerifier() {
096                this(null, null, null, null);
097        }
098        
099        
100        /**
101         * Creates a new JWT claims verifier. Allows any audience ("aud")
102         * unless an exact match is specified. Will check the expiration
103         * ("exp") and not-before ("nbf") times if present.
104         *
105         * @param exactMatchClaims The JWT claims that must match exactly,
106         *                         {@code null} if none.
107         * @param requiredClaims   The names of the JWT claims that must be
108         *                         present, empty set or {@code null} if none.
109         */
110        public DefaultJWTClaimsVerifier(final JWTClaimsSet exactMatchClaims,
111                                        final Set<String> requiredClaims) {
112                
113                this(null, exactMatchClaims, requiredClaims, null);
114        }
115        
116        
117        /**
118         * Creates new default JWT claims verifier.
119         *
120         * @param requiredAudience The required JWT audience, {@code null} if
121         *                         not specified.
122         * @param exactMatchClaims The JWT claims that must match exactly,
123         *                         {@code null} if none.
124         * @param requiredClaims   The names of the JWT claims that must be
125         *                         present, empty set or {@code null} if none.
126         */
127        public DefaultJWTClaimsVerifier(final String requiredAudience,
128                                        final JWTClaimsSet exactMatchClaims,
129                                        final Set<String> requiredClaims) {
130                
131                this(requiredAudience != null ? Collections.singleton(requiredAudience) : null,
132                        exactMatchClaims,
133                        requiredClaims,
134                        null);
135        }
136        
137        
138        /**
139         * Creates new default JWT claims verifier.
140         *
141         * @param acceptedAudience The accepted JWT audience values,
142         *                         {@code null} if not specified. A
143         *                         {@code null} value in the set allows JWTs
144         *                         with no audience.
145         * @param exactMatchClaims The JWT claims that must match exactly,
146         *                         {@code null} if none.
147         * @param requiredClaims   The names of the JWT claims that must be
148         *                         present, empty set or {@code null} if none.
149         * @param prohibitedClaims The names of the JWT claims that must not be
150         *                         present, empty set or {@code null} if none.
151         */
152        public DefaultJWTClaimsVerifier(final Set<String> acceptedAudience,
153                                        final JWTClaimsSet exactMatchClaims,
154                                        final Set<String> requiredClaims,
155                                        final Set<String> prohibitedClaims) {
156                
157                this.acceptedAudienceValues = acceptedAudience != null ? Collections.unmodifiableSet(acceptedAudience) : null;
158                
159                this.exactMatchClaims = exactMatchClaims != null ? exactMatchClaims : new JWTClaimsSet.Builder().build();
160                
161                Set<String> requiredClaimsCopy = new HashSet<>(this.exactMatchClaims.getClaims().keySet());
162                if (acceptedAudienceValues != null && ! acceptedAudienceValues.contains(null)) {
163                        // check if an explicit aud is required
164                        requiredClaimsCopy.add("aud");
165                }
166                if (requiredClaims != null) {
167                        requiredClaimsCopy.addAll(requiredClaims);
168                }
169                this.requiredClaims = Collections.unmodifiableSet(requiredClaimsCopy);
170                
171                this.prohibitedClaims = prohibitedClaims != null ? Collections.unmodifiableSet(prohibitedClaims) : Collections.<String>emptySet();
172        }
173        
174        
175        /**
176         * Returns the accepted audience values.
177         *
178         * @return The accepted JWT audience values, {@code null} if not
179         *         specified. A {@code null} value in the set allows JWTs with
180         *         no audience.
181         */
182        public Set<String> getAcceptedAudienceValues() {
183                return acceptedAudienceValues;
184        }
185        
186        
187        /**
188         * Returns the JWT claims that must match exactly.
189         *
190         * @return The JWT claims that must match exactly, empty set if none.
191         */
192        public JWTClaimsSet getExactMatchClaims() {
193                return exactMatchClaims;
194        }
195        
196        
197        /**
198         * Returns the names of the JWT claims that must be present, including
199         * the name of those that must match exactly.
200         *
201         * @return The names of the JWT claims that must be present, empty set
202         *         if none.
203         */
204        public Set<String> getRequiredClaims() {
205                return requiredClaims;
206        }
207        
208        
209        /**
210         * Returns the names of the JWT claims that must not be present.
211         *
212         * @return The names of the JWT claims that must not be present, empty
213         *         set if none.
214         */
215        public Set<String> getProhibitedClaims() {
216                return prohibitedClaims;
217        }
218        
219        
220        @Override
221        public int getMaxClockSkew() {
222                return maxClockSkew;
223        }
224
225
226        @Override
227        public void setMaxClockSkew(final int maxClockSkewSeconds) {
228                maxClockSkew = maxClockSkewSeconds;
229        }
230        
231        
232        @Override
233        public void verify(final JWTClaimsSet claimsSet)
234                throws BadJWTException {
235
236                verify(claimsSet, null);
237        }
238        
239        
240        @Override
241        public void verify(final JWTClaimsSet claimsSet, final C context)
242                throws BadJWTException {
243                
244                // Check audience
245                if (acceptedAudienceValues != null) {
246                        List<String> audList = claimsSet.getAudience();
247                        if (audList != null && ! audList.isEmpty()) {
248                                boolean audMatch = false;
249                                for (String aud : audList) {
250                                        if (acceptedAudienceValues.contains(aud)) {
251                                                audMatch = true;
252                                                break;
253                                        }
254                                }
255                                if (! audMatch) {
256                                        throw new BadJWTException("JWT audience rejected: " + audList);
257                                }
258                        } else if (! acceptedAudienceValues.contains(null)) {
259                                throw new BadJWTException("JWT missing required audience");
260                        }
261                }
262                
263                // Check if all required claims are present
264                if (! claimsSet.getClaims().keySet().containsAll(requiredClaims)) {
265                        Set<String> missingClaims = new HashSet<>(requiredClaims);
266                        missingClaims.removeAll(claimsSet.getClaims().keySet());
267                        throw new BadJWTException("JWT missing required claims: " + missingClaims);
268                }
269                
270                // Check if prohibited claims are present
271                Set<String> presentProhibitedClaims = new HashSet<>();
272                for (String prohibited: prohibitedClaims) {
273                        if (claimsSet.getClaims().containsKey(prohibited)) {
274                                presentProhibitedClaims.add(prohibited);
275                        }
276                        if (! presentProhibitedClaims.isEmpty()) {
277                                throw new BadJWTException("JWT has prohibited claims: " + presentProhibitedClaims);
278                        }
279                }
280                
281                // Check exact matches
282                for (String exactMatch: exactMatchClaims.getClaims().keySet()) {
283                        Object actualClaim = claimsSet.getClaim(exactMatch);
284                        Object expectedClaim = exactMatchClaims.getClaim(exactMatch);
285                        if (! actualClaim.equals(expectedClaim)) {
286                                throw new BadJWTException("JWT \"" + exactMatch + "\" claim has value " + actualClaim + " but should be " + expectedClaim);
287                        }
288                }
289                
290                // Check time window
291                final Date now = new Date();
292                
293                final Date exp = claimsSet.getExpirationTime();
294                if (exp != null) {
295                        
296                        if (! DateUtils.isAfter(exp, now, maxClockSkew)) {
297                                throw new BadJWTException("Expired JWT");
298                        }
299                }
300                
301                final Date nbf = claimsSet.getNotBeforeTime();
302                if (nbf != null) {
303                        
304                        if (! DateUtils.isBefore(nbf, now, maxClockSkew)) {
305                                throw new BadJWTException("JWT before use time");
306                        }
307                }
308        }
309}