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