/*
* eXist Open Source Native XML Database
* Copyright (C) 2009-2011 The eXist Project
* http://exist-db.org
*
* This program 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
* of the License, or (at your option) any later version.
*
* This program 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 program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
* $Id$
*/
package org.exist.security.realm.openid;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import javax.security.auth.Subject;
import javax.servlet.*;
import javax.servlet.http.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.jetty.security.DefaultIdentityService;
import org.eclipse.jetty.server.Authentication;
import org.eclipse.jetty.server.UserIdentity;
import org.exist.Database;
import org.exist.EXistException;
import org.exist.config.ConfigurationException;
import org.exist.security.AXSchemaType;
import org.exist.security.AbstractAccount;
import org.exist.security.Account;
import org.exist.security.AbstractRealm;
import org.exist.security.PermissionDeniedException;
import org.exist.security.internal.HttpSessionAuthentication;
import org.exist.security.internal.SubjectAccreditedImpl;
import org.exist.security.internal.aider.UserAider;
import org.exist.storage.DBBroker;
import org.exist.xquery.util.HTTPUtils;
import org.openid4java.OpenIDException;
import org.openid4java.association.AssociationSessionType;
import org.openid4java.consumer.*;
import org.openid4java.discovery.*;
import org.openid4java.message.*;
import org.openid4java.message.ax.AxMessage;
import org.openid4java.message.ax.FetchRequest;
import org.openid4java.message.ax.FetchResponse;
import org.openid4java.message.sreg.SRegMessage;
import org.openid4java.message.sreg.SRegRequest;
import org.openid4java.message.sreg.SRegResponse;
import org.openid4java.util.*;
/**
* OpenId authenticator servlet.
*
* @author <a href="mailto:shabanovd@gmail.com">Dmitriy Shabanov</a>
*
*/
public class AuthenticatorOpenIdServlet extends HttpServlet {
private static final long serialVersionUID = -558037449837549034L;
private static final Log LOG = LogFactory.getLog(AuthenticatorOpenIdServlet.class);
public ConsumerManager manager;
public static AbstractRealm realm = null;
public AuthenticatorOpenIdServlet() throws ConsumerException {
}
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
// --- Forward proxy setup (only if needed) ---
ProxyProperties proxyProps = getProxyProperties(config);
if (proxyProps != null) {
LOG.debug("ProxyProperties: " + proxyProps);
HttpClientFactory.setProxyProperties(proxyProps);
}
manager = new ConsumerManager();
manager.setAssociations(new InMemoryConsumerAssociationStore());
manager.setNonceVerifier(new InMemoryNonceVerifier(5000));
manager.setMinAssocSessEnc(AssociationSessionType.DH_SHA256);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
if ("true".equals(req.getParameter("is_return"))) {
processReturn(req, resp);
} else {
String identifier = req.getParameter("openid_identifier");
if (identifier != null) {
this.authRequest(identifier, req, resp);
} else {
//this.getServletContext().getRequestDispatcher("/openid/login.xql").forward(req, resp);
resp.sendRedirect(
OpenIDRealm.instance.getSecurityManager().getAuthenticationEntryPoint()
);
}
}
}
private void processReturn(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
Account principal = this.verifyResponse(req);
//System.out.println(principal);
String returnURL = req.getParameter("exist_return");
if (principal == null) {
//this.getServletContext().getRequestDispatcher("/openid/login.xql").forward(req, resp);
resp.sendRedirect(returnURL);
} else {
HttpSession session = req.getSession(true);
//((XQueryURLRewrite.RequestWrapper)req).setUserPrincipal(principal);
Subject subject = new Subject();
//TODO: hardcoded to jetty - rewrite
//*******************************************************
DefaultIdentityService _identityService = new DefaultIdentityService();
UserIdentity user = _identityService.newUserIdentity(subject, principal, new String[0]);
Authentication cached=new HttpSessionAuthentication(session, user);
session.setAttribute(HttpSessionAuthentication.__J_AUTHENTICATED, cached);
//*******************************************************
resp.sendRedirect(returnURL);
}
}
// authentication request
public String authRequest(String userSuppliedString,
HttpServletRequest httpReq, HttpServletResponse httpResp)
throws IOException, ServletException {
if (OpenIDRealm.instance == null) {
ServletOutputStream out = httpResp.getOutputStream();
httpResp.setContentType("text/html; charset=\"UTF-8\"");
httpResp.addHeader( "pragma", "no-cache" );
httpResp.addHeader( "Cache-Control", "no-cache" );
httpResp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
out.print("<html><head>");
out.print("<title>OpenIDServlet Error</title>");
out.print("<link rel=\"stylesheet\" type=\"text/css\" href=\"error.css\"></link></head>");
out.print("<body><div id=\"container\"><h1>Error found</h1>");
out.print("<h2>Message:");
out.print("OpenID realm wasn't initialized.");
out.print("</h2>");
//out.print(HTTPUtils.printStackTraceHTML(t));
out.print("</div></body></html>");
return null;
}
try {
String returnAfterAuthentication = httpReq.getParameter("return_to");
// configure the return_to URL where your application will receive
// the authentication responses from the OpenID provider
String returnToUrl = httpReq.getRequestURL().toString() + "?is_return=true&exist_return="+returnAfterAuthentication;
// perform discovery on the user-supplied identifier
List<?> discoveries = manager.discover(userSuppliedString);
// attempt to associate with the OpenID provider
// and retrieve one service endpoint for authentication
DiscoveryInformation discovered = manager.associate(discoveries);
// store the discovery information in the user's session
httpReq.getSession().setAttribute("openid-disc", discovered);
// obtain a AuthRequest message to be sent to the OpenID provider
AuthRequest authReq = manager.authenticate(discovered, returnToUrl);
if (authReq.getOPEndpoint().indexOf("myopenid.com")>0) {
SRegRequest sregReq = SRegRequest.createFetchRequest();
sregReq.addAttribute(AXSchemaType.FULLNAME.name().toLowerCase(), true);
sregReq.addAttribute(AXSchemaType.EMAIL.name().toLowerCase(), true);
sregReq.addAttribute(AXSchemaType.COUNTRY.name().toLowerCase(), true);
sregReq.addAttribute(AXSchemaType.LANGUAGE.name().toLowerCase(), true);
authReq.addExtension(sregReq);
} else {
FetchRequest fetch = FetchRequest.createFetchRequest();
fetch.addAttribute(AXSchemaType.FIRSTNAME.getAlias(), AXSchemaType.FIRSTNAME.getNamespace(), true);
fetch.addAttribute(AXSchemaType.LASTNAME.getAlias(), AXSchemaType.LASTNAME.getNamespace(), true);
fetch.addAttribute(AXSchemaType.EMAIL.getAlias(), AXSchemaType.EMAIL.getNamespace(), true);
fetch.addAttribute(AXSchemaType.COUNTRY.getAlias(), AXSchemaType.COUNTRY.getNamespace(), true);
fetch.addAttribute(AXSchemaType.LANGUAGE.getAlias(), AXSchemaType.LANGUAGE.getNamespace(), true);
// wants up to three email addresses
fetch.setCount(AXSchemaType.EMAIL.getAlias(), 3);
authReq.addExtension(fetch);
}
if (!discovered.isVersion2()) {
// Option 1: GET HTTP-redirect to the OpenID Provider endpoint
// The only method supported in OpenID 1.x
// redirect-URL usually limited ~2048 bytes
httpResp.sendRedirect(authReq.getDestinationUrl(true));
return null;
} else {
// Option 2: HTML FORM Redirection (Allows payloads >2048 bytes)
Object OPEndpoint = authReq.getDestinationUrl(false);
ServletOutputStream out = httpResp.getOutputStream();
httpResp.setContentType("text/html; charset=UTF-8");
httpResp.addHeader( "pragma", "no-cache" );
httpResp.addHeader( "Cache-Control", "no-cache" );
out.println("<html xmlns=\"http://www.w3.org/1999/xhtml\">");
out.println("<head>");
out.println(" <title>OpenID HTML FORM Redirection</title>");
out.println("</head>");
out.println("<body onload=\"document.forms['openid-form-redirection'].submit();\">");
out.println(" <form name=\"openid-form-redirection\" action=\""+OPEndpoint+"\" method=\"post\" accept-charset=\"utf-8\">");
Map<String, String> parameterMap = authReq.getParameterMap();
for (Entry<String, String> entry : parameterMap.entrySet()) {
out.println(" <input type=\"hidden\" name=\""+entry.getKey()+"\" value=\""+entry.getValue()+"\"/>");
}
out.println(" <button type=\"submit\">Continue...</button>");
out.println(" </form>");
out.println("</body>");
out.println("</html>");
out.flush();
}
} catch (OpenIDException e) {
// present error to the user
LOG.debug("OpenIDException",e);
ServletOutputStream out = httpResp.getOutputStream();
httpResp.setContentType("text/html; charset=\"UTF-8\"");
httpResp.addHeader( "pragma", "no-cache" );
httpResp.addHeader( "Cache-Control", "no-cache" );
httpResp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
out.print("<html><head>");
out.print("<title>OpenIDServlet Error</title>");
out.print("<link rel=\"stylesheet\" type=\"text/css\" href=\"error.css\"></link></head>");
out.print("<body><div id=\"container\"><h1>Error found</h1>");
out.print("<h2>Message:");
out.print(e.getMessage());
out.print("</h2>");
Throwable t = e.getCause();
if(t!=null){
// t can be null
out.print(HTTPUtils.printStackTraceHTML(t));
}
out.print("</div></body></html>");
}
return null;
}
// authentication response
public Account verifyResponse(HttpServletRequest httpReq)
throws ServletException {
try {
// extract the parameters from the authentication response
// (which comes in as a HTTP request from the OpenID provider)
ParameterList response = new ParameterList(httpReq
.getParameterMap());
// retrieve the previously stored discovery information
DiscoveryInformation discovered = (DiscoveryInformation) httpReq
.getSession().getAttribute("openid-disc");
// extract the receiving URL from the HTTP request
StringBuffer receivingURL = httpReq.getRequestURL();
String queryString = httpReq.getQueryString();
if (queryString != null && queryString.length() > 0)
receivingURL.append("?").append(httpReq.getQueryString());
// verify the response; ConsumerManager needs to be the same
// (static) instance used to place the authentication request
VerificationResult verification = manager.verify(receivingURL.toString(), response, discovered);
// examine the verification result and extract the verified
// identifier
Identifier verified = verification.getVerifiedId();
if (verified != null) {
// success
String accountName = AccountImpl.escape(verified.getIdentifier());
AbstractAccount account = (AbstractAccount) OpenIDRealm.instance.getAccount(accountName);
if (account == null) {
Database db = OpenIDRealm.instance.getDatabase();
try(final DBBroker broker = db.get(Optional.of(db.getSecurityManager().getSystemSubject()))) {
//XXX: set OpenID group by default
account = (AbstractAccount) OpenIDRealm.instance.addAccount(
new UserAider(OpenIDRealm.instance.getId(), accountName)
);
}
}
org.exist.security.Subject principal =
new SubjectAccreditedImpl( account, verified );
AuthSuccess authSuccess = (AuthSuccess) verification.getAuthResponse();
authSuccess.getExtensions();
if (authSuccess.hasExtension(SRegMessage.OPENID_NS_SREG)) {
MessageExtension ext = authSuccess.getExtension(SRegMessage.OPENID_NS_SREG);
if (ext instanceof SRegResponse) {
SRegResponse sregResp = (SRegResponse) ext;
for (Iterator iter = sregResp.getAttributeNames().iterator(); iter.hasNext();) {
String name = (String) iter.next();
if (LOG.isDebugEnabled())
LOG.debug(name + " : " + sregResp.getParameterValue(name));
principal.setMetadataValue(AXSchemaType.valueOfNamespace(name), sregResp.getParameterValue(name));
}
}
}
if (authSuccess.hasExtension(AxMessage.OPENID_NS_AX)) {
FetchResponse fetchResp = (FetchResponse) authSuccess.getExtension(AxMessage.OPENID_NS_AX);
List aliases = fetchResp.getAttributeAliases();
for (Iterator iter = aliases.iterator(); iter.hasNext();) {
String alias = (String) iter.next();
List values = fetchResp.getAttributeValues(alias);
if (values.size() > 0) {
if (LOG.isDebugEnabled())
LOG.debug(alias + " : " + values.get(0));
principal.setMetadataValue(AXSchemaType.valueOfAlias(alias), (String)values.get(0));
}
}
}
//update metadata
Database db = OpenIDRealm.instance.getDatabase();
try(final DBBroker broker = db.get(Optional.of(db.getSecurityManager().getSystemSubject()))) {
OpenIDRealm.instance.updateAccount(principal);
}
OpenIDUtility.registerUser(principal);
return principal;
}
} catch (OpenIDException e) {
LOG.error(e);
} catch (ConfigurationException e) {
LOG.error(e);
} catch (PermissionDeniedException e) {
LOG.error(e);
} catch (EXistException e) {
LOG.error(e);
}
return null;
}
private static ProxyProperties getProxyProperties(ServletConfig config) {
ProxyProperties proxyProps;
String host = config.getInitParameter("proxy.host");
LOG.debug("proxy.host: " + host);
if (host == null) {
proxyProps = null;
} else {
proxyProps = new ProxyProperties();
String port = config.getInitParameter("proxy.port");
String username = config.getInitParameter("proxy.username");
String password = config.getInitParameter("proxy.password");
String domain = config.getInitParameter("proxy.domain");
proxyProps.setProxyHostName(host);
proxyProps.setProxyPort(Integer.parseInt(port));
proxyProps.setUserName(username);
proxyProps.setPassword(password);
proxyProps.setDomain(domain);
}
return proxyProps;
}
}