001package com.nimbusds.oauth2.sdk.auth.verifier;
002
003
004import java.security.PublicKey;
005import java.util.List;
006import java.util.Set;
007
008import com.nimbusds.jose.JOSEException;
009import com.nimbusds.jose.JWSVerifier;
010import com.nimbusds.jose.crypto.MACVerifier;
011import com.nimbusds.jose.crypto.factories.DefaultJWSVerifierFactory;
012import com.nimbusds.jose.proc.JWSVerifierFactory;
013import com.nimbusds.jwt.SignedJWT;
014import com.nimbusds.jwt.proc.BadJWTException;
015import com.nimbusds.oauth2.sdk.auth.*;
016import com.nimbusds.oauth2.sdk.id.Audience;
017import net.jcip.annotations.ThreadSafe;
018import org.apache.commons.collections4.CollectionUtils;
019
020
021/**
022 * Client authentication verifier.
023 *
024 * <p>Related specifications:
025 *
026 * <ul>
027 *     <li>OAuth 2.0 (RFC 6749), sections 2.3.1 and 3.2.1.
028 *     <li>OpenID Connect Core 1.0, section 9.
029 *     <li>JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and
030 *         Authorization Grants (RFC 7523).
031 * </ul>
032 */
033@ThreadSafe
034public class ClientAuthenticationVerifier<T> {
035
036
037        /**
038         * The client credentials selector.
039         */
040        private final ClientCredentialsSelector<T> clientCredentialsSelector;
041
042
043        /**
044         * The JWT assertion claims set verifier.
045         */
046        private final JWTAuthenticationClaimsSetVerifier claimsSetVerifier;
047
048
049        /**
050         * JWS verifier factory for private_key_jwt authentication.
051         */
052        private final JWSVerifierFactory jwsVerifierFactory = new DefaultJWSVerifierFactory();
053
054
055        /**
056         * Creates a new client authentication verifier.
057         *
058         * @param clientCredentialsSelector The client credentials selector.
059         *                                  Must not be {@code null}.
060         * @param expectedAudience          The permitted audience (aud) claim
061         *                                  values in JWT authentication
062         *                                  assertions. Must not be empty or
063         *                                  {@code null}. Should typically
064         *                                  contain the token endpoint URI and
065         *                                  for OpenID provider it may also
066         *                                  include the issuer URI.
067         */
068        public ClientAuthenticationVerifier(final ClientCredentialsSelector<T> clientCredentialsSelector,
069                                            final Set<Audience> expectedAudience) {
070
071                claimsSetVerifier = new JWTAuthenticationClaimsSetVerifier(expectedAudience);
072
073                if (clientCredentialsSelector == null) {
074                        throw new IllegalArgumentException("The client credentials selector must not be null");
075                }
076
077                this.clientCredentialsSelector = clientCredentialsSelector;
078        }
079
080
081        /**
082         * Returns the client credentials selector.
083         *
084         * @return The client credentials selector.
085         */
086        public ClientCredentialsSelector<T> getClientCredentialsSelector() {
087
088                return clientCredentialsSelector;
089        }
090
091
092        /**
093         * Returns the permitted audience values in JWT authentication
094         * assertions.
095         *
096         * @return The permitted audience (aud) claim values.
097         */
098        public Set<Audience> getExpectedAudience() {
099
100                return claimsSetVerifier.getExpectedAudience();
101        }
102
103
104        /**
105         * Verifies a client authentication request.
106         *
107         * @param clientAuth The client authentication. Must not be
108         *                   {@code null}.
109         * @param hints      Optional hints to the verifier, empty set of
110         *                   {@code null} if none.
111         * @param context    Additional context to be passed to the client
112         *                   credentials selector. May be {@code null}.
113         *
114         * @throws InvalidClientException If the client authentication is
115         *                                invalid, typically due to bad
116         *                                credentials.
117         * @throws JOSEException          If authentication failed due to an
118         *                                internal JOSE / JWT processing
119         *                                exception.
120         */
121        public void verify(final ClientAuthentication clientAuth, final Set<Hint> hints, final Context<T> context)
122                throws InvalidClientException, JOSEException {
123
124                if (clientAuth instanceof PlainClientSecret) {
125
126                        List<Secret> secretCandidates = clientCredentialsSelector.selectClientSecrets(
127                                clientAuth.getClientID(),
128                                clientAuth.getMethod(),
129                                context);
130
131                        if (CollectionUtils.isEmpty(secretCandidates)) {
132                                throw InvalidClientException.NO_REGISTERED_SECRET;
133                        }
134
135                        PlainClientSecret plainAuth = (PlainClientSecret)clientAuth;
136
137                        for (Secret candidate: secretCandidates) {
138                                if (plainAuth.getClientSecret().equals(candidate)) {
139                                        return; // success
140                                }
141                        }
142
143                        throw InvalidClientException.BAD_SECRET;
144
145                } else if (clientAuth instanceof ClientSecretJWT) {
146
147                        ClientSecretJWT jwtAuth = (ClientSecretJWT) clientAuth;
148
149                        // Check claims first before requesting secret from backend
150                        try {
151                                claimsSetVerifier.verify(jwtAuth.getJWTAuthenticationClaimsSet().toJWTClaimsSet());
152                        } catch (BadJWTException e) {
153                                throw InvalidClientException.BAD_JWT_CLAIMS;
154                        }
155
156                        List<Secret> secretCandidates = clientCredentialsSelector.selectClientSecrets(
157                                clientAuth.getClientID(),
158                                clientAuth.getMethod(),
159                                context);
160
161                        if (CollectionUtils.isEmpty(secretCandidates)) {
162                                throw InvalidClientException.NO_REGISTERED_SECRET;
163                        }
164
165                        SignedJWT assertion = jwtAuth.getClientAssertion();
166
167                        for (Secret candidate : secretCandidates) {
168
169                                boolean valid = assertion.verify(new MACVerifier(candidate.getValueBytes()));
170
171                                if (valid) {
172                                        return; // success
173                                }
174                        }
175
176                        throw InvalidClientException.BAD_JWT_HMAC;
177
178                } else if (clientAuth instanceof PrivateKeyJWT) {
179
180                        PrivateKeyJWT jwtAuth = (PrivateKeyJWT)clientAuth;
181
182                        // Check claims first before requesting / retrieving public keys
183                        try {
184                                claimsSetVerifier.verify(jwtAuth.getJWTAuthenticationClaimsSet().toJWTClaimsSet());
185                        } catch (BadJWTException e) {
186                                throw InvalidClientException.BAD_JWT_CLAIMS;
187                        }
188
189                        List<? extends PublicKey> keyCandidates = clientCredentialsSelector.selectPublicKeys(
190                                jwtAuth.getClientID(),
191                                jwtAuth.getMethod(),
192                                jwtAuth.getClientAssertion().getHeader(),
193                                false,  // don't force refresh if we have a remote JWK set;
194                                        // selector may however do so if it encounters an unknown key ID
195                                context);
196
197                        if (CollectionUtils.isEmpty(keyCandidates)) {
198                                throw InvalidClientException.NO_MATCHING_JWK;
199                        }
200
201                        SignedJWT assertion = jwtAuth.getClientAssertion();
202
203                        for (PublicKey candidate: keyCandidates) {
204
205                                if (candidate == null) {
206                                        continue; // skip
207                                }
208
209                                JWSVerifier jwsVerifier = jwsVerifierFactory.createJWSVerifier(
210                                        jwtAuth.getClientAssertion().getHeader(),
211                                        candidate);
212
213                                boolean valid = assertion.verify(jwsVerifier);
214
215                                if (valid) {
216                                        return; // success
217                                }
218                        }
219
220                        // Second pass
221                        if (hints != null && hints.contains(Hint.CLIENT_HAS_REMOTE_JWK_SET)) {
222                                // Client possibly registered JWK set URL with keys that have no IDs
223                                // force JWK set reload from URL and retry
224                                keyCandidates = clientCredentialsSelector.selectPublicKeys(
225                                        jwtAuth.getClientID(),
226                                        jwtAuth.getMethod(),
227                                        jwtAuth.getClientAssertion().getHeader(),
228                                        true, // force reload of remote JWK set
229                                        context);
230
231                                if (CollectionUtils.isEmpty(keyCandidates)) {
232                                        throw InvalidClientException.NO_MATCHING_JWK;
233                                }
234
235                                assertion = jwtAuth.getClientAssertion();
236
237                                for (PublicKey candidate: keyCandidates) {
238
239                                        if (candidate == null) {
240                                                continue; // skip
241                                        }
242
243                                        JWSVerifier jwsVerifier = jwsVerifierFactory.createJWSVerifier(
244                                                jwtAuth.getClientAssertion().getHeader(),
245                                                candidate);
246
247                                        boolean valid = assertion.verify(jwsVerifier);
248
249                                        if (valid) {
250                                                return; // success
251                                        }
252                                }
253                        }
254
255                        throw InvalidClientException.BAD_JWT_SIGNATURE;
256
257                } else {
258                        throw new RuntimeException("Unexpected client authentication: " + clientAuth.getMethod());
259                }
260        }
261}