/*
* Copyright (C) 2012 eXo Platform SAS.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.gatein.security.sso.spnego;
import java.io.IOException;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.UUID;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import javax.servlet.FilterChain;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.gatein.common.logging.Logger;
import org.gatein.common.logging.LoggerFactory;
import org.gatein.common.util.Base64;
import org.gatein.sso.agent.filter.api.AbstractSSOInterceptor;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.Oid;
public class SPNEGOSSOFilter extends AbstractSSOInterceptor {
private static final Logger log = LoggerFactory.getLogger(AbstractSSOInterceptor.class);
private static final GSSManager MANAGER = GSSManager.getInstance();
private LoginContext loginContext;
private String[] patterns = {"/login", "/spnegosso"};
private String loginServletPath = "/login";
private String securityDomain = "spnego-server";
public SPNEGOSSOFilter() {}
@Override
protected void initImpl() {
String patternParam = this.getInitParameter("patterns");
if(patternParam != null && !patternParam.isEmpty()) {
this.patterns = patternParam.split(",");
}
String loginServlet = this.getInitParameter("loginServletPath");
if(loginServlet != null && !loginServlet.isEmpty()) {
this.loginServletPath = loginServlet;
}
String domain = this.getInitParameter("securityDomain");
if(domain != null && !domain.isEmpty()) {
this.securityDomain = domain;
}
try {
this.loginContext = new LoginContext(this.securityDomain);
} catch (LoginException ex) {
log.warn("Exception while init LoginContext, so SPNEGO SSO will not work", ex);
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
final HttpServletRequest req = (HttpServletRequest)request;
final HttpServletResponse resp = (HttpServletResponse)response;
//. Check if this is not spnego login request
if(!isSpnegoLoginRequest(req)) {
chain.doFilter(request, response);
return;
}
SPNEGOSSOContext.setCurrentRequest(req);
final String contextPath = req.getContextPath();
final String loginURI = contextPath + this.loginServletPath;
final String requestURI = req.getRequestURI();
String username = req.getParameter("username");
final String remoteUser = req.getRemoteUser();
if(username != null || remoteUser != null) {
if(!loginURI.equalsIgnoreCase(requestURI)) {
// Redirect to /login if current request is /spnegosso to avoid error 404
// when user access to /spnegosso?username=username or when loggedIn user access to /spengosso
StringBuilder login = new StringBuilder(loginURI);
if(req.getQueryString() != null) {
login.append("?").append(req.getQueryString());
}
resp.sendRedirect(login.toString());
} else {
chain.doFilter(req, resp);
}
return;
}
String principal = null;
final String auth = req.getHeader("Authorization");
if(auth != null) {
try {
principal = this.login(req, resp, auth);
} catch (Exception ex) {
log.error("Exception occur when trying to login with SPNEGO", ex);
}
}
if(principal != null && !principal.isEmpty()) {
username = principal.substring(0, principal.indexOf('@'));
// We don't need user password when he login using SSO (SPNEGO)
// But LoginServlet require password is not empty to call login action instead of display input form
// So, we need to generate a random password
String password = UUID.randomUUID().toString();
HttpSession session = req.getSession();
session.setAttribute("SPNEGO_PRINCIPAL", username);
StringBuilder login = new StringBuilder(loginURI)
.append("?username=")
.append(username)
.append("&password=")
.append(password);
String initURL = req.getParameter("initialURI");
if(initURL != null) {
login.append("&initialURI=").append(initURL);
}
resp.sendRedirect(login.toString());
} else {
if(!loginURI.equals(requestURI)) {
RequestDispatcher dispatcher = req.getRequestDispatcher("/login");
dispatcher.include(req, resp);
} else {
chain.doFilter(req, resp);
}
resp.setHeader("WWW-Authenticate", "Negotiate");
resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
}
private boolean isSpnegoLoginRequest(HttpServletRequest request) {
final String uri = request.getRequestURI();
final String context = request.getContextPath();
for(String pattern : this.patterns) {
if(uri.equals(context.concat(pattern))) {
return true;
}
}
return false;
}
private String login(HttpServletRequest req, HttpServletResponse resp, String auth) throws Exception {
if(this.loginContext == null) {
return null;
}
this.loginContext.login();
final String principal;
final String tok = auth.substring("Negotiate".length() + 1);
final byte[] gss = Base64.decode(tok);
GSSContext context = null;
byte[] token = null;
context = MANAGER.createContext(getServerCredential(loginContext.getSubject()));
token = context.acceptSecContext(gss, 0, gss.length);
if (null == token) {
return null;
}
resp.setHeader("WWW-Authenticate", "Negotiate" + ' ' + Base64.encodeBytes(token));
if (!context.isEstablished()) {
resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return null;
}
principal = context.getSrcName().toString();
context.dispose();
this.loginContext.logout();
return principal;
}
/**
* Returns the {@link org.ietf.jgss.GSSCredential} the server uses for pre-authentication.
*
* @param subject account server uses for pre-authentication
* @return credential that allows server to authenticate clients
* @throws java.security.PrivilegedActionException
*/
static GSSCredential getServerCredential(final Subject subject)
throws PrivilegedActionException {
final PrivilegedExceptionAction<GSSCredential> action =
new PrivilegedExceptionAction<GSSCredential>() {
public GSSCredential run() throws GSSException {
return MANAGER.createCredential(
null
, GSSCredential.INDEFINITE_LIFETIME
, getOid()
, GSSCredential.ACCEPT_ONLY);
}
};
return Subject.doAs(subject, action);
}
/**
* Returns the Universal Object Identifier representation of
* the SPNEGO mechanism.
*
* @return Object Identifier of the GSS-API mechanism
*/
private static Oid getOid() {
Oid oid = null;
try {
oid = new Oid("1.3.6.1.5.5.2");
} catch (GSSException gsse) {
gsse.printStackTrace();
}
return oid;
}
@Override
public void destroy() {}
}