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;
019
020
021import com.nimbusds.common.contenttype.ContentType;
022import com.nimbusds.oauth2.sdk.ParseException;
023import com.nimbusds.oauth2.sdk.http.HTTPRequest;
024import com.nimbusds.oauth2.sdk.id.ClientID;
025import com.nimbusds.oauth2.sdk.util.CollectionUtils;
026import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils;
027import com.nimbusds.oauth2.sdk.util.StringUtils;
028
029import javax.security.auth.x500.X500Principal;
030import java.util.*;
031
032
033/**
034 * Base abstract class for client authentication.
035 *
036 * <p>Related specifications:
037 *
038 * <ul>
039 *     <li>OAuth 2.0 (RFC 6749)
040 *     <li>JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and
041 *         Authorization Grants (RFC 7523)
042 *     <li>OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound
043 *         Access Tokens (RFC 8705)
044 * </ul>
045 */
046public abstract class ClientAuthentication {
047        
048        
049        /**
050         * The client authentication method.
051         */
052        private final ClientAuthenticationMethod method;
053
054
055        /**
056         * The client ID.
057         */
058        private final ClientID clientID;
059        
060        
061        /**
062         * Creates a new abstract client authentication.
063         *
064         * @param method   The client authentication method. Must not be
065         *                 {@code null}.
066         * @param clientID The client identifier. Must not be {@code null}.
067         */
068        protected ClientAuthentication(final ClientAuthenticationMethod method, final ClientID clientID) {
069        
070                this.method = Objects.requireNonNull(method);
071                this.clientID = Objects.requireNonNull(clientID);
072        }
073        
074        
075        /**
076         * Returns the client authentication method.
077         *
078         * @return The client authentication method.
079         */
080        public ClientAuthenticationMethod getMethod() {
081        
082                return method;
083        }
084
085
086        /**
087         * Returns the client identifier.
088         *
089         * @return The client identifier.
090         */
091        public ClientID getClientID() {
092
093                return clientID;
094        }
095        
096        
097        /**
098         * Returns the names of the form parameters if used by the
099         * authentication method.
100         *
101         * @return The form parameter names, empty set if none.
102         */
103        public abstract Set<String> getFormParameterNames();
104        
105        
106        /**
107         * Parses the specified HTTP request for a supported client 
108         * authentication (see {@link ClientAuthenticationMethod}).
109         *
110         * @param httpRequest The HTTP request to parse. Must not be 
111         *                    {@code null}.
112         *
113         * @return The client authentication, {@code null} if none or the HTTP
114         *         method is not supported.
115         *
116         * @throws ParseException If the client authentication couldn't be
117         *                        parsed.
118         */
119        public static ClientAuthentication parse(final HTTPRequest httpRequest)
120                throws ParseException {
121
122                List<ClientAuthentication> candidates = parseCandidates(httpRequest);
123                if (CollectionUtils.isEmpty(candidates)) {
124                        return null;
125                } else {
126                        return candidates.get(0);
127                }
128        }
129
130
131        /**
132         * Parses the specified HTTP request for a supported client
133         * authentication (see {@link ClientAuthenticationMethod}).
134         *
135         * @param httpRequest The HTTP request to parse. Must not be
136         *                    {@code null}.
137         *
138         * @return The client authentication candidates, {@code null} if none
139         *         or the HTTP method is not supported.
140         *
141         * @throws ParseException If the client authentication couldn't be
142         *                        parsed.
143         */
144        public static List<ClientAuthentication> parseCandidates(final HTTPRequest httpRequest)
145                throws ParseException {
146
147                // Preserve legacy order: client_secret_basic, client_secret_post, JWT auth, TLS auth
148                Map<ClientAuthenticationMethod, ClientAuthentication> candidates = new LinkedHashMap<>();
149
150                // Check for client_secret_basic
151                if (httpRequest.getAuthorization() != null &&
152                    httpRequest.getAuthorization().startsWith("Basic ")) {
153
154                        candidates.put(ClientAuthenticationMethod.CLIENT_SECRET_BASIC, ClientSecretBasic.parse(httpRequest));
155                }
156
157                // The other methods require HTTP POST with URL-encoded params
158                if (httpRequest.getMethod().equals(HTTPRequest.Method.POST)
159                    && httpRequest.getEntityContentType() != null
160                    && httpRequest.getEntityContentType().matches(ContentType.APPLICATION_URLENCODED)) {
161
162                        Map<String,List<String>> params = httpRequest.getBodyAsFormParameters();
163
164                        // We have client_secret_post
165                        if (StringUtils.isNotBlank(MultivaluedMapUtils.getFirstValue(params, "client_id")) && StringUtils.isNotBlank(MultivaluedMapUtils.getFirstValue(params, "client_secret"))) {
166                                candidates.put(ClientAuthenticationMethod.CLIENT_SECRET_POST, ClientSecretPost.parse(httpRequest));
167                        }
168
169                        // Do we have a signed JWT assertion?
170                        if (StringUtils.isNotBlank(MultivaluedMapUtils.getFirstValue(params, "client_assertion")) && StringUtils.isNotBlank(MultivaluedMapUtils.getFirstValue(params, "client_assertion_type"))) {
171                                JWTAuthentication jwtAuth = JWTAuthentication.parse(httpRequest);
172                                candidates.put(jwtAuth.getMethod(), jwtAuth);
173                        }
174
175                        // Client TLS?
176                        if (httpRequest.getClientX509Certificate() != null && StringUtils.isNotBlank(MultivaluedMapUtils.getFirstValue(params, "client_id"))) {
177
178                                // Check for self-issued first (not for self-signed - too CPU expensive)
179
180                                X500Principal issuer = httpRequest.getClientX509Certificate().getIssuerX500Principal();
181                                X500Principal subject = httpRequest.getClientX509Certificate().getSubjectX500Principal();
182
183                                if (issuer.equals(subject)) {
184                                        // Additional checks
185                                        if (httpRequest.getClientX509CertificateRootDN() != null) {
186                                                // If TLS proxy set issuer header it must match the certificate's
187                                                if (! httpRequest.getClientX509CertificateRootDN().equalsIgnoreCase(issuer.toString())) {
188                                                        throw new ParseException("Client X.509 certificate issuer DN doesn't match HTTP request metadata");
189                                                }
190                                        }
191                                        if (httpRequest.getClientX509CertificateSubjectDN() != null) {
192                                                // If TLS proxy set subject header it must match the certificate's
193                                                if (! httpRequest.getClientX509CertificateSubjectDN().equalsIgnoreCase(subject.toString())) {
194                                                        throw new ParseException("Client X.509 certificate subject DN doesn't match HTTP request metadata");
195                                                }
196                                        }
197
198                                        // Self-issued (assumes self-signed)
199                                        candidates.put(ClientAuthenticationMethod.SELF_SIGNED_TLS_CLIENT_AUTH, SelfSignedTLSClientAuthentication.parse(httpRequest));
200                                } else {
201                                        // PKI bound
202                                        candidates.put(ClientAuthenticationMethod.TLS_CLIENT_AUTH, PKITLSClientAuthentication.parse(httpRequest));
203                                }
204                        }
205                }
206
207                if (candidates.isEmpty()) {
208                        return null; // no auth
209                }
210
211                Utils.ensureSameClientID(candidates.values());
212
213                return new ArrayList<>(candidates.values());
214        }
215        
216        
217        /**
218         * Applies the authentication to the specified HTTP request by setting 
219         * its Authorization header and/or POST entity-body parameters 
220         * (according to the implemented client authentication method).
221         *
222         * @param httpRequest The HTTP request. Must not be {@code null}.
223         */
224        public abstract void applyTo(final HTTPRequest httpRequest);
225}