001package org.nuxeo.ecm.platform.ui.web.auth.krb5; 002 003import java.io.IOException; 004import java.security.PrivilegedActionException; 005import java.security.PrivilegedExceptionAction; 006import java.util.List; 007import java.util.Map; 008 009import javax.security.auth.Subject; 010import javax.security.auth.login.LoginContext; 011import javax.security.auth.login.LoginException; 012import javax.servlet.http.HttpServletRequest; 013import javax.servlet.http.HttpServletResponse; 014 015import org.apache.commons.codec.binary.Base64; 016import org.apache.commons.logging.Log; 017import org.apache.commons.logging.LogFactory; 018import org.ietf.jgss.GSSContext; 019import org.ietf.jgss.GSSCredential; 020import org.ietf.jgss.GSSException; 021import org.ietf.jgss.GSSManager; 022import org.ietf.jgss.Oid; 023import org.nuxeo.ecm.platform.api.login.UserIdentificationInfo; 024import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPlugin; 025import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper; 026 027/** 028 * Kerberos v5 in SPNEGO authentication. TODO handle NTLMSSP as a fallback position. 029 * 030 * @author schambon 031 */ 032public class Krb5Authenticator implements NuxeoAuthenticationPlugin { 033 034 private static final String CONTEXT_ATTRIBUTE = "Krb5Authenticator_context"; 035 036 private static final Log logger = LogFactory.getLog(Krb5Authenticator.class); 037 038 private static final String WWW_AUTHENTICATE = "WWW-Authenticate"; 039 040 private static final String AUTHORIZATION = "Authorization"; 041 042 private static final String NEGOTIATE = "Negotiate"; 043 044 private static final String SKIP_KERBEROS = "X-Skip-Kerberos"; // magic header used by the reverse proxy to skip 045 // this authenticator 046 047 private static final GSSManager MANAGER = GSSManager.getInstance(); 048 049 private LoginContext loginContext = null; 050 051 private GSSCredential serverCredential = null; 052 053 private boolean disabled = false; 054 055 @Override 056 public List<String> getUnAuthenticatedURLPrefix() { 057 return null; 058 } 059 060 @Override 061 public Boolean handleLoginPrompt(HttpServletRequest req, HttpServletResponse res, String baseURL) { 062 063 logger.debug("Sending login prompt..."); 064 if (res.getHeader(WWW_AUTHENTICATE) == null) { 065 res.setHeader(WWW_AUTHENTICATE, NEGOTIATE); 066 } 067 // hack to support fallback to form auth in case the 068 // client does not answer the SPNEGO challenge. 069 // This will obviously break if form auth is disabled; but this isn't 070 // much of an issue since other sso filters will not work nicely after 071 // this one (as this one takes over the response and flushes it to start 072 // negotiation). 073 String refresh = String.format("1;url=/%s/login.jsp", VirtualHostHelper.getWebAppName(req)); 074 res.setHeader("Refresh", refresh); 075 res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 076 res.setContentLength(0); 077 try { 078 res.flushBuffer(); 079 080 } catch (IOException e) { 081 logger.warn("Cannot flush response", e); 082 } 083 return true; 084 } 085 086 @Override 087 public UserIdentificationInfo handleRetrieveIdentity(HttpServletRequest req, HttpServletResponse res) { 088 String authorization = req.getHeader(AUTHORIZATION); 089 if (authorization == null) { 090 return null; // no auth 091 } 092 093 if (!authorization.startsWith(NEGOTIATE)) { 094 logger.warn("Received invalid Authorization header (expected: Negotiate then SPNEGO blob): " 095 + authorization); 096 // ignore invalid authorization headers. 097 return null; 098 } 099 100 byte[] token = Base64.decodeBase64(authorization.substring(NEGOTIATE.length() + 1)); 101 byte[] respToken = null; 102 103 GSSContext context = null; 104 105 try { 106 synchronized (this) { 107 context = (GSSContext) req.getSession().getAttribute(CONTEXT_ATTRIBUTE); 108 if (context == null) { 109 context = MANAGER.createContext(serverCredential); 110 } 111 respToken = context.acceptSecContext(token, 0, token.length); 112 113 } 114 if (context.isEstablished()) { 115 String principal = context.getSrcName().toString(); 116 String username = principal.split("@")[0]; // throw away the realm 117 UserIdentificationInfo info = new UserIdentificationInfo(username, "Trust"); 118 info.setLoginPluginName("Trusting_LM"); 119 req.getSession().removeAttribute(CONTEXT_ATTRIBUTE); 120 return info; 121 } else { 122 // save context in the HTTP session to be reused after client response 123 req.getSession().setAttribute(CONTEXT_ATTRIBUTE, context); 124 // need another roundtrip 125 res.setHeader(WWW_AUTHENTICATE, NEGOTIATE + " " + Base64.encodeBase64String(respToken)); 126 return null; 127 } 128 129 } catch (GSSException ge) { 130 req.getSession().removeAttribute(CONTEXT_ATTRIBUTE); 131 logger.error("Cannot accept provided security token", ge); 132 return null; 133 } 134 135 } 136 137 @Override 138 public void initPlugin(Map<String, String> parameters) { 139 140 try { 141 this.loginContext = new LoginContext("Nuxeo"); 142 // note: we assume that all configuration is done in loginconfig, so there are NO parameters here 143 loginContext.login(); 144 serverCredential = Subject.doAs(loginContext.getSubject(), getServerCredential); 145 logger.debug("Successfully initialized Kerberos auth module"); 146 } catch (LoginException le) { 147 logger.error("Cannot create LoginContext, disabling Kerberos module", le); 148 this.disabled = true; 149 } catch (PrivilegedActionException pae) { 150 logger.error("Cannot get server credentials, disabling Kerberos module", pae); 151 this.disabled = true; 152 } 153 154 } 155 156 @Override 157 public Boolean needLoginPrompt(HttpServletRequest req) { 158 return !disabled && (req.getHeader(SKIP_KERBEROS) == null); 159 } 160 161 private PrivilegedExceptionAction<GSSCredential> getServerCredential = new PrivilegedExceptionAction<GSSCredential>() { 162 163 @Override 164 public GSSCredential run() throws GSSException { 165 return MANAGER.createCredential(null, GSSCredential.DEFAULT_LIFETIME, 166 new Oid[] { new Oid("1.3.6.1.5.5.2") /* Oid for Kerberos */, new Oid("1.2.840.113554.1.2.2") /* 167 * Oid 168 * for 169 * SPNEGO 170 */}, 171 GSSCredential.ACCEPT_ONLY); 172 } 173 }; 174}