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