View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  package org.apache.hc.client5.http.impl.auth;
28  
29  import java.net.UnknownHostException;
30  import java.security.Principal;
31  
32  import org.apache.hc.client5.http.utils.Base64;
33  import org.apache.hc.client5.http.DnsResolver;
34  import org.apache.hc.client5.http.SystemDefaultDnsResolver;
35  import org.apache.hc.client5.http.auth.AuthChallenge;
36  import org.apache.hc.client5.http.auth.AuthScheme;
37  import org.apache.hc.client5.http.auth.StandardAuthScheme;
38  import org.apache.hc.client5.http.auth.AuthScope;
39  import org.apache.hc.client5.http.auth.AuthenticationException;
40  import org.apache.hc.client5.http.auth.Credentials;
41  import org.apache.hc.client5.http.auth.CredentialsProvider;
42  import org.apache.hc.client5.http.auth.InvalidCredentialsException;
43  import org.apache.hc.client5.http.auth.KerberosConfig;
44  import org.apache.hc.client5.http.auth.KerberosCredentials;
45  import org.apache.hc.client5.http.auth.MalformedChallengeException;
46  import org.apache.hc.client5.http.protocol.HttpClientContext;
47  import org.apache.hc.core5.http.HttpHost;
48  import org.apache.hc.core5.http.HttpRequest;
49  import org.apache.hc.core5.http.protocol.HttpContext;
50  import org.apache.hc.core5.util.Args;
51  import org.ietf.jgss.GSSContext;
52  import org.ietf.jgss.GSSCredential;
53  import org.ietf.jgss.GSSException;
54  import org.ietf.jgss.GSSManager;
55  import org.ietf.jgss.GSSName;
56  import org.ietf.jgss.Oid;
57  import org.slf4j.Logger;
58  import org.slf4j.LoggerFactory;
59  
60  /**
61   * Common behavior for {@code GSS} based authentication schemes.
62   *
63   * @since 4.2
64   */
65  public abstract class GGSSchemeBase implements AuthScheme {
66  
67      enum State {
68          UNINITIATED,
69          CHALLENGE_RECEIVED,
70          TOKEN_GENERATED,
71          FAILED,
72      }
73  
74      private static final Logger LOG = LoggerFactory.getLogger(GGSSchemeBase.class);
75      private static final String NO_TOKEN = "";
76      private static final String KERBEROS_SCHEME = "HTTP";
77      private final KerberosConfig config;
78      private final DnsResolver dnsResolver;
79  
80      /** Authentication process state */
81      private State state;
82      private GSSCredential gssCredential;
83      private String challenge;
84      private byte[] token;
85  
86      GGSSchemeBase(final KerberosConfig config, final DnsResolver dnsResolver) {
87          super();
88          this.config = config != null ? config : KerberosConfig.DEFAULT;
89          this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE;
90          this.state = State.UNINITIATED;
91      }
92  
93      GGSSchemeBase(final KerberosConfig config) {
94          this(config, SystemDefaultDnsResolver.INSTANCE);
95      }
96  
97      GGSSchemeBase() {
98          this(KerberosConfig.DEFAULT, SystemDefaultDnsResolver.INSTANCE);
99      }
100 
101     @Override
102     public String getRealm() {
103         return null;
104     }
105 
106     @Override
107     public void processChallenge(
108             final AuthChallenge authChallenge,
109             final HttpContext context) throws MalformedChallengeException {
110         Args.notNull(authChallenge, "AuthChallenge");
111 
112         this.challenge = authChallenge.getValue() != null ? authChallenge.getValue() : NO_TOKEN;
113 
114         if (state == State.UNINITIATED) {
115             token = Base64.decodeBase64(challenge.getBytes());
116             state = State.CHALLENGE_RECEIVED;
117         } else {
118             if (LOG.isDebugEnabled()) {
119                 final HttpClientContext clientContext = HttpClientContext.adapt(context);
120                 final String exchangeId = clientContext.getExchangeId();
121                 LOG.debug("{} Authentication already attempted", exchangeId);
122             }
123             state = State.FAILED;
124         }
125     }
126 
127     protected GSSManager getManager() {
128         return GSSManager.getInstance();
129     }
130 
131     /**
132      * @since 4.4
133      */
134     protected byte[] generateGSSToken(
135             final byte[] input, final Oid oid, final String serviceName, final String authServer) throws GSSException {
136         final GSSManager manager = getManager();
137         final GSSName serverName = manager.createName(serviceName + "@" + authServer, GSSName.NT_HOSTBASED_SERVICE);
138 
139         final GSSContext gssContext = createGSSContext(manager, oid, serverName, gssCredential);
140         if (input != null) {
141             return gssContext.initSecContext(input, 0, input.length);
142         } else {
143             return gssContext.initSecContext(new byte[] {}, 0, 0);
144         }
145     }
146 
147     /**
148      * @since 5.0
149      */
150     protected GSSContext createGSSContext(
151             final GSSManager manager,
152             final Oid oid,
153             final GSSName serverName,
154             final GSSCredential gssCredential) throws GSSException {
155         final GSSContext gssContext = manager.createContext(serverName.canonicalize(oid), oid, gssCredential,
156                 GSSContext.DEFAULT_LIFETIME);
157         gssContext.requestMutualAuth(true);
158         if (config.getRequestDelegCreds() != KerberosConfig.Option.DEFAULT) {
159             gssContext.requestCredDeleg(config.getRequestDelegCreds() == KerberosConfig.Option.ENABLE);
160         }
161         return gssContext;
162     }
163     /**
164      * @since 4.4
165      */
166     protected abstract byte[] generateToken(byte[] input, String serviceName, String authServer) throws GSSException;
167 
168     @Override
169     public boolean isChallengeComplete() {
170         return this.state == State.TOKEN_GENERATED || this.state == State.FAILED;
171     }
172 
173     @Override
174     public boolean isResponseReady(
175             final HttpHost host,
176             final CredentialsProvider credentialsProvider,
177             final HttpContext context) throws AuthenticationException {
178 
179         Args.notNull(host, "Auth host");
180         Args.notNull(credentialsProvider, "CredentialsProvider");
181 
182         final Credentials credentials = credentialsProvider.getCredentials(
183                 new AuthScope(host, null, getName()), context);
184         if (credentials instanceof KerberosCredentials) {
185             this.gssCredential = ((KerberosCredentials) credentials).getGSSCredential();
186         } else {
187             this.gssCredential = null;
188         }
189         return true;
190     }
191 
192     @Override
193     public Principal getPrincipal() {
194         return null;
195     }
196 
197     @Override
198     public String generateAuthResponse(
199             final HttpHost host,
200             final HttpRequest request,
201             final HttpContext context) throws AuthenticationException {
202         Args.notNull(host, "HTTP host");
203         Args.notNull(request, "HTTP request");
204         switch (state) {
205         case UNINITIATED:
206             throw new AuthenticationException(getName() + " authentication has not been initiated");
207         case FAILED:
208             throw new AuthenticationException(getName() + " authentication has failed");
209         case CHALLENGE_RECEIVED:
210             try {
211                 final String authServer;
212                 String hostname = host.getHostName();
213                 if (config.getUseCanonicalHostname() != KerberosConfig.Option.DISABLE){
214                     try {
215                          hostname = dnsResolver.resolveCanonicalHostname(host.getHostName());
216                     } catch (final UnknownHostException ignore){
217                     }
218                 }
219                 if (config.getStripPort() != KerberosConfig.Option.DISABLE) {
220                     authServer = hostname;
221                 } else {
222                     authServer = hostname + ":" + host.getPort();
223                 }
224 
225                 if (LOG.isDebugEnabled()) {
226                     final HttpClientContext clientContext = HttpClientContext.adapt(context);
227                     final String exchangeId = clientContext.getExchangeId();
228                     LOG.debug("{} init {}", exchangeId, authServer);
229                 }
230                 token = generateToken(token, KERBEROS_SCHEME, authServer);
231                 state = State.TOKEN_GENERATED;
232             } catch (final GSSException gsse) {
233                 state = State.FAILED;
234                 if (gsse.getMajor() == GSSException.DEFECTIVE_CREDENTIAL
235                         || gsse.getMajor() == GSSException.CREDENTIALS_EXPIRED) {
236                     throw new InvalidCredentialsException(gsse.getMessage(), gsse);
237                 }
238                 if (gsse.getMajor() == GSSException.NO_CRED ) {
239                     throw new InvalidCredentialsException(gsse.getMessage(), gsse);
240                 }
241                 if (gsse.getMajor() == GSSException.DEFECTIVE_TOKEN
242                         || gsse.getMajor() == GSSException.DUPLICATE_TOKEN
243                         || gsse.getMajor() == GSSException.OLD_TOKEN) {
244                     throw new AuthenticationException(gsse.getMessage(), gsse);
245                 }
246                 // other error
247                 throw new AuthenticationException(gsse.getMessage());
248             }
249         case TOKEN_GENERATED:
250             final Base64 codec = new Base64(0);
251             final String tokenstr = new String(codec.encode(token));
252             if (LOG.isDebugEnabled()) {
253                 final HttpClientContext clientContext = HttpClientContext.adapt(context);
254                 final String exchangeId = clientContext.getExchangeId();
255                 LOG.debug("{} Sending response '{}' back to the auth server", exchangeId, tokenstr);
256             }
257             return StandardAuthScheme.SPNEGO + " " + tokenstr;
258         default:
259             throw new IllegalStateException("Illegal state: " + state);
260         }
261     }
262 
263     @Override
264     public String toString() {
265         return getName() + "{" + this.state + " " + challenge + '}';
266     }
267 
268 }