duplicate and rename mutual SPNEGO related classes

This commit is contained in:
Istvan Toth 2024-09-30 13:54:14 +02:00
parent 66dca67f9a
commit a23b9f84cd
9 changed files with 639 additions and 167 deletions

View File

@ -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);

View File

@ -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:

View File

@ -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
*/

View File

@ -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
*/

View File

@ -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
* <http://www.apache.org/>.
*
*/
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 + '}';
}
}

View File

@ -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
* <http://www.apache.org/>.
*
*/
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.
* <p>
* This is the new mutual authentication capable Scheme which replaces the old deprecated non mutual
* authentication capable {@link SPNegoScheme}
* </p>
*
* <p>
* 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.
* </p>
*
* <pre>
* {@code
* private static class SpnegoAuthenticationStrategy extends DefaultAuthenticationStrategy {
* private static final List<String> SPNEGO_SCHEME_PRIORITY =
* Collections.unmodifiableList(
* Arrays.asList(StandardAuthScheme.SPNEGO
* // Add other Schemes as needed
* );
*
* @Override
* protected final List<String> getSchemePriority() {
* return SPNEGO_SCHEME_PRIORITY;
* }
* }
*
* AuthenticationStrategy mutualStrategy = new SpnegoAuthenticationStrategy();
*
* AuthSchemeFactory mutualFactory = new MutualSpnegoSchemeFactory();
* Registry<AuthSchemeFactory> mutualSchemeRegistry = RegistryBuilder.<AuthSchemeFactory>create()
* .register(StandardAuthScheme.SPNEGO, mutualFactory)
* //register other schemes as needed
* .build();
*
* CloseableHttpClient mutualClient = HttpClientBuilder.create()
* .setTargetAuthenticationStrategy(mutualStrategy);
* .setDefaultAuthSchemeRegistry(mutualSchemeRegistry);
* .build();
* }
* </pre>
*
* @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;
}
}

View File

@ -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
* <http://www.apache.org/>.
*
*/
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.
* <p>
* This replaces the old deprecated {@link SPNegoSchemeFactory}
* </p>
*
* @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);
}
}

View File

@ -36,12 +36,19 @@ import org.ietf.jgss.Oid;
* SPNEGO (Simple and Protected GSSAPI Negotiation Mechanism) authentication
* scheme.
* <p>
* 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.
* </p>
*
* @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

View File

@ -39,15 +39,15 @@ import org.apache.hc.core5.http.protocol.HttpContext;
* {@link AuthSchemeFactory} implementation that creates and initializes
* {@link SPNegoScheme} instances.
* <p>
* 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.
* </p>
*
* @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
*/