/* * JBoss, Home of Professional Open Source. * Copyright 2012, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.test.integration.security.common.negotiation; import org.apache.commons.codec.binary.Base64; import org.apache.http.Header; import org.apache.http.HttpHost; import org.apache.http.HttpRequest; import org.apache.http.auth.AUTH; import org.apache.http.auth.AuthenticationException; import org.apache.http.auth.Credentials; import org.apache.http.auth.InvalidCredentialsException; import org.apache.http.auth.MalformedChallengeException; import org.apache.http.impl.auth.AuthSchemeBase; import org.apache.http.message.BasicHeader; import org.apache.http.message.BufferedHeader; import org.apache.http.protocol.ExecutionContext; import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.HttpCoreContext; import org.apache.http.util.CharArrayBuffer; import org.ietf.jgss.GSSContext; import org.ietf.jgss.GSSException; import org.ietf.jgss.GSSManager; import org.ietf.jgss.GSSName; import org.ietf.jgss.Oid; import org.jboss.logging.Logger; /** * SPNEGO (Simple and Protected GSSAPI Negotiation Mechanism) authentication scheme. It's based on NegotiateScheme class from * Apache HC, it fixes DEFAULT_LIFETIME problem with IBM JDK. * <p> * This class could extend {@link org.apache.http.impl.auth.SPNegoScheme SPNegoScheme} when the <a * href="https://issues.apache.org/jira/browse/HTTPCLIENT-1305">HTTPCLIENT-1305</a> is fixed. * * @author Josef Cacek */ public class JBossNegotiateScheme extends AuthSchemeBase { private static final Logger LOGGER = Logger.getLogger(JBossNegotiateScheme.class); /** The DEFAULT_LIFETIME */ private static final int DEFAULT_LIFETIME = 60; private static final String SPNEGO_OID = "1.3.6.1.5.5.2"; enum State { UNINITIATED, CHALLENGE_RECEIVED, TOKEN_GENERATED, FAILED, } private final boolean stripPort; /** Authentication process state */ private State state; /** base64 decoded challenge **/ private byte[] token; private final Base64 base64codec; // Constructors ---------------------------------------------------------- /** * Default constructor for the Negotiate authentication scheme. */ public JBossNegotiateScheme(boolean stripPort) { super(); this.state = State.UNINITIATED; this.stripPort = stripPort; this.base64codec = new Base64(0); } // Public methods -------------------------------------------------------- /** * Tests if the Negotiate authentication process has been completed. * * @return <tt>true</tt> if authorization has been processed, <tt>false</tt> otherwise. */ public boolean isComplete() { return this.state == State.TOKEN_GENERATED || this.state == State.FAILED; } /** * Returns textual designation of the Negotiate authentication scheme. * * @return <code>Negotiate</code> */ public String getSchemeName() { return "Negotiate"; } @Deprecated public Header authenticate(final Credentials credentials, final HttpRequest request) throws AuthenticationException { return authenticate(credentials, request, null); } /** * Produces Negotiate authorization Header based on token created by processChallenge. * * @param credentials Never used be the Negotiate scheme but must be provided to satisfy common-httpclient API. Credentials * from JAAS will be used instead. * @param request The request being authenticated * * @throws AuthenticationException if authorization string cannot be generated due to an authentication failure * * @return an Negotiate authorization Header */ @Override public Header authenticate(final Credentials credentials, final HttpRequest request, final HttpContext context) throws AuthenticationException { if (request == null) { throw new IllegalArgumentException("HTTP request may not be null"); } if (state == State.TOKEN_GENERATED) { // hack for auto redirects return new BasicHeader("X-dummy", "Token already generated"); } if (state != State.CHALLENGE_RECEIVED) { throw new IllegalStateException("Negotiation authentication process has not been initiated"); } try { String key = null; if (isProxy()) { key = ExecutionContext.HTTP_PROXY_HOST; } else { key = HttpCoreContext.HTTP_TARGET_HOST; } HttpHost host = (HttpHost) context.getAttribute(key); if (host == null) { throw new AuthenticationException("Authentication host is not set " + "in the execution context"); } String authServer; if (!this.stripPort && host.getPort() > 0) { authServer = host.toHostString(); } else { authServer = host.getHostName(); } if (LOGGER.isDebugEnabled()) { LOGGER.debug("init " + authServer); } final Oid negotiationOid = new Oid(SPNEGO_OID); final GSSManager manager = GSSManager.getInstance(); final GSSName serverName = manager.createName("HTTP@" + authServer, GSSName.NT_HOSTBASED_SERVICE); final GSSContext gssContext = manager.createContext(serverName.canonicalize(negotiationOid), negotiationOid, null, DEFAULT_LIFETIME); gssContext.requestMutualAuth(true); gssContext.requestCredDeleg(true); if (token == null) { token = new byte[0]; } token = gssContext.initSecContext(token, 0, token.length); if (token == null) { state = State.FAILED; throw new AuthenticationException("GSS security context initialization failed"); } state = State.TOKEN_GENERATED; String tokenstr = new String(base64codec.encode(token)); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Sending response '" + tokenstr + "' back to the auth server"); } CharArrayBuffer buffer = new CharArrayBuffer(32); if (isProxy()) { buffer.append(AUTH.PROXY_AUTH_RESP); } else { buffer.append(AUTH.WWW_AUTH_RESP); } buffer.append(": Negotiate "); buffer.append(tokenstr); return new BufferedHeader(buffer); } catch (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()); } } /** * Returns the authentication parameter with the given name, if available. * * <p> * There are no valid parameters for Negotiate authentication so this method always returns <tt>null</tt>. * </p> * * @param name The name of the parameter to be returned * * @return the parameter with the given name */ public String getParameter(String name) { if (name == null) { throw new IllegalArgumentException("Parameter name may not be null"); } return null; } /** * The concept of an authentication realm is not supported by the Negotiate authentication scheme. Always returns * <code>null</code>. * * @return <code>null</code> */ public String getRealm() { return null; } /** * Returns <tt>true</tt>. Negotiate authentication scheme is connection based. * * @return <tt>true</tt>. */ public boolean isConnectionBased() { return true; } // Protected methods ----------------------------------------------------- @Override protected void parseChallenge(final CharArrayBuffer buffer, int beginIndex, int endIndex) throws MalformedChallengeException { String challenge = buffer.substringTrimmed(beginIndex, endIndex); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Received challenge '" + challenge + "' from the auth server"); } if (state == State.UNINITIATED) { token = new Base64().decode(challenge.getBytes()); state = State.CHALLENGE_RECEIVED; } else { LOGGER.debug("Authentication already attempted"); state = State.FAILED; } } }