duplicate and rename mutual SPNEGO related classes
This commit is contained in:
parent
66dca67f9a
commit
a23b9f84cd
|
@ -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);
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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 + '}';
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue