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  import java.util.Locale;
32  
33  import org.apache.commons.codec.binary.Base64;
34  import org.apache.hc.client5.http.DnsResolver;
35  import org.apache.hc.client5.http.SystemDefaultDnsResolver;
36  import org.apache.hc.client5.http.auth.AuthChallenge;
37  import org.apache.hc.client5.http.auth.AuthScheme;
38  import org.apache.hc.client5.http.auth.StandardAuthScheme;
39  import org.apache.hc.client5.http.auth.AuthScope;
40  import org.apache.hc.client5.http.auth.AuthenticationException;
41  import org.apache.hc.client5.http.auth.Credentials;
42  import org.apache.hc.client5.http.auth.CredentialsProvider;
43  import org.apache.hc.client5.http.auth.InvalidCredentialsException;
44  import org.apache.hc.client5.http.auth.KerberosConfig;
45  import org.apache.hc.client5.http.auth.KerberosCredentials;
46  import org.apache.hc.client5.http.auth.MalformedChallengeException;
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  
76      private final KerberosConfig config;
77      private final DnsResolver dnsResolver;
78  
79      /** Authentication process state */
80      private State state;
81      private GSSCredential gssCredential;
82      private String challenge;
83      private byte[] token;
84  
85      GGSSchemeBase(final KerberosConfig config, final DnsResolver dnsResolver) {
86          super();
87          this.config = config != null ? config : KerberosConfig.DEFAULT;
88          this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE;
89          this.state = State.UNINITIATED;
90      }
91  
92      GGSSchemeBase(final KerberosConfig config) {
93          this(config, SystemDefaultDnsResolver.INSTANCE);
94      }
95  
96      GGSSchemeBase() {
97          this(KerberosConfig.DEFAULT, SystemDefaultDnsResolver.INSTANCE);
98      }
99  
100     @Override
101     public String getRealm() {
102         return null;
103     }
104 
105     @Override
106     public void processChallenge(
107             final AuthChallenge authChallenge,
108             final HttpContext context) throws MalformedChallengeException {
109         Args.notNull(authChallenge, "AuthChallenge");
110         if (authChallenge.getValue() == null) {
111             throw new MalformedChallengeException("Missing auth challenge");
112         }
113         this.challenge = authChallenge.getValue();
114         if (state == State.UNINITIATED) {
115             token = Base64.decodeBase64(challenge.getBytes());
116             state = State.CHALLENGE_RECEIVED;
117         } else {
118             LOG.debug("Authentication already attempted");
119             state = State.FAILED;
120         }
121     }
122 
123     protected GSSManager getManager() {
124         return GSSManager.getInstance();
125     }
126 
127     /**
128      * @since 4.4
129      */
130     protected byte[] generateGSSToken(
131             final byte[] input, final Oid oid, final String serviceName, final String authServer) throws GSSException {
132         final GSSManager manager = getManager();
133         final GSSName serverName = manager.createName(serviceName + "@" + authServer, GSSName.NT_HOSTBASED_SERVICE);
134 
135         final GSSContext gssContext = createGSSContext(manager, oid, serverName, gssCredential);
136         if (input != null) {
137             return gssContext.initSecContext(input, 0, input.length);
138         } else {
139             return gssContext.initSecContext(new byte[] {}, 0, 0);
140         }
141     }
142 
143     /**
144      * @since 5.0
145      */
146     protected GSSContext createGSSContext(
147             final GSSManager manager,
148             final Oid oid,
149             final GSSName serverName,
150             final GSSCredential gssCredential) throws GSSException {
151         final GSSContext gssContext = manager.createContext(serverName.canonicalize(oid), oid, gssCredential,
152                 GSSContext.DEFAULT_LIFETIME);
153         gssContext.requestMutualAuth(true);
154         if (config.getRequestDelegCreds() != KerberosConfig.Option.DEFAULT) {
155             gssContext.requestCredDeleg(config.getRequestDelegCreds() == KerberosConfig.Option.ENABLE);
156         }
157         return gssContext;
158     }
159     /**
160      * @since 4.4
161      */
162     protected abstract byte[] generateToken(byte[] input, String serviceName, String authServer) throws GSSException;
163 
164     @Override
165     public boolean isChallengeComplete() {
166         return this.state == State.TOKEN_GENERATED || this.state == State.FAILED;
167     }
168 
169     @Override
170     public boolean isResponseReady(
171             final HttpHost host,
172             final CredentialsProvider credentialsProvider,
173             final HttpContext context) throws AuthenticationException {
174 
175         Args.notNull(host, "Auth host");
176         Args.notNull(credentialsProvider, "CredentialsProvider");
177 
178         final Credentials credentials = credentialsProvider.getCredentials(
179                 new AuthScope(host, null, getName()), context);
180         if (credentials instanceof KerberosCredentials) {
181             this.gssCredential = ((KerberosCredentials) credentials).getGSSCredential();
182         } else {
183             this.gssCredential = null;
184         }
185         return true;
186     }
187 
188     @Override
189     public Principal getPrincipal() {
190         return null;
191     }
192 
193     @Override
194     public String generateAuthResponse(
195             final HttpHost host,
196             final HttpRequest request,
197             final HttpContext context) throws AuthenticationException {
198         Args.notNull(host, "HTTP host");
199         Args.notNull(request, "HTTP request");
200         switch (state) {
201         case UNINITIATED:
202             throw new AuthenticationException(getName() + " authentication has not been initiated");
203         case FAILED:
204             throw new AuthenticationException(getName() + " authentication has failed");
205         case CHALLENGE_RECEIVED:
206             try {
207                 final String authServer;
208                 String hostname = host.getHostName();
209                 if (config.getUseCanonicalHostname() != KerberosConfig.Option.DISABLE){
210                     try {
211                          hostname = dnsResolver.resolveCanonicalHostname(host.getHostName());
212                     } catch (final UnknownHostException ignore){
213                     }
214                 }
215                 if (config.getStripPort() != KerberosConfig.Option.DISABLE) {
216                     authServer = hostname;
217                 } else {
218                     authServer = hostname + ":" + host.getPort();
219                 }
220                 final String serviceName = host.getSchemeName().toUpperCase(Locale.ROOT);
221 
222                 if (LOG.isDebugEnabled()) {
223                     LOG.debug("init {}", authServer);
224                 }
225                 token = generateToken(token, serviceName, authServer);
226                 state = State.TOKEN_GENERATED;
227             } catch (final GSSException gsse) {
228                 state = State.FAILED;
229                 if (gsse.getMajor() == GSSException.DEFECTIVE_CREDENTIAL
230                         || gsse.getMajor() == GSSException.CREDENTIALS_EXPIRED) {
231                     throw new InvalidCredentialsException(gsse.getMessage(), gsse);
232                 }
233                 if (gsse.getMajor() == GSSException.NO_CRED ) {
234                     throw new InvalidCredentialsException(gsse.getMessage(), gsse);
235                 }
236                 if (gsse.getMajor() == GSSException.DEFECTIVE_TOKEN
237                         || gsse.getMajor() == GSSException.DUPLICATE_TOKEN
238                         || gsse.getMajor() == GSSException.OLD_TOKEN) {
239                     throw new AuthenticationException(gsse.getMessage(), gsse);
240                 }
241                 // other error
242                 throw new AuthenticationException(gsse.getMessage());
243             }
244         case TOKEN_GENERATED:
245             final Base64 codec = new Base64(0);
246             final String tokenstr = new String(codec.encode(token));
247             if (LOG.isDebugEnabled()) {
248                 LOG.debug("Sending response '{}' back to the auth server", tokenstr);
249             }
250             return StandardAuthScheme.SPNEGO + " " + tokenstr;
251         default:
252             throw new IllegalStateException("Illegal state: " + state);
253         }
254     }
255 
256     @Override
257     public String toString() {
258         return getName() + "{" + this.state + " " + challenge + '}';
259     }
260 
261 }