/*******************************************************************************
* Copyright (c) 2007, Dave Whitla
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
* OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package au.net.ocean.httpclient.auth.spnego;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.auth.AuthScheme;
import org.apache.commons.httpclient.auth.AuthenticationException;
import org.apache.commons.httpclient.auth.CredentialsNotAvailableException;
import org.apache.commons.httpclient.auth.InvalidCredentialsException;
import org.apache.commons.httpclient.auth.MalformedChallengeException;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Created by dwhitla at Apr 15, 2007 4:20:35 PM
*
* @author <a href="mailto:dave.whitla@ocean.net.au">Dave Whitla</a>
* @version $Id: SPNegoAuthScheme.java 0 Apr 15, 2007 4:20:35 PM dwhitla $
*/
public class SPNegoAuthScheme implements AuthScheme {
public static final String IDENTIFIER = "Negotiate";
private static final Logger LOGGER = Logger.getLogger(SPNegoAuthScheme.class.getName());
private static final String CHALLENGE = "Negotiate";
private static final String TOKEN_PREFIX = "Negotiate";
private static final Base64 BASE64_CODEC = new Base64();
private byte[] serverToken;
private boolean complete;
/**
* Processes the given challenge token. Some authentication schemes
* may involve multiple challenge-response exchanges. Such schemes must be able
* to maintain the state information when dealing with sequential challenges
*
* @param challenge the challenge string
* @since 3.0
*/
public void processChallenge(final String challenge) throws MalformedChallengeException {
if (challenge == null) {
throw new MalformedChallengeException("Null challenge");
}
String[] tokens = challenge.trim().split("\\s+");
if (!tokens[0].equalsIgnoreCase(CHALLENGE)) {
LOGGER.log(Level.WARNING, "Received malformed challenge: \"{0}\"", challenge);
throw new MalformedChallengeException(challenge);
}
switch (tokens.length) {
case 1:
LOGGER.log(Level.INFO, "Received initial \"{0}\" challenge", CHALLENGE);
serverToken = new byte[0];
break;
case 2:
LOGGER.log(Level.INFO, "Received \"{0}\" challenge with token \"{1}\"", new String[]{CHALLENGE, tokens[1]});
serverToken = BASE64_CODEC.decode(tokens[1].getBytes());
break;
default:
LOGGER.log(Level.WARNING, "Received malformed challenge: \"{0}\"", challenge);
throw new MalformedChallengeException();
}
}
/**
* @InheritDoc
*/
public String getSchemeName() {
return IDENTIFIER;
}
/**
* Returns authentication parameter with the given name, if available.
*
* @param name The name of the parameter to be returned
* @return the parameter with the given name
*/
public String getParameter(final String name) {
throw new UnsupportedOperationException("Not yet implemented");
// return null;
}
/**
* Returns authentication realm. If the concept of an authentication
* realm is not applicable to the given authentication scheme, returns
* <code>null</code>.
*
* @return the authentication realm
*/
public String getRealm() {
return null;
}
/**
* Tests if the authentication scheme provides authorization on a per
* connection basis instead of usual per request basis
*
* @return <tt>true</tt> if the scheme is connection based, <tt>false</tt> if the scheme is request based.
* @since 3.0
*/
public boolean isConnectionBased() {
return true;
}
/**
* Authentication process may involve a series of challenge-response exchanges.
* This method tests if the authorization process has been completed, either
* successfully or unsuccessfully, that is, all the required authorization
* challenges have been processed in their entirety.
*
* @return <tt>true</tt> if the authentication process has been completed,
* <tt>false</tt> otherwise.
* @since 3.0
*/
public boolean isComplete() {
return complete;
}
/**
* Produces an authorization string for the given set of {@link org.apache.commons.httpclient.Credentials}.
*
* @param credentials The set of credentials to be used for athentication
* @param method The method being authenticated
* @return the authorization string
* @throws org.apache.commons.httpclient.auth.AuthenticationException
* if authorization string cannot
* be generated due to an authentication failure
* @since 3.0
*/
public String authenticate(Credentials credentials, HttpMethod method) throws AuthenticationException {
try {
SPNegoCredentials spnegoCredentials;
try {
spnegoCredentials = (SPNegoCredentials) credentials;
} catch (ClassCastException e) {
throw new InvalidCredentialsException(
"Credentials cannot be used for SPNego authentication: " + credentials.getClass().getName());
}
GSSContext gssContext = spnegoCredentials.getGSSContext();
byte[] clientToken = gssContext.initSecContext(serverToken, 0, serverToken.length);
if (gssContext.isEstablished()) {
complete = true;
LOGGER.log(Level.INFO, "GSS Context established");
LOGGER.log(Level.INFO, "Caller is " + gssContext.getSrcName());
LOGGER.log(Level.INFO, "Server is " + gssContext.getTargName());
if (gssContext.getMutualAuthState()) {
LOGGER.log(Level.INFO, "Mutually authenticated");
}
}
String encodedToken = new String(BASE64_CODEC.encode(clientToken));
return new StringBuffer(TOKEN_PREFIX).append(' ').append(encodedToken).toString();
} catch (GSSException e) {
complete = true;
switch (e.getMajor()) {
case GSSException.CREDENTIALS_EXPIRED:
throw new InvalidCredentialsException(e.getMessage(), e);
case GSSException.NO_CRED:
throw new CredentialsNotAvailableException(e.getMessage(), e);
default:
String errorMessage = "Caught GSSException in GSSContext.initSecContext()";
LOGGER.log(Level.SEVERE, errorMessage, e);
throw new AuthenticationException(errorMessage, e);
}
}
}
// DEPRECATED METHODS
/**
* Returns a String identifying the authentication challenge. This is
* used, in combination with the host and port to determine if
* authorization has already been attempted or not. Schemes which
* require multiple requests to complete the authentication should
* return a different value for each stage in the request.
* <p/>
* <p>Additionally, the ID should take into account any changes to the
* authentication challenge and return a different value when appropriate.
* For example when the realm changes in basic authentication it should be
* considered a different authentication attempt and a different value should
* be returned.</p>
*
* @return String a String identifying the authentication challenge. The
* returned value may be null.
* @deprecated no longer used
*/
public String getID() {
throw new UnsupportedOperationException("Deprecated.");
}
/**
* @param credentials The set of credentials to be used for athentication
* @param method The name of the method that requires authorization.
* This parameter may be ignored, if it is irrelevant
* or not applicable to the given authentication scheme
* @param uri The URI for which authorization is needed.
* This parameter may be ignored, if it is irrelevant or not
* applicable to the given authentication scheme
* @return the authorization string
* @throws org.apache.commons.httpclient.auth.AuthenticationException
* if authorization string cannot
* be generated due to an authentication failure
* @see org.apache.commons.httpclient.HttpMethod#getName()
* @see org.apache.commons.httpclient.HttpMethod#getPath()
* @deprecated Use {@link #authenticate(org.apache.commons.httpclient.Credentials,org.apache.commons.httpclient.HttpMethod)}
* <p/>
* Produces an authorization string for the given set of {@link org.apache.commons.httpclient.Credentials},
* method name and URI using the given authentication scheme in response to
* the actual authorization challenge.
*/
public String authenticate(Credentials credentials, String method, String uri) throws AuthenticationException {
throw new UnsupportedOperationException(
"Deprecated. Use authenticate(org.apache.commons.httpclient.Credentials," +
" org.apache.commons.httpclient.HttpMethod) instead.");
}
}