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}