/*
* JBoss, Home of Professional Open Source.
* Copyright 2014 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed 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.
*/
package io.undertow.security.impl;
import io.undertow.UndertowLogger;
import io.undertow.security.api.AuthenticationMechanism;
import io.undertow.security.api.GSSAPIServerSubjectFactory;
import io.undertow.security.api.SecurityContext;
import io.undertow.security.idm.Account;
import io.undertow.security.idm.GSSContextCredential;
import io.undertow.security.idm.IdentityManager;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.ServerConnection;
import io.undertow.server.handlers.proxy.ExclusivityChecker;
import io.undertow.util.AttachmentKey;
import io.undertow.util.FlexBase64;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.Oid;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
import java.security.Principal;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.List;
import javax.security.auth.Subject;
import javax.security.auth.kerberos.KerberosPrincipal;
import static io.undertow.util.Headers.AUTHORIZATION;
import static io.undertow.util.Headers.HOST;
import static io.undertow.util.Headers.NEGOTIATE;
import static io.undertow.util.Headers.WWW_AUTHENTICATE;
import static io.undertow.util.StatusCodes.UNAUTHORIZED;
AuthenticationMechanism
for GSSAPI / SPNEGO based authentication.
GSSAPI authentication is associated with the HTTP connection, as long as a connection is being re-used allow the
authentication state to be re-used.
TODO - May consider an option to allow it to also be associated with the underlying session but that has it's own risks so
would need to come with a warning.
Author: Darran Lofthouse
/**
* {@link io.undertow.security.api.AuthenticationMechanism} for GSSAPI / SPNEGO based authentication.
* <p>
* GSSAPI authentication is associated with the HTTP connection, as long as a connection is being re-used allow the
* authentication state to be re-used.
* <p>
* TODO - May consider an option to allow it to also be associated with the underlying session but that has it's own risks so
* would need to come with a warning.
*
* @author <a href="mailto:darran.lofthouse@jboss.com">Darran Lofthouse</a>
*/
public class GSSAPIAuthenticationMechanism implements AuthenticationMechanism {
public static final ExclusivityChecker EXCLUSIVITY_CHECKER = new ExclusivityChecker() {
@Override
public boolean isExclusivityRequired(HttpServerExchange exchange) {
List<String> authHeaders = exchange.getRequestHeaders().get(AUTHORIZATION);
if (authHeaders != null) {
for (String current : authHeaders) {
if (current.startsWith(NEGOTIATE_PREFIX)) {
return true;
}
}
}
return false;
}
};
private static final String NEGOTIATION_PLAIN = NEGOTIATE.toString();
private static final String NEGOTIATE_PREFIX = NEGOTIATE + " ";
private static final Oid[] DEFAULT_MECHANISMS;
static {
try {
Oid spnego = new Oid("1.3.6.1.5.5.2");
Oid kerberos = new Oid("1.2.840.113554.1.2.2");
DEFAULT_MECHANISMS = new Oid[] { spnego, kerberos };
} catch (GSSException e) {
throw new RuntimeException(e);
}
}
private static final String name = "SPNEGO";
private final IdentityManager identityManager;
private final GSSAPIServerSubjectFactory subjectFactory;
private final Oid[] mechanisms;
public GSSAPIAuthenticationMechanism(final GSSAPIServerSubjectFactory subjectFactory, IdentityManager identityManager, Oid ...supportedMechanisms) {
this.subjectFactory = subjectFactory;
this.identityManager = identityManager;
this.mechanisms = supportedMechanisms;
}
public GSSAPIAuthenticationMechanism(final GSSAPIServerSubjectFactory subjectFactory, Oid ...supportedMechanisms) {
this(subjectFactory, null, supportedMechanisms);
}
public GSSAPIAuthenticationMechanism(final GSSAPIServerSubjectFactory subjectFactory) {
this(subjectFactory, DEFAULT_MECHANISMS);
}
@SuppressWarnings("deprecation")
private IdentityManager getIdentityManager(SecurityContext securityContext) {
return identityManager != null ? identityManager : securityContext.getIdentityManager();
}
@Override
public AuthenticationMechanismOutcome authenticate(final HttpServerExchange exchange,
final SecurityContext securityContext) {
ServerConnection connection = exchange.getConnection();
NegotiationContext negContext = connection.getAttachment(NegotiationContext.ATTACHMENT_KEY);
if (negContext != null) {
UndertowLogger.SECURITY_LOGGER.debugf("Existing negotiation context found for %s", exchange);
exchange.putAttachment(NegotiationContext.ATTACHMENT_KEY, negContext);
if (negContext.isEstablished()) {
IdentityManager identityManager = getIdentityManager(securityContext);
final Account account = identityManager.verify(new GSSContextCredential(negContext.getGssContext()));
if (account != null) {
securityContext.authenticationComplete(account, name, false);
UndertowLogger.SECURITY_LOGGER.debugf("Authenticated as user %s with existing GSSAPI negotiation context for %s", account.getPrincipal().getName(), exchange);
return AuthenticationMechanismOutcome.AUTHENTICATED;
} else {
UndertowLogger.SECURITY_LOGGER.debugf("Failed to authenticate with existing GSSAPI negotiation context for %s", exchange);
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
}
}
List<String> authHeaders = exchange.getRequestHeaders().get(AUTHORIZATION);
if (authHeaders != null) {
for (String current : authHeaders) {
if (current.startsWith(NEGOTIATE_PREFIX)) {
String base64Challenge = current.substring(NEGOTIATE_PREFIX.length());
try {
ByteBuffer challenge = FlexBase64.decode(base64Challenge);
return runGSSAPI(exchange, challenge, securityContext);
} catch (IOException e) {
}
// By this point we had a header we should have been able to verify but for some reason
// it was not correctly structured.
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
}
}
// No suitable header was found so authentication was not even attempted.
return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
}
public ChallengeResult sendChallenge(final HttpServerExchange exchange, final SecurityContext securityContext) {
NegotiationContext negContext = exchange.getAttachment(NegotiationContext.ATTACHMENT_KEY);
String header = NEGOTIATION_PLAIN;
if (negContext != null) {
byte[] responseChallenge = negContext.useResponseToken();
exchange.putAttachment(NegotiationContext.ATTACHMENT_KEY, null);
if (responseChallenge != null) {
header = NEGOTIATE_PREFIX + FlexBase64.encodeString(responseChallenge, false);
}
} else {
Subject server = null;
try {
server = subjectFactory.getSubjectForHost(getHostName(exchange));
} catch (GeneralSecurityException e) {
// Deliberately ignore - no Subject so don't offer GSSAPI is our main concern here.
}
if (server == null) {
return ChallengeResult.NOT_SENT;
}
}
exchange.getResponseHeaders().add(WWW_AUTHENTICATE, header);
UndertowLogger.SECURITY_LOGGER.debugf("Sending GSSAPI challenge for %s", exchange);
return new ChallengeResult(true, UNAUTHORIZED);
}
public AuthenticationMechanismOutcome runGSSAPI(final HttpServerExchange exchange,
final ByteBuffer challenge, final SecurityContext securityContext) {
try {
Subject server = subjectFactory.getSubjectForHost(getHostName(exchange));
// The AcceptSecurityContext takes over responsibility for setting the result.
return Subject.doAs(server, new AcceptSecurityContext(exchange, challenge, securityContext));
} catch (GeneralSecurityException e) {
e.printStackTrace();
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
} catch (PrivilegedActionException e) {
e.printStackTrace();
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
}
private String getHostName(final HttpServerExchange exchange) {
String hostName = exchange.getRequestHeaders().getFirst(HOST);
if (hostName != null) {
if (hostName.startsWith("[") && hostName.contains("]")) {
hostName = hostName.substring(0, hostName.indexOf(']') + 1);
} else if (hostName.contains(":")) {
hostName = hostName.substring(0, hostName.indexOf(":"));
}
return hostName;
}
return null;
}
private class AcceptSecurityContext implements PrivilegedExceptionAction<AuthenticationMechanismOutcome> {
private final HttpServerExchange exchange;
private final ByteBuffer challenge;
private final SecurityContext securityContext;
private AcceptSecurityContext(final HttpServerExchange exchange,
final ByteBuffer challenge, final SecurityContext securityContext) {
this.exchange = exchange;
this.challenge = challenge;
this.securityContext = securityContext;
}
public AuthenticationMechanismOutcome run() throws GSSException {
NegotiationContext negContext = exchange.getAttachment(NegotiationContext.ATTACHMENT_KEY);
if (negContext == null) {
negContext = new NegotiationContext();
exchange.putAttachment(NegotiationContext.ATTACHMENT_KEY, negContext);
// Also cache it on the connection for future calls.
exchange.getConnection().putAttachment(NegotiationContext.ATTACHMENT_KEY, negContext);
}
GSSContext gssContext = negContext.getGssContext();
if (gssContext == null) {
GSSManager manager = GSSManager.getInstance();
GSSCredential credential = manager.createCredential(null, GSSCredential.INDEFINITE_LIFETIME, mechanisms, GSSCredential.ACCEPT_ONLY);
gssContext = manager.createContext(credential);
negContext.setGssContext(gssContext);
}
byte[] respToken = gssContext.acceptSecContext(challenge.array(), challenge.arrayOffset(), challenge.limit());
negContext.setResponseToken(respToken);
if (negContext.isEstablished()) {
if (respToken != null) {
// There will be no further challenge but we do have a token so set it here.
exchange.getResponseHeaders().add(WWW_AUTHENTICATE,
NEGOTIATE_PREFIX + FlexBase64.encodeString(respToken, false));
}
IdentityManager identityManager = securityContext.getIdentityManager();
final Account account = identityManager.verify(new GSSContextCredential(negContext.getGssContext()));
if (account != null) {
securityContext.authenticationComplete(account, name, false);
return AuthenticationMechanismOutcome.AUTHENTICATED;
} else {
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
} else {
// This isn't a failure but as the context is not established another round trip with the client is needed.
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
}
}
private static class NegotiationContext {
static final AttachmentKey<NegotiationContext> ATTACHMENT_KEY = AttachmentKey.create(NegotiationContext.class);
private GSSContext gssContext;
private byte[] responseToken;
private Principal principal;
GSSContext getGssContext() {
return gssContext;
}
void setGssContext(GSSContext gssContext) {
this.gssContext = gssContext;
}
byte[] useResponseToken() {
// The token only needs to be returned once so clear it once used.
try {
return responseToken;
} finally {
responseToken = null;
}
}
void setResponseToken(byte[] responseToken) {
this.responseToken = responseToken;
}
boolean isEstablished() {
return gssContext != null ? gssContext.isEstablished() : false;
}
Principal getPrincipal() {
if (!isEstablished()) {
throw new IllegalStateException("No established GSSContext to use for the Principal.");
}
if (principal == null) {
try {
principal = new KerberosPrincipal(gssContext.getSrcName().toString());
} catch (GSSException e) {
throw new IllegalStateException("Unable to create Principal", e);
}
}
return principal;
}
}
}