blob: 1c7a16415633c0add8bb0ecd649d5eb63c793acd [file] [log] [blame]
/**
* 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.
*/
package org.apache.cxf.jaxrs.security;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.List;
import java.util.logging.Logger;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.PreMatching;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.security.SimplePrincipal;
import org.apache.cxf.common.security.SimpleSecurityContext;
import org.apache.cxf.common.util.Base64Exception;
import org.apache.cxf.common.util.Base64Utility;
import org.apache.cxf.common.util.PropertyUtils;
import org.apache.cxf.common.util.StringUtils;
import org.apache.cxf.jaxrs.ext.MessageContext;
import org.apache.cxf.jaxrs.utils.ExceptionUtils;
import org.apache.cxf.jaxrs.utils.JAXRSUtils;
import org.apache.cxf.message.Message;
import org.apache.cxf.security.SecurityContext;
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;
@PreMatching
public class KerberosAuthenticationFilter implements ContainerRequestFilter {
private static final Logger LOG = LogUtils.getL7dLogger(KerberosAuthenticationFilter.class);
private static final String NEGOTIATE_SCHEME = "Negotiate";
private static final String PROPERTY_USE_KERBEROS_OID = "auth.spnego.useKerberosOid";
private static final String KERBEROS_OID = "1.2.840.113554.1.2.2";
private static final String SPNEGO_OID = "1.3.6.1.5.5.2";
private MessageContext messageContext;
private CallbackHandler callbackHandler;
private Configuration loginConfig;
private String loginContextName = "";
private String servicePrincipalName;
private String realm;
@Override
public void filter(ContainerRequestContext context) {
List<String> authHeaders = messageContext.getHttpHeaders()
.getRequestHeader(HttpHeaders.AUTHORIZATION);
if (authHeaders == null || authHeaders.size() != 1) {
LOG.fine("No Authorization header is available");
throw ExceptionUtils.toNotAuthorizedException(null, getFaultResponse());
}
String[] authPair = authHeaders.get(0).split(" ");
if (authPair.length != 2 || !NEGOTIATE_SCHEME.equalsIgnoreCase(authPair[0])) {
LOG.fine("Negotiate Authorization scheme is expected");
throw ExceptionUtils.toNotAuthorizedException(null, getFaultResponse());
}
byte[] serviceTicket = getServiceTicket(authPair[1]);
try {
Subject serviceSubject = loginAndGetSubject();
GSSContext gssContext = createGSSContext();
Subject.doAs(serviceSubject, new ValidateServiceTicketAction(gssContext, serviceTicket));
GSSName srcName = gssContext.getSrcName();
if (srcName == null) {
throw ExceptionUtils.toNotAuthorizedException(null, getFaultResponse());
}
String complexUserName = srcName.toString();
String simpleUserName = complexUserName;
int index = simpleUserName.lastIndexOf('@');
if (index > 0) {
simpleUserName = simpleUserName.substring(0, index);
}
Message m = JAXRSUtils.getCurrentMessage();
m.put(SecurityContext.class, createSecurityContext(simpleUserName, complexUserName, gssContext));
if (!gssContext.getCredDelegState()) {
gssContext.dispose();
}
} catch (LoginException e) {
LOG.fine("Unsuccessful JAAS login for the service principal: " + e.getMessage());
throw ExceptionUtils.toNotAuthorizedException(e, getFaultResponse());
} catch (GSSException e) {
LOG.fine("GSS API exception: " + e.getMessage());
throw ExceptionUtils.toNotAuthorizedException(e, getFaultResponse());
} catch (PrivilegedActionException e) {
LOG.fine("PrivilegedActionException: " + e.getMessage());
throw ExceptionUtils.toNotAuthorizedException(e, getFaultResponse());
}
}
protected SecurityContext createSecurityContext(String simpleUserName, String complexUserName,
GSSContext gssContext) {
return new KerberosSecurityContext(new KerberosPrincipal(simpleUserName, complexUserName), gssContext);
}
protected GSSContext createGSSContext() throws GSSException {
boolean useKerberosOid = PropertyUtils.isTrue(
messageContext.getContextualProperty(PROPERTY_USE_KERBEROS_OID));
Oid oid = new Oid(useKerberosOid ? KERBEROS_OID : SPNEGO_OID);
GSSManager gssManager = GSSManager.getInstance();
String spn = getCompleteServicePrincipalName();
GSSName gssService = gssManager.createName(spn, null);
return gssManager.createContext(gssService.canonicalize(oid),
oid, null, GSSContext.DEFAULT_LIFETIME);
}
protected Subject loginAndGetSubject() throws LoginException {
// The login without a callback can work if
// - Kerberos keytabs are used with a principal name set in the JAAS config
// - Kerberos is integrated into the OS logon process
// meaning that a process which runs this code has the
// user identity
final LoginContext lc;
if (!StringUtils.isEmpty(loginContextName) || loginConfig != null) {
lc = new LoginContext(loginContextName, null, callbackHandler, loginConfig);
} else {
LOG.fine("LoginContext can not be initialized");
throw new LoginException();
}
lc.login();
return lc.getSubject();
}
private byte[] getServiceTicket(String encodedServiceTicket) {
try {
return Base64Utility.decode(encodedServiceTicket);
} catch (Base64Exception ex) {
throw ExceptionUtils.toNotAuthorizedException(null, getFaultResponse());
}
}
private static Response getFaultResponse() {
return JAXRSUtils.toResponseBuilder(401).header(HttpHeaders.WWW_AUTHENTICATE, NEGOTIATE_SCHEME).build();
}
protected String getCompleteServicePrincipalName() {
String name = servicePrincipalName == null
? "HTTP/" + messageContext.getUriInfo().getBaseUri().getHost() : servicePrincipalName;
if (realm != null) {
name += "@" + realm;
}
return name;
}
@Context
public void setMessageContext(MessageContext context) {
this.messageContext = context;
}
public void setLoginContextName(String contextName) {
this.loginContextName = contextName;
}
public void setServicePrincipalName(String servicePrincipalName) {
this.servicePrincipalName = servicePrincipalName;
}
public void setRealm(String realm) {
this.realm = realm;
}
public void setCallbackHandler(CallbackHandler callbackHandler) {
this.callbackHandler = callbackHandler;
}
private static final class ValidateServiceTicketAction implements PrivilegedExceptionAction<byte[]> {
private final GSSContext context;
private final byte[] token;
private ValidateServiceTicketAction(GSSContext context, byte[] token) {
this.context = context;
this.token = token;
}
public byte[] run() throws GSSException {
return context.acceptSecContext(token, 0, token.length);
}
}
public static class KerberosPrincipal extends SimplePrincipal {
private static final long serialVersionUID = 1L;
private String complexName;
public KerberosPrincipal(String simpleName, String complexName) {
super(simpleName);
this.complexName = complexName;
}
public String getKerberosName() {
return complexName;
}
}
public static class KerberosSecurityContext extends SimpleSecurityContext {
private GSSContext context;
public KerberosSecurityContext(KerberosPrincipal principal,
GSSContext context) {
super(principal);
this.context = context;
}
public GSSContext getGSSContext() {
return context;
}
}
}