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}