Issue #2868 - Adding SPNEGO authentication support for Jetty Client.

Removed old deprecated SPNEGO implementation on server-side.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
Simone Bordet 2018-10-04 13:02:04 +02:00
parent c25b7196bd
commit 1ca4e12c3b
9 changed files with 55 additions and 592 deletions

View File

@ -19,11 +19,12 @@
[[spnego-support]]
=== SPNEGO Support
Simple and Protected GSSAPI Negotiation Mechanism (SPNEGO) is a way for users to be seamlessly authenticated when running on a Windows or Active Directory based network.
Jetty supports this type of authentication and authorization through the JDK (which has been enabled since the later versions of Java 6 and 7).
Also important to note is that this is an _incredibly_ fragile setup where everything needs to be configured just right for things to work, otherwise it can fail in fun and exciting, not to mention obscure, ways.
Simple and Protected GSSAPI Negotiation Mechanism (SPNEGO) is a way for users
to be seamlessly authenticated when running on systems that rely on Kerberos
for authentication, such as Windows Active Directory based networks.
There is a substantial amount of configuration and testing required to enable this feature as well as knowledge and access to central systems on a Windows network such as the Active Domain Controller and the ability to create and maintain service users.
Jetty supports this type of authentication and authorization through the JDK
(which has been enabled since the later versions of Java 6 and 7).
==== Configuring Jetty and SPNEGO
@ -31,97 +32,62 @@ To run with SPNEGO enabled the following command line options are required:
[source,screen, subs="{sub-order}"]
----
-Djava.security.krb5.conf=/path/to/jetty/etc/krb5.ini \
-Djava.security.auth.login.config=/path/to/jetty/etc/spnego.conf \
-Djavax.security.auth.useSubjectCredsOnly=false
-Djava.security.krb5.conf=/path/to/krb5.ini
----
For debugging the SPNEGO authentication the following options are very helpful:
For debugging the SPNEGO authentication the following options are helpful:
[source,screen, subs="{sub-order}"]
----
-Dorg.eclipse.jetty.LEVEL=debug \
-Dsun.security.spnego.debug=all
-Dorg.eclipse.jetty.LEVEL=debug
-Dsun.security.spnego.debug=true
-Dsun.security.jgss.debug=true
-Dsun.security.krb5.debug=true
----
SPNEGO Authentication must be enabled in the webapp in the following way.
SPNEGO authentication must be enabled in the webapp in the following way.
The name of the role will be different for your network.
[source, xml, subs="{sub-order}"]
----
<security-constraint>
<web-resource-collection>
<web-resource-name>Secure Area</web-resource-name>
<url-pattern>/secure/me/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<!-- this is the domain that the user is a member of -->
<role-name>MORTBAY.ORG</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>SPNEGO</auth-method>
<realm-name>Test Realm</realm-name>
<!-- optionally to add custom error page -->
<spnego-login-config>
<spnego-error-page>/loginError.html?param=foo</spnego-error-page>
</spnego-login-config>
</login-config>
<security-constraint>
<web-resource-collection>
<web-resource-name>Secure Area</web-resource-name>
<url-pattern>/secure/me/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<!-- this is the domain that the user is a member of -->
<role-name>MORTBAY.ORG</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>SPNEGO</auth-method>
<realm-name>Test Realm</realm-name>
<!-- optionally to add custom error page -->
<spnego-login-config>
<spnego-error-page>/loginError.html?param=foo</spnego-error-page>
</spnego-login-config>
</login-config>
----
A corresponding `UserRealm` needs to be created either programmatically if embedded, via the `jetty.xml` or in a context file for the webapp.
A corresponding `UserRealm` needs to be created either programmatically if
embedded, via the `jetty.xml` or in a context file for the webapp.
This is what the configuration within a Jetty xml file would look like.
This is what the configuration within a context XML file would look like:
[source, xml, subs="{sub-order}"]
----
<Call name="addBean">
<Arg>
<New class="org.eclipse.jetty.security.SpnegoLoginService">
<Set name="name">Test Realm</Set>
<Set name="config"><Property name="jetty.home" default="."/>/etc/spnego.properties</Set>
</New>
</Arg>
</Call>
<Get name="securityHandler">
<Set name="loginService">
<New class="org.eclipse.jetty.security.ConfigurableSpnegoLoginService">
<Arg>Test Realm</Arg>
<Arg><Ref refid="authorizationService" /></Arg>
<Set name="keyTabPath"><Ref refid="keyTabPath" /></Set>
</New>
</Set>
</Get>
----
This is what the configuration within a context xml file would look like.
[source, xml, subs="{sub-order}"]
----
<Get name="securityHandler">
<Set name="loginService">
<New class="org.eclipse.jetty.security.SpnegoLoginService">
<Set name="name">Test Realm</Set>
<Set name="config">
<SystemProperty name="jetty.home" default="."/>/etc/spnego.properties
</Set>
</New>
</Set>
<Set name="checkWelcomeFiles">true</Set>
</Get>
----
There are a number of important configuration files with S3pnego that are required. The default values for these configuration files from this
test example are found in the `/etc` folder of the Jetty distribution.
spnego.properties::
configures the user realm with runtime properties
krb5.ini::
configures the underlying kerberos setup
spnego.conf::
configures the glue between gssapi and kerberos
It is important to note that the keytab file referenced in the `krb5.ini` and the `spnego.conf` files needs to contain the keytab for the `targetName` for the http server.
To do this use a process similar to this:
On the Windows Active Domain Controller run:
[source, screen, subs="{sub-order}"]
@ -129,15 +95,15 @@ On the Windows Active Domain Controller run:
$ setspn -A HTTP/linux.mortbay.org ADUser
----
To create the keytab file use the following process:
To create the keyTab file use the following process:
[source, screen, subs="{sub-order}"]
----
$ ktpass -out c:\dir\krb5.keytab -princ HTTP/linux.mortbay.org@MORTBAY.ORG -mapUser ADUser -mapOp set -pass ADUserPWD -crypto RC4-HMAC-NT -pType KRB5_NT_PRINCIPAL
----
This step will give you the keytab file which should then be copied to the machine running the http server and referenced from the configuration files.
For our testing we put the keytab into the `/etc` directory of Jetty and referenced it from there.
This step will give you the keyTab file which should then be copied to the
machine running the http server and referenced from the configuration files.
==== Configuring Firefox
@ -161,7 +127,6 @@ The follows steps have been required to inform Internet Explorer that it should
7. Tools -> Options -> Advanced -> Security -> Ok
8. Close IE then reopen and browse to your SPNEGO protected resource
You *must* use hostname and not the IP.
If you use the IP it will default to NTLM authentication.
The following conditions must be true for SPNEGO authentication to work:

View File

@ -1,62 +0,0 @@
This setup will enable you to authenticate a user via SPNEGO into your
webapp.
To run with SPNEGO enabled the following command line options are required:
-Djava.security.krb5.conf=/path/to/jetty/etc/krb5.ini
-Djava.security.auth.login.config=/path/to/jetty/etc/spnego.conf
-Djavax.security.auth.useSubjectCredsOnly=false
The easiest place to put these lines are in the start.ini file.
For debugging the SPNEGO authentication the following options are helpful:
-Dorg.eclipse.jetty.LEVEL=debug
-Dsun.security.spnego.debug=true
SPNEGO Authentication is enabled in the webapp with the following setup.
<security-constraint>
<web-resource-collection>
<web-resource-name>Secure Area</web-resource-name>
<url-pattern>/secure/me/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>MORTBAY.ORG</role-name> <-- this is the domain that the user is a member of
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>SPNEGO</auth-method>
<realm-name>Test Realm</realm-name>
(optionally to add custom error page)
<spnego-login-config>
<spnego-error-page>/loginError.html?param=foo</spnego-error-page>
</spnego-login-config>
</login-config>
A corresponding UserRealm needs to be created either programmatically if
embedded, via the jetty.xml or in a context file for the webapp.
(in the jetty.xml)
<Call name="addBean">
<Arg>
<New class="org.eclipse.jetty.security.SpnegoLoginService">
<Set name="name">Test Realm</Set>
<Set name="config"><Property name="jetty.home" default="."/>/etc/spnego.properties</Set>
</New>
</Arg>
</Call>
(context file)
<Get name="securityHandler">
<Set name="loginService">
<New class="org.eclipse.jetty.security.SpnegoLoginService">
<Set name="name">Test Realm</Set>
<Set name="config"><SystemProperty name="jetty.home" default="."/>/etc/spnego.properties</Set>
</New>
</Set>
<Set name="checkWelcomeFiles">true</Set>
</Get>

View File

@ -1,23 +0,0 @@
[libdefaults]
default_realm = MORTBAY.ORG
default_keytab_name = FILE:/path/to/jetty/etc/krb5.keytab
permitted_enctypes = aes128-cts aes256-cts arcfour-hmac-md5
default_tgs_enctypes = aes128-cts aes256-cts arcfour-hmac-md5
default_tkt_enctypes = aes128-cts aes256-cts arcfour-hmac-md5
[realms]
MORTBAY.ORG = {
kdc = 192.168.2.30
admin_server = 192.168.2.30
default_domain = MORTBAY.ORG
}
[domain_realm]
mortbay.org= MORTBAY.ORG
.mortbay.org = MORTBAY.ORG
[appdefaults]
autologin = true
forwardable = true

View File

@ -1,19 +0,0 @@
com.sun.security.jgss.initiate {
com.sun.security.auth.module.Krb5LoginModule required
principal="HTTP/vm.mortbay.org@MORTBAY.ORG"
keyTab="/path/to/jetty/etc/krb5.keytab"
useKeyTab=true
storeKey=true
debug=true
isInitiator=false;
};
com.sun.security.jgss.accept {
com.sun.security.auth.module.Krb5LoginModule required
principal="HTTP/vm.mortbay.org@MORTBAY.ORG"
useKeyTab=true
keyTab="/path/to/jetty/etc/krb5.keytab"
storeKey=true
debug=true
isInitiator=false;
};

View File

@ -1 +0,0 @@
targetName = HTTP/vm.mortbay.org

View File

@ -23,9 +23,9 @@ import javax.servlet.ServletContext;
import org.eclipse.jetty.security.Authenticator.AuthConfiguration;
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
import org.eclipse.jetty.security.authentication.ClientCertAuthenticator;
import org.eclipse.jetty.security.authentication.ConfigurableSpnegoAuthenticator;
import org.eclipse.jetty.security.authentication.DigestAuthenticator;
import org.eclipse.jetty.security.authentication.FormAuthenticator;
import org.eclipse.jetty.security.authentication.SpnegoAuthenticator;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.security.Constraint;
@ -66,10 +66,10 @@ public class DefaultAuthenticatorFactory implements Authenticator.Factory
authenticator=new DigestAuthenticator();
else if (Constraint.__FORM_AUTH.equalsIgnoreCase(auth))
authenticator=new FormAuthenticator();
else if ( Constraint.__SPNEGO_AUTH.equalsIgnoreCase(auth) )
authenticator = new SpnegoAuthenticator();
else if ( Constraint.__NEGOTIATE_AUTH.equalsIgnoreCase(auth) ) // see Bug #377076
authenticator = new SpnegoAuthenticator(Constraint.__NEGOTIATE_AUTH);
else if (Constraint.__SPNEGO_AUTH.equalsIgnoreCase(auth))
authenticator = new ConfigurableSpnegoAuthenticator();
else if (Constraint.__NEGOTIATE_AUTH.equalsIgnoreCase(auth)) // see Bug #377076
authenticator = new ConfigurableSpnegoAuthenticator(Constraint.__NEGOTIATE_AUTH);
if (Constraint.__CERT_AUTH.equalsIgnoreCase(auth)||Constraint.__CERT_AUTH2.equalsIgnoreCase(auth))
authenticator=new ClientCertAuthenticator();

View File

@ -1,196 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.security;
import java.util.Properties;
import javax.security.auth.Subject;
import javax.servlet.ServletRequest;
import org.eclipse.jetty.server.UserIdentity;
import org.eclipse.jetty.util.B64Code;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.resource.Resource;
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;
/**
* @deprecated use {@link ConfigurableSpnegoLoginService} instead
*/
@Deprecated
public class SpnegoLoginService extends AbstractLifeCycle implements LoginService
{
private static final Logger LOG = Log.getLogger(SpnegoLoginService.class);
protected IdentityService _identityService;// = new LdapIdentityService();
protected String _name;
private String _config;
private String _targetName;
public SpnegoLoginService()
{
}
public SpnegoLoginService( String name )
{
setName(name);
}
public SpnegoLoginService( String name, String config )
{
setName(name);
setConfig(config);
}
@Override
public String getName()
{
return _name;
}
public void setName(String name)
{
if (isRunning())
{
throw new IllegalStateException("Running");
}
_name = name;
}
public String getConfig()
{
return _config;
}
public void setConfig( String config )
{
if (isRunning())
{
throw new IllegalStateException("Running");
}
_config = config;
}
@Override
protected void doStart() throws Exception
{
Properties properties = new Properties();
Resource resource = Resource.newResource(_config);
properties.load(resource.getInputStream());
_targetName = properties.getProperty("targetName");
LOG.debug("Target Name {}", _targetName);
super.doStart();
}
/**
* username will be null since the credentials will contain all the relevant info
*/
@Override
public UserIdentity login(String username, Object credentials, ServletRequest request)
{
String encodedAuthToken = (String)credentials;
byte[] authToken = B64Code.decode(encodedAuthToken);
GSSManager manager = GSSManager.getInstance();
try
{
Oid krb5Oid = new Oid("1.3.6.1.5.5.2"); // http://java.sun.com/javase/6/docs/technotes/guides/security/jgss/jgss-features.html
GSSName gssName = manager.createName(_targetName,null);
GSSCredential serverCreds = manager.createCredential(gssName,GSSCredential.INDEFINITE_LIFETIME,krb5Oid,GSSCredential.ACCEPT_ONLY);
GSSContext gContext = manager.createContext(serverCreds);
if (gContext == null)
{
LOG.debug("SpnegoUserRealm: failed to establish GSSContext");
}
else
{
while (!gContext.isEstablished())
{
authToken = gContext.acceptSecContext(authToken,0,authToken.length);
}
if (gContext.isEstablished())
{
String clientName = gContext.getSrcName().toString();
String role = clientName.substring(clientName.indexOf('@') + 1);
LOG.debug("SpnegoUserRealm: established a security context");
LOG.debug("Client Principal is: " + gContext.getSrcName());
LOG.debug("Server Principal is: " + gContext.getTargName());
LOG.debug("Client Default Role: " + role);
SpnegoUserPrincipal user = new SpnegoUserPrincipal(clientName,authToken);
Subject subject = new Subject();
subject.getPrincipals().add(user);
return _identityService.newUserIdentity(subject,user, new String[]{role});
}
}
}
catch (GSSException gsse)
{
LOG.warn(gsse);
}
return null;
}
@Override
public boolean validate(UserIdentity user)
{
return false;
}
@Override
public IdentityService getIdentityService()
{
return _identityService;
}
@Override
public void setIdentityService(IdentityService service)
{
_identityService = service;
}
@Override
public void logout(UserIdentity user)
{
// TODO Auto-generated method stub
}
}

View File

@ -1,161 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.security.authentication;
import java.io.IOException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.security.ServerAuthException;
import org.eclipse.jetty.security.UserAuthentication;
import org.eclipse.jetty.server.Authentication;
import org.eclipse.jetty.server.Authentication.User;
import org.eclipse.jetty.server.UserIdentity;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.security.Constraint;
/**
* @deprecated use {@link ConfigurableSpnegoAuthenticator} instead.
*/
@Deprecated
public class SpnegoAuthenticator extends LoginAuthenticator
{
private static final Logger LOG = Log.getLogger(SpnegoAuthenticator.class);
private String _authMethod = Constraint.__SPNEGO_AUTH;
public SpnegoAuthenticator()
{
}
/**
* Allow for a custom authMethod value to be set for instances where SPNEGO may not be appropriate
* @param authMethod the auth method
*/
public SpnegoAuthenticator( String authMethod )
{
_authMethod = authMethod;
}
@Override
public String getAuthMethod()
{
return _authMethod;
}
@Override
public Authentication validateRequest(ServletRequest request, ServletResponse response, boolean mandatory) throws ServerAuthException
{
HttpServletRequest req = (HttpServletRequest)request;
HttpServletResponse res = (HttpServletResponse)response;
String header = req.getHeader(HttpHeader.AUTHORIZATION.asString());
String authScheme = getAuthSchemeFromHeader(header);
if (!mandatory)
{
return new DeferredAuthentication(this);
}
// The client has responded to the challenge we sent previously
if (header != null && isAuthSchemeNegotiate(authScheme))
{
String spnegoToken = header.substring(10);
UserIdentity user = login(null,spnegoToken, request);
if ( user != null )
{
return new UserAuthentication(getAuthMethod(),user);
}
}
// A challenge should be sent if any of the following cases are true:
// 1. There was no Authorization header provided
// 2. There was an Authorization header for a type other than Negotiate
try
{
if (DeferredAuthentication.isDeferred(res))
{
return Authentication.UNAUTHENTICATED;
}
LOG.debug("Sending challenge");
res.setHeader(HttpHeader.WWW_AUTHENTICATE.asString(), HttpHeader.NEGOTIATE.asString());
res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return Authentication.SEND_CONTINUE;
}
catch (IOException ioe)
{
throw new ServerAuthException(ioe);
}
}
/**
* Extracts the auth_scheme from the HTTP Authorization header, {@code Authorization: <auth_scheme> <token>}.
*
* @param header The HTTP Authorization header or null.
* @return The parsed auth scheme from the header, or the empty string.
*/
String getAuthSchemeFromHeader(String header)
{
// No header provided, return the empty string
if (header == null || header.isEmpty())
{
return "";
}
// Trim any leading whitespace
String trimmed_header = header.trim();
// Find the first space, all characters prior should be the auth_scheme
int index = trimmed_header.indexOf(' ');
if (index > 0) {
return trimmed_header.substring(0, index);
}
// If we don't find a space, this is likely malformed, just return the entire value
return trimmed_header;
}
/**
* Determines if provided auth scheme text from the Authorization header is case-insensitively
* equal to {@code negotiate}.
*
* @param authScheme The auth scheme component of the Authorization header
* @return True if the auth scheme component is case-insensitively equal to {@code negotiate}, False otherwise.
*/
boolean isAuthSchemeNegotiate(String authScheme)
{
if (authScheme == null || authScheme.length() != HttpHeader.NEGOTIATE.asString().length())
{
return false;
}
// Headers should be treated case-insensitively, so we have to jump through some extra hoops.
return authScheme.equalsIgnoreCase(HttpHeader.NEGOTIATE.asString());
}
@Override
public boolean secureResponse(ServletRequest request, ServletResponse response, boolean mandatory, User validatedUser) throws ServerAuthException
{
return true;
}
}

View File

@ -18,10 +18,6 @@
package org.eclipse.jetty.security.authentication;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpFields;
@ -38,16 +34,15 @@ import org.eclipse.jetty.server.Server;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* Test class for {@link SpnegoAuthenticator}.
*/
import static org.junit.jupiter.api.Assertions.assertEquals;
public class SpnegoAuthenticatorTest {
private SpnegoAuthenticator _authenticator;
private ConfigurableSpnegoAuthenticator _authenticator;
@BeforeEach
public void setup() throws Exception
public void setup()
{
_authenticator = new SpnegoAuthenticator();
_authenticator = new ConfigurableSpnegoAuthenticator();
}
@Test
@ -67,7 +62,6 @@ public class SpnegoAuthenticatorTest {
@Override
public void close()
{
return;
}
};
Response res = new Response(channel, out);
@ -97,7 +91,6 @@ public class SpnegoAuthenticatorTest {
@Override
public void close()
{
return;
}
};
Response res = new Response(channel, out);
@ -112,37 +105,4 @@ public class SpnegoAuthenticatorTest {
assertEquals(HttpHeader.NEGOTIATE.asString(), res.getHeader(HttpHeader.WWW_AUTHENTICATE.asString()));
assertEquals(HttpServletResponse.SC_UNAUTHORIZED, res.getStatus());
}
@Test
public void testCaseInsensitiveHeaderParsing()
{
assertFalse(_authenticator.isAuthSchemeNegotiate(null));
assertFalse(_authenticator.isAuthSchemeNegotiate(""));
assertFalse(_authenticator.isAuthSchemeNegotiate("Basic"));
assertFalse(_authenticator.isAuthSchemeNegotiate("basic"));
assertFalse(_authenticator.isAuthSchemeNegotiate("Digest"));
assertFalse(_authenticator.isAuthSchemeNegotiate("LotsandLotsandLots of nonsense"));
assertFalse(_authenticator.isAuthSchemeNegotiate("Negotiat asdfasdf"));
assertFalse(_authenticator.isAuthSchemeNegotiate("Negotiated"));
assertFalse(_authenticator.isAuthSchemeNegotiate("Negotiate-and-more"));
assertTrue(_authenticator.isAuthSchemeNegotiate("Negotiate"));
assertTrue(_authenticator.isAuthSchemeNegotiate("negotiate"));
assertTrue(_authenticator.isAuthSchemeNegotiate("negOtiAte"));
}
@Test
public void testExtractAuthScheme()
{
assertEquals("", _authenticator.getAuthSchemeFromHeader(null));
assertEquals("", _authenticator.getAuthSchemeFromHeader(""));
assertEquals("", _authenticator.getAuthSchemeFromHeader(" "));
assertEquals("Basic", _authenticator.getAuthSchemeFromHeader(" Basic asdfasdf"));
assertEquals("Basicasdf", _authenticator.getAuthSchemeFromHeader("Basicasdf asdfasdf"));
assertEquals("basic", _authenticator.getAuthSchemeFromHeader(" basic asdfasdf "));
assertEquals("Negotiate", _authenticator.getAuthSchemeFromHeader("Negotiate asdfasdf"));
assertEquals("negotiate", _authenticator.getAuthSchemeFromHeader("negotiate asdfasdf"));
assertEquals("negotiate", _authenticator.getAuthSchemeFromHeader(" negotiate asdfasdf"));
assertEquals("negotiated", _authenticator.getAuthSchemeFromHeader(" negotiated asdfasdf"));
}
}