diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestSPNegoScheme.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestMutualSpnegoScheme.java similarity index 99% rename from httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestSPNegoScheme.java rename to httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestMutualSpnegoScheme.java index 1347dd928..55b5c08c1 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestSPNegoScheme.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestMutualSpnegoScheme.java @@ -49,7 +49,7 @@ import org.apache.hc.client5.http.auth.StandardAuthScheme; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy; import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder; -import org.apache.hc.client5.http.impl.auth.SPNegoScheme; +import org.apache.hc.client5.http.impl.auth.MutualSpnegoScheme; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.client5.http.utils.Base64; import org.apache.hc.client5.testing.extension.sync.ClientProtocolLevel; @@ -80,9 +80,9 @@ import org.mockito.Mockito; /** * Tests for {@link SPNegoScheme}. */ -public class TestSPNegoScheme extends AbstractIntegrationTestBase { +public class TestMutualSpnegoScheme extends AbstractIntegrationTestBase { - protected TestSPNegoScheme() { + protected TestMutualSpnegoScheme() { super(URIScheme.HTTP, ClientProtocolLevel.STANDARD); } @@ -191,7 +191,7 @@ public class TestSPNegoScheme extends AbstractIntegrationTestBase { * Kerberos configuration. * */ - private static class NegotiateSchemeWithMockGssManager extends SPNegoScheme { + private static class NegotiateSchemeWithMockGssManager extends MutualSpnegoScheme { final GSSManager manager = Mockito.mock(GSSManager.class); final GSSName name = Mockito.mock(GSSName.class); @@ -218,7 +218,7 @@ public class TestSPNegoScheme extends AbstractIntegrationTestBase { } - private static class MutualNegotiateSchemeWithMockGssManager extends SPNegoScheme { + private static class MutualNegotiateSchemeWithMockGssManager extends MutualSpnegoScheme { final GSSManager manager = Mockito.mock(GSSManager.class); final GSSName name = Mockito.mock(GSSName.class); diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/GGSSchemeBase.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/GGSSchemeBase.java index 245fbb733..2953416f3 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/GGSSchemeBase.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/GGSSchemeBase.java @@ -32,14 +32,14 @@ import java.security.Principal; import org.apache.hc.client5.http.DnsResolver; import org.apache.hc.client5.http.SystemDefaultDnsResolver; import org.apache.hc.client5.http.auth.AuthChallenge; -import org.apache.hc.client5.http.auth.AuthScheme2; +import org.apache.hc.client5.http.auth.AuthScheme; import org.apache.hc.client5.http.auth.AuthScope; import org.apache.hc.client5.http.auth.AuthenticationException; import org.apache.hc.client5.http.auth.Credentials; import org.apache.hc.client5.http.auth.CredentialsProvider; import org.apache.hc.client5.http.auth.InvalidCredentialsException; +import org.apache.hc.client5.http.auth.MalformedChallengeException; import org.apache.hc.client5.http.auth.StandardAuthScheme; -import org.apache.hc.client5.http.auth.KerberosConfig; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.client5.http.utils.Base64; import org.apache.hc.core5.http.HttpHost; @@ -60,51 +60,45 @@ import org.slf4j.LoggerFactory; * * @since 4.2 * + * @deprecated Do not use. This class implements functionality for the old deprecated non mutual + * authentication capable {@link SPNegoScheme} and {@link KerberosScheme} classes. + * The new mutual authentication capable implementation is {@link MutualGSSSchemeBase}. */ -// FIXME The class name looks like a Typo. Rename in 6.0 ? -public abstract class GGSSchemeBase implements AuthScheme2 { +@Deprecated +public abstract class GGSSchemeBase implements AuthScheme { enum State { UNINITIATED, - TOKEN_READY, - TOKEN_SENT, - SUCCEEDED, + CHALLENGE_RECEIVED, + TOKEN_GENERATED, FAILED, } private static final Logger LOG = LoggerFactory.getLogger(GGSSchemeBase.class); private static final String NO_TOKEN = ""; private static final String KERBEROS_SCHEME = "HTTP"; - - // The GSS spec does not specify how long the conversation can be. This should be plenty. - // Realistically, we get one initial token, then one maybe one more for mutual authentication. - private static final int MAX_GSS_CHALLENGES = 3; - private final KerberosConfig config; + private final org.apache.hc.client5.http.auth.KerberosConfig config; private final DnsResolver dnsResolver; - private final boolean mutualAuth; - private int challengesLeft = MAX_GSS_CHALLENGES; /** Authentication process state */ private State state; private GSSCredential gssCredential; - private GSSContext gssContext; private String challenge; - private byte[] queuedToken = new byte[0]; + private byte[] token; - GGSSchemeBase(final KerberosConfig config, final DnsResolver dnsResolver) { + GGSSchemeBase(final org.apache.hc.client5.http.auth.KerberosConfig config, final DnsResolver dnsResolver) { super(); - this.config = config != null ? config : KerberosConfig.DEFAULT; + this.config = config != null ? config : org.apache.hc.client5.http.auth.KerberosConfig.DEFAULT; this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE; - this.mutualAuth = config.getRequestMutualAuth() == KerberosConfig.Option.ENABLE; this.state = State.UNINITIATED; } - GGSSchemeBase(final KerberosConfig config) { + GGSSchemeBase(final org.apache.hc.client5.http.auth.KerberosConfig config) { this(config, SystemDefaultDnsResolver.INSTANCE); } GGSSchemeBase() { - this(KerberosConfig.DEFAULT, SystemDefaultDnsResolver.INSTANCE); + this(org.apache.hc.client5.http.auth.KerberosConfig.DEFAULT, SystemDefaultDnsResolver.INSTANCE); } @Override @@ -112,115 +106,24 @@ public abstract class GGSSchemeBase implements AuthScheme2 { return null; } - // The AuthScheme API maps awkwardly to GSSAPI, where proccessChallange and generateAuthResponse - // map to the same single method call. Hence the generated token is only stored in this method. @Override public void processChallenge( - final HttpHost host, final AuthChallenge authChallenge, - final HttpContext context, - final boolean challenged) throws AuthenticationException { + final HttpContext context) throws MalformedChallengeException { + Args.notNull(authChallenge, "AuthChallenge"); - if (challengesLeft-- <= 0 ) { + this.challenge = authChallenge.getValue() != null ? authChallenge.getValue() : NO_TOKEN; + + if (state == State.UNINITIATED) { + token = Base64.decodeBase64(challenge.getBytes()); + state = State.CHALLENGE_RECEIVED; + } else { if (LOG.isDebugEnabled()) { final HttpClientContext clientContext = HttpClientContext.cast(context); final String exchangeId = clientContext.getExchangeId(); - LOG.debug("{} GSS error: too many challenges received. Infinite loop ?", exchangeId); + LOG.debug("{} Authentication already attempted", exchangeId); } - // TODO: Should we throw an exception ? There is a test for this behaviour. state = State.FAILED; - return; - } - - final byte[] challengeToken = Base64.decodeBase64(authChallenge == null ? null : authChallenge.getValue()); - - final String gssHostname; - String hostname = host.getHostName(); - if (config.getUseCanonicalHostname() != KerberosConfig.Option.DISABLE) { - try { - hostname = dnsResolver.resolveCanonicalHostname(host.getHostName()); - } catch (final UnknownHostException ignore) { - } - } - if (config.getStripPort() != KerberosConfig.Option.DISABLE) { - gssHostname = hostname; - } else { - gssHostname = hostname + ":" + host.getPort(); - } - - if (LOG.isDebugEnabled()) { - final HttpClientContext clientContext = HttpClientContext.cast(context); - final String exchangeId = clientContext.getExchangeId(); - LOG.debug("{} GSS init {}", exchangeId, gssHostname); - } - try { - queuedToken = generateToken(challengeToken, KERBEROS_SCHEME, gssHostname); - switch (state) { - case UNINITIATED: - if (challenge != NO_TOKEN) { - if (LOG.isDebugEnabled()) { - final HttpClientContext clientContext = HttpClientContext.cast(context); - final String exchangeId = clientContext.getExchangeId(); - LOG.debug("{} Internal GSS error: token received when none was sent yet: {}", exchangeId, challengeToken); - } - // TODO Should we fail ? That would break existing tests that send a token - // in the first response, which is against the RFC. - } - state = State.TOKEN_READY; - break; - case TOKEN_SENT: - if (challenged) { - state = State.TOKEN_READY; - } else if (mutualAuth) { - // We should have received a valid mutualAuth token - if (!gssContext.isEstablished()) { - if (LOG.isDebugEnabled()) { - final HttpClientContext clientContext = - HttpClientContext.cast(context); - final String exchangeId = clientContext.getExchangeId(); - LOG.debug("{} GSSContext is not established ", exchangeId); - } - state = State.FAILED; - // TODO should we have specific exception(s) for these ? - throw new AuthenticationException( - "requireMutualAuth is set but GSSContext is not established"); - } else if (!gssContext.getMutualAuthState()) { - if (LOG.isDebugEnabled()) { - final HttpClientContext clientContext = - HttpClientContext.cast(context); - final String exchangeId = clientContext.getExchangeId(); - LOG.debug("{} requireMutualAuth is set but GSSAUthContext does not have" - + " mutualAuthState set", exchangeId); - } - state = State.FAILED; - throw new AuthenticationException( - "requireMutualAuth is set but GSSContext mutualAuthState is not set"); - } else { - state = State.SUCCEEDED; - } - } - break; - default: - state = State.FAILED; - throw new IllegalStateException("Illegal state: " + state); - - } - } catch (final GSSException gsse) { - state = State.FAILED; - if (gsse.getMajor() == GSSException.DEFECTIVE_CREDENTIAL - || gsse.getMajor() == GSSException.CREDENTIALS_EXPIRED) { - throw new InvalidCredentialsException(gsse.getMessage(), gsse); - } - if (gsse.getMajor() == GSSException.NO_CRED) { - throw new InvalidCredentialsException(gsse.getMessage(), gsse); - } - if (gsse.getMajor() == GSSException.DEFECTIVE_TOKEN - || gsse.getMajor() == GSSException.DUPLICATE_TOKEN - || gsse.getMajor() == GSSException.OLD_TOKEN) { - throw new AuthenticationException(gsse.getMessage(), gsse); - } - // other error - throw new AuthenticationException(gsse.getMessage(), gsse); } } @@ -232,13 +135,11 @@ public abstract class GGSSchemeBase implements AuthScheme2 { * @since 4.4 */ protected byte[] generateGSSToken( - final byte[] input, final Oid oid, final String gssServiceName, final String gssHostname) throws GSSException { + final byte[] input, final Oid oid, final String serviceName, final String authServer) throws GSSException { final GSSManager manager = getManager(); - final GSSName peerName = manager.createName(gssServiceName + "@" + gssHostname, GSSName.NT_HOSTBASED_SERVICE); + final GSSName serverName = manager.createName(serviceName + "@" + authServer, GSSName.NT_HOSTBASED_SERVICE); - if (gssContext == null) { - gssContext = createGSSContext(manager, oid, peerName, gssCredential); - } + final GSSContext gssContext = createGSSContext(manager, oid, serverName, gssCredential); if (input != null) { return gssContext.initSecContext(input, 0, input.length); } @@ -251,35 +152,24 @@ public abstract class GGSSchemeBase implements AuthScheme2 { protected GSSContext createGSSContext( final GSSManager manager, final Oid oid, - final GSSName peerName, + final GSSName serverName, final GSSCredential gssCredential) throws GSSException { - final GSSContext gssContext = manager.createContext(peerName.canonicalize(oid), oid, gssCredential, + final GSSContext gssContext = manager.createContext(serverName.canonicalize(oid), oid, gssCredential, GSSContext.DEFAULT_LIFETIME); gssContext.requestMutualAuth(true); - if (config.getRequestDelegCreds() != KerberosConfig.Option.DEFAULT) { - gssContext.requestCredDeleg(config.getRequestDelegCreds() == KerberosConfig.Option.ENABLE); - } - if (config.getRequestMutualAuth() != KerberosConfig.Option.DEFAULT) { - gssContext.requestMutualAuth(config.getRequestMutualAuth() == KerberosConfig.Option.ENABLE); + if (config.getRequestDelegCreds() != org.apache.hc.client5.http.auth.KerberosConfig.Option.DEFAULT) { + gssContext.requestCredDeleg(config.getRequestDelegCreds() == org.apache.hc.client5.http.auth.KerberosConfig.Option.ENABLE); } return gssContext; } /** * @since 4.4 */ - protected abstract byte[] generateToken(byte[] input, String gssServiceName, String gssHostname) throws GSSException; + protected abstract byte[] generateToken(byte[] input, String serviceName, String authServer) throws GSSException; @Override public boolean isChallengeComplete() { - // For the mutual authentication response, this is should technically return true. - // However, the HttpAuthenticator immediately fails the authentication - // process if we return true, so we only return true here if the authentication has failed. - return this.state == State.FAILED; - } - - @Override - public boolean isChallengeExpected() { - return state == State.TOKEN_SENT && mutualAuth; + return this.state == State.TOKEN_GENERATED || this.state == State.FAILED; } @Override @@ -306,8 +196,6 @@ public abstract class GGSSchemeBase implements AuthScheme2 { return null; } - // Format the queued token and update the state. - // All token processing is done in processChallenge() @Override public String generateAuthResponse( final HttpHost host, @@ -320,16 +208,53 @@ public abstract class GGSSchemeBase implements AuthScheme2 { throw new AuthenticationException(getName() + " authentication has not been initiated"); case FAILED: throw new AuthenticationException(getName() + " authentication has failed"); - case SUCCEEDED: - return null; - case TOKEN_READY: - state = State.TOKEN_SENT; + case CHALLENGE_RECEIVED: + try { + final String authServer; + String hostname = host.getHostName(); + if (config.getUseCanonicalHostname() != org.apache.hc.client5.http.auth.KerberosConfig.Option.DISABLE) { + try { + hostname = dnsResolver.resolveCanonicalHostname(host.getHostName()); + } catch (final UnknownHostException ignore) { + } + } + if (config.getStripPort() != org.apache.hc.client5.http.auth.KerberosConfig.Option.DISABLE) { + authServer = hostname; + } else { + authServer = hostname + ":" + host.getPort(); + } + + if (LOG.isDebugEnabled()) { + final HttpClientContext clientContext = HttpClientContext.cast(context); + final String exchangeId = clientContext.getExchangeId(); + LOG.debug("{} init {}", exchangeId, authServer); + } + token = generateToken(token, KERBEROS_SCHEME, authServer); + state = State.TOKEN_GENERATED; + } catch (final GSSException gsse) { + state = State.FAILED; + if (gsse.getMajor() == GSSException.DEFECTIVE_CREDENTIAL + || gsse.getMajor() == GSSException.CREDENTIALS_EXPIRED) { + throw new InvalidCredentialsException(gsse.getMessage(), gsse); + } + if (gsse.getMajor() == GSSException.NO_CRED ) { + throw new InvalidCredentialsException(gsse.getMessage(), gsse); + } + if (gsse.getMajor() == GSSException.DEFECTIVE_TOKEN + || gsse.getMajor() == GSSException.DUPLICATE_TOKEN + || gsse.getMajor() == GSSException.OLD_TOKEN) { + throw new AuthenticationException(gsse.getMessage(), gsse); + } + // other error + throw new AuthenticationException(gsse.getMessage()); + } + case TOKEN_GENERATED: final Base64 codec = new Base64(0); - final String tokenstr = new String(codec.encode(queuedToken)); + final String tokenstr = new String(codec.encode(token)); if (LOG.isDebugEnabled()) { final HttpClientContext clientContext = HttpClientContext.cast(context); final String exchangeId = clientContext.getExchangeId(); - LOG.debug("{} Sending GSS response '{}' back to the auth server", exchangeId, tokenstr); + LOG.debug("{} Sending response '{}' back to the auth server", exchangeId, tokenstr); } return StandardAuthScheme.SPNEGO + " " + tokenstr; default: diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosScheme.java index 5b00e0fe1..9b2adb96d 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosScheme.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosScheme.java @@ -41,8 +41,10 @@ import org.ietf.jgss.Oid; * * @since 4.2 * - * @deprecated Do not use. Consider using Spengo, Basic or Bearer authentication with TLS instead. + * @deprecated Do not use. The Kerberos authentication scheme was never standardised. + * Use {@link MutualSpnegoScheme} or some other scheme instead. * + * @see MutualSpnegoScheme * @see BasicScheme * @see BearerScheme */ diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosSchemeFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosSchemeFactory.java index 25930f099..9c6cf9349 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosSchemeFactory.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosSchemeFactory.java @@ -45,9 +45,10 @@ import org.apache.hc.core5.http.protocol.HttpContext; * * @since 4.2 * - * @deprecated Do not use. The GGS based experimental authentication schemes are no longer - * supported. Consider using Basic or Bearer authentication with TLS instead. + * @deprecated Do not use. The Kerberos authentication scheme was never standardised. + * Use {@link MutualSpnegoScheme} or some other scheme instead. * + * @see MutualSpnegoSchemeFactory * @see BasicSchemeFactory * @see BearerSchemeFactory */ diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualGssSchemeBase.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualGssSchemeBase.java new file mode 100644 index 000000000..9fbee575a --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualGssSchemeBase.java @@ -0,0 +1,348 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.client5.http.impl.auth; + +import java.net.UnknownHostException; +import java.security.Principal; + +import org.apache.hc.client5.http.DnsResolver; +import org.apache.hc.client5.http.SystemDefaultDnsResolver; +import org.apache.hc.client5.http.auth.AuthChallenge; +import org.apache.hc.client5.http.auth.AuthScheme2; +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.AuthenticationException; +import org.apache.hc.client5.http.auth.Credentials; +import org.apache.hc.client5.http.auth.CredentialsProvider; +import org.apache.hc.client5.http.auth.InvalidCredentialsException; +import org.apache.hc.client5.http.auth.StandardAuthScheme; +import org.apache.hc.client5.http.auth.KerberosConfig; +import org.apache.hc.client5.http.protocol.HttpClientContext; +import org.apache.hc.client5.http.utils.Base64; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.util.Args; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.GSSName; +import org.ietf.jgss.Oid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Common behaviour for the new mutual authentication capable {@code GSS} based authentication + * schemes. + * + * This class is derived from the old {@link GGSScheme} class, which was deprecated in 5.3. + * + * @since 5.5 + * + * @see GGSSchemeBase + */ +public abstract class MutualGssSchemeBase implements AuthScheme2 { + + enum State { + UNINITIATED, + TOKEN_READY, + TOKEN_SENT, + SUCCEEDED, + FAILED, + } + + private static final Logger LOG = LoggerFactory.getLogger(MutualGssSchemeBase.class); + private static final String NO_TOKEN = ""; + private static final String KERBEROS_SCHEME = "HTTP"; + + // The GSS spec does not specify how long the conversation can be. This should be plenty. + // Realistically, we get one initial token, then one maybe one more for mutual authentication. + private static final int MAX_GSS_CHALLENGES = 3; + private final KerberosConfig config; + private final DnsResolver dnsResolver; + private final boolean mutualAuth; + private int challengesLeft = MAX_GSS_CHALLENGES; + + /** Authentication process state */ + private State state; + private GSSCredential gssCredential; + private GSSContext gssContext; + private String challenge; + private byte[] queuedToken = new byte[0]; + + MutualGssSchemeBase(final KerberosConfig config, final DnsResolver dnsResolver) { + super(); + this.config = config != null ? config : KerberosConfig.DEFAULT; + this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE; + this.mutualAuth = config.getRequestMutualAuth() == KerberosConfig.Option.ENABLE; + this.state = State.UNINITIATED; + } + + MutualGssSchemeBase(final KerberosConfig config) { + this(config, SystemDefaultDnsResolver.INSTANCE); + } + + MutualGssSchemeBase() { + this(KerberosConfig.DEFAULT, SystemDefaultDnsResolver.INSTANCE); + } + + @Override + public String getRealm() { + return null; + } + + // The AuthScheme API maps awkwardly to GSSAPI, where proccessChallange and generateAuthResponse + // map to the same single method call. Hence the generated token is only stored in this method. + @Override + public void processChallenge( + final HttpHost host, + final AuthChallenge authChallenge, + final HttpContext context, + final boolean challenged) throws AuthenticationException { + + if (challengesLeft-- <= 0 ) { + if (LOG.isDebugEnabled()) { + final HttpClientContext clientContext = HttpClientContext.cast(context); + final String exchangeId = clientContext.getExchangeId(); + LOG.debug("{} GSS error: too many challenges received. Infinite loop ?", exchangeId); + } + // TODO: Should we throw an exception ? There is a test for this behaviour. + state = State.FAILED; + return; + } + + final byte[] challengeToken = Base64.decodeBase64(authChallenge == null ? null : authChallenge.getValue()); + + final String gssHostname; + String hostname = host.getHostName(); + if (config.getUseCanonicalHostname() != KerberosConfig.Option.DISABLE) { + try { + hostname = dnsResolver.resolveCanonicalHostname(host.getHostName()); + } catch (final UnknownHostException ignore) { + } + } + if (config.getStripPort() != KerberosConfig.Option.DISABLE) { + gssHostname = hostname; + } else { + gssHostname = hostname + ":" + host.getPort(); + } + + if (LOG.isDebugEnabled()) { + final HttpClientContext clientContext = HttpClientContext.cast(context); + final String exchangeId = clientContext.getExchangeId(); + LOG.debug("{} GSS init {}", exchangeId, gssHostname); + } + try { + queuedToken = generateToken(challengeToken, KERBEROS_SCHEME, gssHostname); + switch (state) { + case UNINITIATED: + if (challenge != NO_TOKEN) { + if (LOG.isDebugEnabled()) { + final HttpClientContext clientContext = HttpClientContext.cast(context); + final String exchangeId = clientContext.getExchangeId(); + LOG.debug("{} Internal GSS error: token received when none was sent yet: {}", exchangeId, challengeToken); + } + // TODO Should we fail ? That would break existing tests that send a token + // in the first response, which is against the RFC. + } + state = State.TOKEN_READY; + break; + case TOKEN_SENT: + if (challenged) { + state = State.TOKEN_READY; + } else if (mutualAuth) { + // We should have received a valid mutualAuth token + if (!gssContext.isEstablished()) { + if (LOG.isDebugEnabled()) { + final HttpClientContext clientContext = + HttpClientContext.cast(context); + final String exchangeId = clientContext.getExchangeId(); + LOG.debug("{} GSSContext is not established ", exchangeId); + } + state = State.FAILED; + // TODO should we have specific exception(s) for these ? + throw new AuthenticationException( + "requireMutualAuth is set but GSSContext is not established"); + } else if (!gssContext.getMutualAuthState()) { + if (LOG.isDebugEnabled()) { + final HttpClientContext clientContext = + HttpClientContext.cast(context); + final String exchangeId = clientContext.getExchangeId(); + LOG.debug("{} requireMutualAuth is set but GSSAUthContext does not have" + + " mutualAuthState set", exchangeId); + } + state = State.FAILED; + throw new AuthenticationException( + "requireMutualAuth is set but GSSContext mutualAuthState is not set"); + } else { + state = State.SUCCEEDED; + } + } + break; + default: + state = State.FAILED; + throw new IllegalStateException("Illegal state: " + state); + + } + } catch (final GSSException gsse) { + state = State.FAILED; + if (gsse.getMajor() == GSSException.DEFECTIVE_CREDENTIAL + || gsse.getMajor() == GSSException.CREDENTIALS_EXPIRED) { + throw new InvalidCredentialsException(gsse.getMessage(), gsse); + } + if (gsse.getMajor() == GSSException.NO_CRED) { + throw new InvalidCredentialsException(gsse.getMessage(), gsse); + } + if (gsse.getMajor() == GSSException.DEFECTIVE_TOKEN + || gsse.getMajor() == GSSException.DUPLICATE_TOKEN + || gsse.getMajor() == GSSException.OLD_TOKEN) { + throw new AuthenticationException(gsse.getMessage(), gsse); + } + // other error + throw new AuthenticationException(gsse.getMessage(), gsse); + } + } + + protected GSSManager getManager() { + return GSSManager.getInstance(); + } + + /** + * @since 4.4 + */ + protected byte[] generateGSSToken( + final byte[] input, final Oid oid, final String gssServiceName, final String gssHostname) throws GSSException { + final GSSManager manager = getManager(); + final GSSName peerName = manager.createName(gssServiceName + "@" + gssHostname, GSSName.NT_HOSTBASED_SERVICE); + + if (gssContext == null) { + gssContext = createGSSContext(manager, oid, peerName, gssCredential); + } + if (input != null) { + return gssContext.initSecContext(input, 0, input.length); + } + return gssContext.initSecContext(new byte[] {}, 0, 0); + } + + /** + * @since 5.0 + */ + protected GSSContext createGSSContext( + final GSSManager manager, + final Oid oid, + final GSSName peerName, + final GSSCredential gssCredential) throws GSSException { + final GSSContext gssContext = manager.createContext(peerName.canonicalize(oid), oid, gssCredential, + GSSContext.DEFAULT_LIFETIME); + gssContext.requestMutualAuth(true); + if (config.getRequestDelegCreds() != KerberosConfig.Option.DEFAULT) { + gssContext.requestCredDeleg(config.getRequestDelegCreds() == KerberosConfig.Option.ENABLE); + } + if (config.getRequestMutualAuth() != KerberosConfig.Option.DEFAULT) { + gssContext.requestMutualAuth(config.getRequestMutualAuth() == KerberosConfig.Option.ENABLE); + } + return gssContext; + } + /** + * @since 4.4 + */ + protected abstract byte[] generateToken(byte[] input, String gssServiceName, String gssHostname) throws GSSException; + + @Override + public boolean isChallengeComplete() { + // For the mutual authentication response, this is should technically return true. + // However, the HttpAuthenticator immediately fails the authentication + // process if we return true, so we only return true here if the authentication has failed. + return this.state == State.FAILED; + } + + @Override + public boolean isChallengeExpected() { + return state == State.TOKEN_SENT && mutualAuth; + } + + @Override + public boolean isResponseReady( + final HttpHost host, + final CredentialsProvider credentialsProvider, + final HttpContext context) throws AuthenticationException { + + Args.notNull(host, "Auth host"); + Args.notNull(credentialsProvider, "CredentialsProvider"); + + final Credentials credentials = credentialsProvider.getCredentials( + new AuthScope(host, null, getName()), context); + if (credentials instanceof org.apache.hc.client5.http.auth.KerberosCredentials) { + this.gssCredential = ((org.apache.hc.client5.http.auth.KerberosCredentials) credentials).getGSSCredential(); + } else { + this.gssCredential = null; + } + return true; + } + + @Override + public Principal getPrincipal() { + return null; + } + + // Format the queued token and update the state. + // All token processing is done in processChallenge() + @Override + public String generateAuthResponse( + final HttpHost host, + final HttpRequest request, + final HttpContext context) throws AuthenticationException { + Args.notNull(host, "HTTP host"); + Args.notNull(request, "HTTP request"); + switch (state) { + case UNINITIATED: + throw new AuthenticationException(getName() + " authentication has not been initiated"); + case FAILED: + throw new AuthenticationException(getName() + " authentication has failed"); + case SUCCEEDED: + return null; + case TOKEN_READY: + state = State.TOKEN_SENT; + final Base64 codec = new Base64(0); + final String tokenstr = new String(codec.encode(queuedToken)); + if (LOG.isDebugEnabled()) { + final HttpClientContext clientContext = HttpClientContext.cast(context); + final String exchangeId = clientContext.getExchangeId(); + LOG.debug("{} Sending GSS response '{}' back to the auth server", exchangeId, tokenstr); + } + return StandardAuthScheme.SPNEGO + " " + tokenstr; + default: + throw new IllegalStateException("Illegal state: " + state); + } + } + + @Override + public String toString() { + return getName() + "{" + this.state + " " + challenge + '}'; + } + +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualSpnegoScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualSpnegoScheme.java new file mode 100644 index 000000000..5403912ba --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualSpnegoScheme.java @@ -0,0 +1,113 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.client5.http.impl.auth; + +import org.apache.hc.client5.http.AuthenticationStrategy; +import org.apache.hc.client5.http.DnsResolver; +import org.apache.hc.client5.http.auth.StandardAuthScheme; +import org.apache.hc.core5.annotation.Experimental; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.Oid; + +/** + * SPNEGO (Simple and Protected GSSAPI Negotiation Mechanism) authentication + * scheme. + *

+ * This is the new mutual authentication capable Scheme which replaces the old deprecated non mutual + * authentication capable {@link SPNegoScheme} + *

+ * + *

+ * Note that this scheme is not enabled by default. To use it, you need create a custom + * {@link AuthenticationStrategy} and a custom {@link AuthSchemeFactory} {@link Registry}, + * and set them on the HttpClientBuilder. + *

+ * + *
+ * {@code
+ * private static class SpnegoAuthenticationStrategy extends DefaultAuthenticationStrategy {
+ *   private static final List SPNEGO_SCHEME_PRIORITY =
+ *       Collections.unmodifiableList(
+ *           Arrays.asList(StandardAuthScheme.SPNEGO
+ *           // Add other Schemes as needed
+ *           );
+ *
+ *   @Override
+ *   protected final List getSchemePriority() {
+ *     return SPNEGO_SCHEME_PRIORITY;
+ *   }
+ * }
+ *
+ * AuthenticationStrategy mutualStrategy = new SpnegoAuthenticationStrategy();
+ *
+ * AuthSchemeFactory mutualFactory = new MutualSpnegoSchemeFactory();
+ * Registry mutualSchemeRegistry = RegistryBuilder.create()
+ *     .register(StandardAuthScheme.SPNEGO, mutualFactory)
+ *     //register other schemes as needed
+ *     .build();
+ *
+ * CloseableHttpClient mutualClient = HttpClientBuilder.create()
+ *    .setTargetAuthenticationStrategy(mutualStrategy);
+ *    .setDefaultAuthSchemeRegistry(mutualSchemeRegistry);
+ *    .build();
+ * }
+ * 
+ * + * @since 5.5 + */ +@Experimental +public class MutualSpnegoScheme extends MutualGssSchemeBase { + + private static final String SPNEGO_OID = "1.3.6.1.5.5.2"; + + /** + * @since 5.0 + */ + public MutualSpnegoScheme(final org.apache.hc.client5.http.auth.KerberosConfig config, final DnsResolver dnsResolver) { + super(config, dnsResolver); + } + + public MutualSpnegoScheme() { + super(); + } + + @Override + public String getName() { + return StandardAuthScheme.SPNEGO; + } + + @Override + protected byte[] generateToken(final byte[] input, final String gssServiceName, final String gssHostname) throws GSSException { + return generateGSSToken(input, new Oid(SPNEGO_OID), gssServiceName, gssHostname); + } + + @Override + public boolean isConnectionBased() { + return true; + } + +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualSpnegoSchemeFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualSpnegoSchemeFactory.java new file mode 100644 index 000000000..5880f47ad --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualSpnegoSchemeFactory.java @@ -0,0 +1,76 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.client5.http.impl.auth; + +import org.apache.hc.client5.http.DnsResolver; +import org.apache.hc.client5.http.SystemDefaultDnsResolver; +import org.apache.hc.client5.http.auth.AuthScheme; +import org.apache.hc.client5.http.auth.AuthSchemeFactory; +import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.Experimental; +import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.http.protocol.HttpContext; + +/** + * {@link AuthSchemeFactory} implementation that creates and initialises + * {@link MutualSpnegoScheme} instances. + *

+ * This replaces the old deprecated {@link SPNegoSchemeFactory} + *

+ * + * @since 5.5 + * + * @see SPNegoSchemeFactory + */ +@Contract(threading = ThreadingBehavior.STATELESS) +@Experimental +public class MutualSpnegoSchemeFactory implements AuthSchemeFactory { + + /** + * Singleton instance for the default configuration. + */ + public static final MutualSpnegoSchemeFactory DEFAULT = new MutualSpnegoSchemeFactory(org.apache.hc.client5.http.auth.KerberosConfig.DEFAULT, + SystemDefaultDnsResolver.INSTANCE); + + private final org.apache.hc.client5.http.auth.KerberosConfig config; + private final DnsResolver dnsResolver; + + /** + * @since 5.0 + */ + public MutualSpnegoSchemeFactory(final org.apache.hc.client5.http.auth.KerberosConfig config, final DnsResolver dnsResolver) { + super(); + this.config = config; + this.dnsResolver = dnsResolver; + } + + @Override + public AuthScheme create(final HttpContext context) { + return new MutualSpnegoScheme(this.config, this.dnsResolver); + } + +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoScheme.java index 2ec37febe..6d9f9408f 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoScheme.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoScheme.java @@ -36,12 +36,19 @@ import org.ietf.jgss.Oid; * SPNEGO (Simple and Protected GSSAPI Negotiation Mechanism) authentication * scheme. *

- * Please note this class is considered experimental and may be discontinued or removed - * in the future. + * This class implements the old deprecated non mutual authentication capable SPNEGO implementation. + * Use {@link MutualSpnegoScheme} instead. *

* * @since 4.2 + * + * @deprecated Use {@link MutualSpnegoScheme} or some other auth scheme instead. + * + * @see MutualSpnegoScheme + * @see BasicScheme + * @see BearerScheme */ +@Deprecated @Experimental public class SPNegoScheme extends GGSSchemeBase { @@ -64,8 +71,8 @@ public class SPNegoScheme extends GGSSchemeBase { } @Override - protected byte[] generateToken(final byte[] input, final String gssServiceName, final String gssHostname) throws GSSException { - return generateGSSToken(input, new Oid(SPNEGO_OID), gssServiceName, gssHostname); + protected byte[] generateToken(final byte[] input, final String serviceName, final String authServer) throws GSSException { + return generateGSSToken(input, new Oid(SPNEGO_OID), serviceName, authServer); } @Override diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoSchemeFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoSchemeFactory.java index 14d8528c5..1231d47e4 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoSchemeFactory.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoSchemeFactory.java @@ -39,15 +39,15 @@ import org.apache.hc.core5.http.protocol.HttpContext; * {@link AuthSchemeFactory} implementation that creates and initializes * {@link SPNegoScheme} instances. *

- * Please note this class is considered experimental and may be discontinued or removed - * in the future. + * This factory creates the old deprecated non mutual authentication capable SPNEGO implementation. + * Use {@link MutualSpnegoAuthFactory} instead. *

* * @since 4.2 * - * @deprecated Do not use. The GGS based experimental authentication schemes are no longer - * supported. Consider using Basic or Bearer authentication with TLS instead. + * @deprecated Use {@link MutualSpnegoAuthFactory} or some other auth scheme instead. * + * @see MutualSpnegoAuthFactory * @see BasicSchemeFactory * @see BearerSchemeFactory */