/* * (C) Copyright 2012 Nuxeo SA (http://nuxeo.com/) and others. * * 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. * * Contributors: * Sylvain Chambon */ package org.nuxeo.ecm.platform.ui.web.auth.krb5; import java.io.IOException; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.List; import java.util.Map; import javax.security.auth.Subject; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.codec.binary.Base64; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; 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 org.nuxeo.ecm.platform.api.login.UserIdentificationInfo; import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPlugin; import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper; /** * Kerberos v5 in SPNEGO authentication. TODO handle NTLMSSP as a fallback position. * * @author schambon */ public class Krb5Authenticator implements NuxeoAuthenticationPlugin { private static final String CONTEXT_ATTRIBUTE = "Krb5Authenticator_context"; private static final Log logger = LogFactory.getLog(Krb5Authenticator.class); private static final String WWW_AUTHENTICATE = "WWW-Authenticate"; private static final String AUTHORIZATION = "Authorization"; private static final String NEGOTIATE = "Negotiate"; private static final String SKIP_KERBEROS = "X-Skip-Kerberos"; // magic header used by the reverse proxy to skip // this authenticator private static final GSSManager MANAGER = GSSManager.getInstance(); private LoginContext loginContext = null; private GSSCredential serverCredential = null; private boolean disabled = false; @Override public List<String> getUnAuthenticatedURLPrefix() { return null; } @Override public Boolean handleLoginPrompt(HttpServletRequest req, HttpServletResponse res, String baseURL) { logger.debug("Sending login prompt..."); if (res.getHeader(WWW_AUTHENTICATE) == null) { res.setHeader(WWW_AUTHENTICATE, NEGOTIATE); } // hack to support fallback to form auth in case the // client does not answer the SPNEGO challenge. // This will obviously break if form auth is disabled; but this isn't // much of an issue since other sso filters will not work nicely after // this one (as this one takes over the response and flushes it to start // negotiation). String refresh = String.format("1;url=/%s/login.jsp", VirtualHostHelper.getWebAppName(req)); res.setHeader("Refresh", refresh); res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); res.setContentLength(0); try { res.flushBuffer(); } catch (IOException e) { logger.warn("Cannot flush response", e); } return true; } @Override public UserIdentificationInfo handleRetrieveIdentity(HttpServletRequest req, HttpServletResponse res) { String authorization = req.getHeader(AUTHORIZATION); if (authorization == null) { return null; // no auth } if (!authorization.startsWith(NEGOTIATE)) { logger.warn("Received invalid Authorization header (expected: Negotiate then SPNEGO blob): " + authorization); // ignore invalid authorization headers. return null; } byte[] token = Base64.decodeBase64(authorization.substring(NEGOTIATE.length() + 1)); byte[] respToken = null; GSSContext context = null; try { synchronized (this) { context = (GSSContext) req.getSession().getAttribute(CONTEXT_ATTRIBUTE); if (context == null) { context = MANAGER.createContext(serverCredential); } respToken = context.acceptSecContext(token, 0, token.length); } if (context.isEstablished()) { String principal = context.getSrcName().toString(); String username = principal.split("@")[0]; // throw away the realm UserIdentificationInfo info = new UserIdentificationInfo(username, "Trust"); info.setLoginPluginName("Trusting_LM"); req.getSession().removeAttribute(CONTEXT_ATTRIBUTE); return info; } else { // save context in the HTTP session to be reused after client response req.getSession().setAttribute(CONTEXT_ATTRIBUTE, context); // need another roundtrip res.setHeader(WWW_AUTHENTICATE, NEGOTIATE + " " + Base64.encodeBase64String(respToken)); return null; } } catch (GSSException ge) { req.getSession().removeAttribute(CONTEXT_ATTRIBUTE); logger.error("Cannot accept provided security token", ge); return null; } } @Override public void initPlugin(Map<String, String> parameters) { try { this.loginContext = new LoginContext("Nuxeo"); // note: we assume that all configuration is done in loginconfig, so there are NO parameters here loginContext.login(); serverCredential = Subject.doAs(loginContext.getSubject(), getServerCredential); logger.debug("Successfully initialized Kerberos auth module"); } catch (LoginException le) { logger.error("Cannot create LoginContext, disabling Kerberos module", le); this.disabled = true; } catch (PrivilegedActionException pae) { logger.error("Cannot get server credentials, disabling Kerberos module", pae); this.disabled = true; } } @Override public Boolean needLoginPrompt(HttpServletRequest req) { return !disabled && (req.getHeader(SKIP_KERBEROS) == null); } private PrivilegedExceptionAction<GSSCredential> getServerCredential = new PrivilegedExceptionAction<GSSCredential>() { @Override public GSSCredential run() throws GSSException { return MANAGER.createCredential(null, GSSCredential.DEFAULT_LIFETIME, new Oid[] { new Oid("1.3.6.1.5.5.2") /* Oid for Kerberos */, new Oid("1.2.840.113554.1.2.2") /* * Oid * for * SPNEGO */}, GSSCredential.ACCEPT_ONLY); } }; }