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 GSSCredential serverCredential = null; 068 069 private boolean disabled = false; 070 071 @Override 072 public List<String> getUnAuthenticatedURLPrefix() { 073 return null; 074 } 075 076 @Override 077 public Boolean handleLoginPrompt(HttpServletRequest req, HttpServletResponse res, String baseURL) { 078 079 logger.debug("Sending login prompt..."); 080 if (res.getHeader(WWW_AUTHENTICATE) == null) { 081 res.setHeader(WWW_AUTHENTICATE, NEGOTIATE); 082 } 083 // hack to support fallback to form auth in case the 084 // client does not answer the SPNEGO challenge. 085 // This will obviously break if form auth is disabled; but this isn't 086 // much of an issue since other sso filters will not work nicely after 087 // this one (as this one takes over the response and flushes it to start 088 // negotiation). 089 String refresh = String.format("1;url=/%s/login.jsp", VirtualHostHelper.getWebAppName(req)); 090 res.setHeader("Refresh", refresh); 091 res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 092 res.setContentLength(0); 093 try { 094 res.flushBuffer(); 095 096 } catch (IOException e) { 097 logger.warn("Cannot flush response", e); 098 } 099 return true; 100 } 101 102 @Override 103 public UserIdentificationInfo handleRetrieveIdentity(HttpServletRequest req, HttpServletResponse res) { 104 String authorization = req.getHeader(AUTHORIZATION); 105 if (authorization == null) { 106 return null; // no auth 107 } 108 109 if (!authorization.startsWith(NEGOTIATE)) { 110 logger.warn( 111 "Received invalid Authorization header (expected: Negotiate then SPNEGO blob): " + authorization); 112 // ignore invalid authorization headers. 113 return null; 114 } 115 116 byte[] token = Base64.decodeBase64(authorization.substring(NEGOTIATE.length() + 1)); 117 byte[] respToken; 118 119 GSSContext context; 120 121 try { 122 synchronized (this) { 123 context = (GSSContext) req.getSession().getAttribute(CONTEXT_ATTRIBUTE); 124 if (context == null) { 125 context = MANAGER.createContext(serverCredential); 126 } 127 respToken = context.acceptSecContext(token, 0, token.length); 128 129 } 130 if (context.isEstablished()) { 131 String principal = context.getSrcName().toString(); 132 String username = principal.split("@")[0]; // throw away the realm 133 UserIdentificationInfo info = new UserIdentificationInfo(username); 134 req.getSession().removeAttribute(CONTEXT_ATTRIBUTE); 135 return info; 136 } else { 137 // save context in the HTTP session to be reused after client response 138 req.getSession().setAttribute(CONTEXT_ATTRIBUTE, context); 139 // need another roundtrip 140 res.setHeader(WWW_AUTHENTICATE, NEGOTIATE + " " + Base64.encodeBase64String(respToken)); 141 return null; 142 } 143 144 } catch (GSSException ge) { 145 req.getSession().removeAttribute(CONTEXT_ATTRIBUTE); 146 logger.error("Cannot accept provided security token", ge); 147 return null; 148 } 149 150 } 151 152 @Override 153 public void initPlugin(Map<String, String> parameters) { 154 155 try { 156 LoginContext loginContext = new LoginContext("Nuxeo"); 157 // note: we assume that all configuration is done in loginconfig, so there are NO parameters here 158 loginContext.login(); 159 serverCredential = Subject.doAs(loginContext.getSubject(), getServerCredential); 160 logger.debug("Successfully initialized Kerberos auth module"); 161 } catch (LoginException le) { 162 logger.warn("Cannot create LoginContext, disabling Kerberos module", le); 163 this.disabled = true; 164 } catch (PrivilegedActionException pae) { 165 logger.warn("Cannot get server credentials, disabling Kerberos module", pae); 166 this.disabled = true; 167 } 168 169 } 170 171 @Override 172 public Boolean needLoginPrompt(HttpServletRequest req) { 173 return !disabled && (req.getHeader(SKIP_KERBEROS) == null); 174 } 175 176 private PrivilegedExceptionAction<GSSCredential> getServerCredential = () -> MANAGER.createCredential(null, 177 GSSCredential.DEFAULT_LIFETIME, new Oid[] { new Oid("1.3.6.1.5.5.2") /* Oid for Kerberos */, 178 new Oid("1.2.840.113554.1.2.2") /* Oid for SPNEGO */ }, 179 GSSCredential.ACCEPT_ONLY); 180}