001package com.nimbusds.oauth2.sdk.assertions.jwt;
002
003
004import java.util.Set;
005
006import com.nimbusds.jwt.JWTClaimsSet;
007import com.nimbusds.jwt.proc.BadJWTException;
008import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier;
009import com.nimbusds.oauth2.sdk.id.Audience;
010import net.jcip.annotations.Immutable;
011import org.apache.commons.collections4.CollectionUtils;
012
013
014/**
015 * JSON Web Token (JWT) bearer assertion details (claims set) verifier for
016 * OAuth 2.0 client authentication and authorisation grants. Intended for
017 * initial validation of JWT assertions:
018 *
019 * <ul>
020 *     <li>Audience check
021 *     <li>Expiration time check
022 *     <li>Not-before time check (is set)
023 *     <li>Subject and issuer presence check
024 * </ul>
025 *
026 * <p>Related specifications:
027 *
028 * <ul>
029 *     <li>JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and
030 *         Authorization Grants (RFC 7523).
031 * </ul>
032 */
033@Immutable
034public class JWTAssertionDetailsVerifier extends DefaultJWTClaimsVerifier {
035
036
037        // Cache JWT exceptions for quick processing of bad claims sets
038
039
040        /**
041         * Missing JWT expiration claim.
042         */
043        private static final BadJWTException MISSING_EXP_CLAIM_EXCEPTION =
044                new BadJWTException("Missing JWT expiration claim");
045
046
047        /**
048         * Missing JWT audience claim.
049         */
050        private static final BadJWTException MISSING_AUD_CLAIM_EXCEPTION =
051                new BadJWTException("Missing JWT audience claim");
052
053
054        /**
055         * Missing JWT subject claim.
056         */
057        private static final BadJWTException MISSING_SUB_CLAIM_EXCEPTION =
058                new BadJWTException("Missing JWT subject claim");
059
060
061        /**
062         * Missing JWT issuer claim.
063         */
064        private static final BadJWTException MISSING_ISS_CLAIM_EXCEPTION =
065                new BadJWTException("Missing JWT issuer claim");
066
067
068        /**
069         * The expected audience.
070         */
071        private final Set<Audience> expectedAudience;
072
073
074        /**
075         * Cached unexpected JWT audience claim exception.
076         */
077        private final BadJWTException unexpectedAudClaimException;
078
079
080        /**
081         * Creates a new JWT bearer assertion details (claims set) verifier.
082         *
083         * @param expectedAudience The expected audience (aud) claim values.
084         *                         Must not be empty or {@code null}. Should
085         *                         typically contain the token endpoint URI and
086         *                         for OpenID provider it may also include the
087         *                         issuer URI.
088         */
089        public JWTAssertionDetailsVerifier(final Set<Audience> expectedAudience) {
090
091                if (CollectionUtils.isEmpty(expectedAudience)) {
092                        throw new IllegalArgumentException("The expected audience set must not be null or empty");
093                }
094
095                this.expectedAudience = expectedAudience;
096
097                unexpectedAudClaimException = new BadJWTException("Invalid JWT audience claim, expected " + expectedAudience);
098        }
099
100
101        /**
102         * Returns the expected audience values.
103         *
104         * @return The expected audience (aud) claim values.
105         */
106        public Set<Audience> getExpectedAudience() {
107
108                return expectedAudience;
109        }
110
111
112        @Override
113        public void verify(final JWTClaimsSet claimsSet)
114                throws BadJWTException {
115
116                super.verify(claimsSet);
117
118                if (claimsSet.getExpirationTime() == null) {
119                        throw MISSING_EXP_CLAIM_EXCEPTION;
120                }
121
122                if (claimsSet.getAudience() == null || claimsSet.getAudience().isEmpty()) {
123                        throw MISSING_AUD_CLAIM_EXCEPTION;
124                }
125
126                boolean audMatch = false;
127
128                for (String aud: claimsSet.getAudience()) {
129
130                        if (aud == null || aud.isEmpty()) {
131                                continue; // skip
132                        }
133
134                        if (expectedAudience.contains(new Audience(aud))) {
135                                audMatch = true;
136                        }
137                }
138
139                if (! audMatch) {
140                        throw unexpectedAudClaimException;
141                }
142
143                if (claimsSet.getIssuer() == null) {
144                        throw MISSING_ISS_CLAIM_EXCEPTION;
145                }
146
147                if (claimsSet.getSubject() == null) {
148                        throw MISSING_SUB_CLAIM_EXCEPTION;
149                }
150        }
151}