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                                if (plainAuth.getClientSecret().equals(candidate)) {
156                                        return; // success
157                                }
158                        }
159
160                        throw InvalidClientException.BAD_SECRET;
161
162                } else if (clientAuth instanceof ClientSecretJWT) {
163
164                        ClientSecretJWT jwtAuth = (ClientSecretJWT) clientAuth;
165
166                        // Check claims first before requesting secret from backend
167                        try {
168                                claimsSetVerifier.verify(jwtAuth.getJWTAuthenticationClaimsSet().toJWTClaimsSet());
169                        } catch (BadJWTException e) {
170                                throw new InvalidClientException("Bad / expired JWT claims: " + e.getMessage());
171                        }
172
173                        List<Secret> secretCandidates = clientCredentialsSelector.selectClientSecrets(
174                                clientAuth.getClientID(),
175                                clientAuth.getMethod(),
176                                context);
177
178                        if (CollectionUtils.isEmpty(secretCandidates)) {
179                                throw InvalidClientException.NO_REGISTERED_SECRET;
180                        }
181
182                        SignedJWT assertion = jwtAuth.getClientAssertion();
183
184                        for (Secret candidate : secretCandidates) {
185
186                                boolean valid = assertion.verify(new MACVerifier(candidate.getValueBytes()));
187
188                                if (valid) {
189                                        return; // success
190                                }
191                        }
192
193                        throw InvalidClientException.BAD_JWT_HMAC;
194
195                } else if (clientAuth instanceof PrivateKeyJWT) {
196
197                        PrivateKeyJWT jwtAuth = (PrivateKeyJWT)clientAuth;
198
199                        // Check claims first before requesting / retrieving public keys
200                        try {
201                                claimsSetVerifier.verify(jwtAuth.getJWTAuthenticationClaimsSet().toJWTClaimsSet());
202                        } catch (BadJWTException e) {
203                                throw new InvalidClientException("Bad / expired JWT claims: " + e.getMessage());
204                        }
205
206                        List<? extends PublicKey> keyCandidates = clientCredentialsSelector.selectPublicKeys(
207                                jwtAuth.getClientID(),
208                                jwtAuth.getMethod(),
209                                jwtAuth.getClientAssertion().getHeader(),
210                                false,  // don't force refresh if we have a remote JWK set;
211                                        // selector may however do so if it encounters an unknown key ID
212                                context);
213
214                        if (CollectionUtils.isEmpty(keyCandidates)) {
215                                throw InvalidClientException.NO_MATCHING_JWK;
216                        }
217
218                        SignedJWT assertion = jwtAuth.getClientAssertion();
219
220                        for (PublicKey candidate: keyCandidates) {
221
222                                if (candidate == null) {
223                                        continue; // skip
224                                }
225
226                                JWSVerifier jwsVerifier = jwsVerifierFactory.createJWSVerifier(
227                                        jwtAuth.getClientAssertion().getHeader(),
228                                        candidate);
229
230                                boolean valid = assertion.verify(jwsVerifier);
231
232                                if (valid) {
233                                        return; // success
234                                }
235                        }
236
237                        // Second pass
238                        if (hints != null && hints.contains(Hint.CLIENT_HAS_REMOTE_JWK_SET)) {
239                                // Client possibly registered JWK set URL with keys that have no IDs
240                                // force JWK set reload from URL and retry
241                                keyCandidates = clientCredentialsSelector.selectPublicKeys(
242                                        jwtAuth.getClientID(),
243                                        jwtAuth.getMethod(),
244                                        jwtAuth.getClientAssertion().getHeader(),
245                                        true, // force reload of remote JWK set
246                                        context);
247
248                                if (CollectionUtils.isEmpty(keyCandidates)) {
249                                        throw InvalidClientException.NO_MATCHING_JWK;
250                                }
251
252                                assertion = jwtAuth.getClientAssertion();
253
254                                for (PublicKey candidate: keyCandidates) {
255
256                                        if (candidate == null) {
257                                                continue; // skip
258                                        }
259
260                                        JWSVerifier jwsVerifier = jwsVerifierFactory.createJWSVerifier(
261                                                jwtAuth.getClientAssertion().getHeader(),
262                                                candidate);
263
264                                        boolean valid = assertion.verify(jwsVerifier);
265
266                                        if (valid) {
267                                                return; // success
268                                        }
269                                }
270                        }
271
272                        throw InvalidClientException.BAD_JWT_SIGNATURE;
273
274                } else {
275                        throw new RuntimeException("Unexpected client authentication: " + clientAuth.getMethod());
276                }
277        }
278}