001package com.nimbusds.oauth2.sdk.auth.verifier;
002
003
004import java.security.PublicKey;
005import java.util.List;
006import java.util.Set;
007
008import net.jcip.annotations.ThreadSafe;
009
010import com.nimbusds.jose.JOSEException;
011import com.nimbusds.jose.JWSVerifier;
012import com.nimbusds.jose.crypto.MACVerifier;
013import com.nimbusds.jose.crypto.factories.DefaultJWSVerifierFactory;
014import com.nimbusds.jose.proc.JWSVerifierFactory;
015import com.nimbusds.jwt.SignedJWT;
016import com.nimbusds.jwt.proc.BadJWTException;
017
018import com.nimbusds.oauth2.sdk.auth.*;
019import com.nimbusds.oauth2.sdk.id.Audience;
020
021
022/**
023 * Client authentication verifier.
024 *
025 * <p>Related specifications:
026 *
027 * <ul>
028 *     <li>OAuth 2.0 (RFC 6749), sections 2.3.1 and 3.2.1.
029 *     <li>OpenID Connect Core 1.0, section 9.
030 *     <li>JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and
031 *         Authorization Grants (RFC 7523).
032 * </ul>
033 */
034@ThreadSafe
035public class ClientAuthenticationVerifier<T> {
036
037
038        /**
039         * The client credentials selector.
040         */
041        private final ClientCredentialsSelector<T> clientCredentialsSelector;
042
043
044        /**
045         * The JWT assertion claims set verifier.
046         */
047        private final JWTAuthenticationClaimsSetVerifier claimsSetVerifier;
048
049
050        /**
051         * JWS verifier factory for private_key_jwt authentication.
052         */
053        private final JWSVerifierFactory jwsVerifierFactory = new DefaultJWSVerifierFactory();
054
055
056        /**
057         * Creates a new client authentication verifier.
058         *
059         * @param clientCredentialsSelector The client credentials selector.
060         *                                  Must not be {@code null}.
061         * @param expectedAudience          The permitted audience (aud) claim
062         *                                  values in JWT authentication
063         *                                  assertions. Must not be empty or
064         *                                  {@code null}. Should typically
065         *                                  contain the token endpoint URI and
066         *                                  for OpenID provider it may also
067         *                                  include the issuer URI.
068         */
069        public ClientAuthenticationVerifier(final ClientCredentialsSelector<T> clientCredentialsSelector,
070                                            final Set<Audience> expectedAudience) {
071
072                claimsSetVerifier = new JWTAuthenticationClaimsSetVerifier(expectedAudience);
073
074                if (clientCredentialsSelector == null) {
075                        throw new IllegalArgumentException("The client credentials selector must not be null");
076                }
077
078                this.clientCredentialsSelector = clientCredentialsSelector;
079        }
080
081
082        /**
083         * Returns the client credentials selector.
084         *
085         * @return The client credentials selector.
086         */
087        public ClientCredentialsSelector<T> getClientCredentialsSelector() {
088
089                return clientCredentialsSelector;
090        }
091
092
093        /**
094         * Returns the permitted audience values in JWT authentication
095         * assertions.
096         *
097         * @return The permitted audience (aud) claim values.
098         */
099        public Set<Audience> getExpectedAudience() {
100
101                return claimsSetVerifier.getExpectedAudience();
102        }
103
104
105        /**
106         * Verifies a client authentication request.
107         *
108         * @param clientAuth The client authentication. Must not be
109         *                   {@code null}.
110         * @param context    Additional context to be passed to the client
111         *                   credentials selector. May be {@code null}.
112         *
113         * @return {@code true} if the client was successfully authenticated,
114         *         {@code false} if the authentication failed due to an unknown
115         *         client, invalid credential or unsupported authentication
116         *         method.
117         *
118         * @throws JOSEException
119         */
120        public boolean verify(final ClientAuthentication clientAuth, final Context<T> context)
121                throws JOSEException {
122
123                if (clientAuth instanceof PlainClientSecret) {
124
125                        List<Secret> secretCandidates = clientCredentialsSelector.selectClientSecrets(
126                                clientAuth.getClientID(),
127                                clientAuth.getMethod(),
128                                context);
129
130                        if (secretCandidates == null) {
131                                return false; // invalid client
132                        }
133
134                        PlainClientSecret plainAuth = (PlainClientSecret)clientAuth;
135
136                        for (Secret candidate: secretCandidates) {
137                                if (plainAuth.getClientSecret().equals(candidate)) {
138                                        return true; // success
139                                }
140                        }
141
142                        return false; // invalid client
143
144                } else if (clientAuth instanceof ClientSecretJWT) {
145
146                        ClientSecretJWT jwtAuth = (ClientSecretJWT) clientAuth;
147
148                        // Check claims first before calling backend
149                        try {
150                                claimsSetVerifier.verify(jwtAuth.getJWTAuthenticationClaimsSet().toJWTClaimsSet());
151                        } catch (BadJWTException e) {
152                                return false; // invalid client
153                        }
154
155                        List<Secret> secretCandidates = clientCredentialsSelector.selectClientSecrets(
156                                clientAuth.getClientID(),
157                                clientAuth.getMethod(),
158                                context);
159
160                        if (secretCandidates == null) {
161                                return false; // invalid client
162                        }
163
164                        SignedJWT assertion = jwtAuth.getClientAssertion();
165
166                        for (Secret candidate : secretCandidates) {
167
168                                boolean valid = assertion.verify(new MACVerifier(candidate.getValueBytes()));
169
170                                if (valid) {
171                                        return true; // success
172                                }
173                        }
174
175                        return false; // invalid client
176
177                } else if (clientAuth instanceof PrivateKeyJWT) {
178
179                        PrivateKeyJWT jwtAuth = (PrivateKeyJWT)clientAuth;
180
181                        // Check claims first before calling backend
182                        try {
183                                claimsSetVerifier.verify(jwtAuth.getJWTAuthenticationClaimsSet().toJWTClaimsSet());
184                        } catch (BadJWTException e) {
185                                return false; // invalid client
186                        }
187
188                        List<? extends PublicKey> keyCandidates = clientCredentialsSelector.selectPublicKeys(
189                                jwtAuth.getClientID(),
190                                jwtAuth.getMethod(),
191                                jwtAuth.getClientAssertion().getHeader(),
192                                context);
193
194                        if  (keyCandidates == null) {
195                                return false; // invalid client
196                        }
197
198                        SignedJWT assertion = jwtAuth.getClientAssertion();
199
200                        for (PublicKey candidate: keyCandidates) {
201
202                                if (candidate == null) {
203                                        continue; // skip
204                                }
205
206                                JWSVerifier jwsVerifier = jwsVerifierFactory.createJWSVerifier(
207                                        jwtAuth.getClientAssertion().getHeader(),
208                                        candidate);
209
210                                boolean valid = assertion.verify(jwsVerifier);
211
212                                if (valid) {
213                                        return true; // success
214                                }
215                        }
216
217                        return false; // invalid client
218
219                } else {
220                        throw new RuntimeException("Unexpected client authentication: " + clientAuth.getMethod());
221                }
222        }
223}