/* See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* Esri Inc. licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.esri.gpt.framework.security.identity.open;
import com.esri.gpt.framework.context.RequestContext;
import com.esri.gpt.framework.security.identity.local.LocalDao;
import com.esri.gpt.framework.security.principal.User;
import com.esri.gpt.framework.util.Val;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLEncoder;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.expressme.openid.Association;
import org.expressme.openid.Authentication;
import org.expressme.openid.Endpoint;
import org.expressme.openid.OpenIdException;
import org.expressme.openid.OpenIdManager;
import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.http.AccessToken;
import twitter4j.http.RequestToken;
/**
* Openid authentication consumer servlet (Openid, oAuth-Twitter).
* <p/>
* This is basically a replacement for org.expressme.openid.MainServlet.
* <p/>
* An Openid provider must return at least an identifier and an email address.
* <p/>
* Twitter return a screen name.
*/
public class OpenidConsumerServlet extends HttpServlet {
/*
google/yahoo
DN = urn:openid:http://host/...?id=...
username = email
twitter
DN = urn:openid:twitter:screenname
username = screenname@twitter
*/
private static Logger LOGGER = Logger.getLogger(OpenidConsumerServlet.class.getName());
private static final long ONE_HOUR = 3600000L;
private static final long TWO_HOUR = ONE_HOUR * 2L;
private static final String ATTR_ALIAS = "gpt_openid_alias";
private static final String ATTR_CBINFO = "gpt_openid_cbinfo";
private static final String ATTR_MAC = "gpt_openid_mac";
private static final String ATTR_TOKEN = "gpt_oauth_token";
private static final String ATTR_TOKEN_SECRET = "gpt_oauth_token_secret";
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
RequestContext context = null;
boolean useFacade = false;
String err = "";
try {
LOGGER.finer("Query string="+request.getQueryString());
String op = request.getParameter("op");
context = RequestContext.extract(request);
OpenProviders providers = context.getIdentityConfiguration().getOpenProviders();
if ((providers == null) || (providers.size() == 0)) {
return;
}
String baseContextPath = RequestContext.resolveBaseContextPath(request);
String callbackUrl = baseContextPath+"/openid";
String realm = baseContextPath;
HttpSession session = request.getSession();
// process a response from an Openid provider
if (op == null) {
String identity = null;
String username = null;
String email = null;
// determine the callback info
String cbinfo = Val.chkStr((String)session.getAttribute(ATTR_CBINFO));
session.setAttribute(ATTR_CBINFO,null);
if (cbinfo.length() == 0) {
throw new ServletException("Invalid openid callback info.");
}
int idx = cbinfo.indexOf(",");
long millis = Long.parseLong(cbinfo.substring(0,idx));
cbinfo = cbinfo.substring(idx+1);
idx = cbinfo.indexOf(",");
String cbid = cbinfo.substring(0,idx);
cbinfo = cbinfo.substring(idx+1);
idx = cbinfo.indexOf(",");
op = cbinfo.substring(0,idx);
String fwd = cbinfo.substring(idx+1);
LOGGER.finer("cbinfo retrieved: "+cbinfo);
// determine the provider
OpenProvider provider = providers.get(op);
if (provider == null) {
throw new ServletException("Invalid openid op parameter on callback: "+op);
}
boolean isTwitter = provider.getName().equalsIgnoreCase("Twitter");
// determine the authenticated user attributes
if (useFacade) {
identity = "http://openidfacade/user123";
email = "user123@openidfacade.com";
username = email;
// Twitter callback
} else if (isTwitter) {
try {
LOGGER.finer("Determining user attributes for: "+op);
String token = (String)session.getAttribute(ATTR_TOKEN);
String tokenSecret = (String)session.getAttribute(ATTR_TOKEN_SECRET);
Twitter twitter = new Twitter();
twitter.setOAuthConsumer(provider.getConsumerKey(),provider.getConsumerSecret());
AccessToken accessToken = twitter.getOAuthAccessToken(token,tokenSecret);
twitter.setOAuthAccessToken(accessToken);
twitter4j.User tUser = twitter.verifyCredentials();
String screenName = Val.chkStr(tUser.getScreenName());
if (screenName.length() > 0) {
username = screenName+"@twitter";
identity = "twitter:"+screenName;
}
} catch (Exception e) {
err = "oAuth authentication failed.";
LOGGER.log(Level.WARNING,err,e);
}
// Openid callback
} else {
try {
// determine the callback UUID
String cbidParam = Val.chkStr(request.getParameter("cbid"));
if (cbidParam.length() == 0) {
throw new ServletException("Empty cbid parameter on callback.");
}
if (!cbid.equals(cbidParam)) {
throw new ServletException("Invalid openid cbid parameter on callback.");
}
callbackUrl += "?cbid="+java.net.URLEncoder.encode(cbid,"UTF-8");
LOGGER.finer("cbinfo based callback: "+cbinfo);
LOGGER.finer("Determining user attributes for: "+op);
OpenIdManager manager = new OpenIdManager();
manager.setRealm(realm);
manager.setReturnTo(callbackUrl);
checkNonce(request.getParameter("openid.response_nonce"));
byte[] mac_key = (byte[])session.getAttribute(ATTR_MAC);
String alias = (String)session.getAttribute(ATTR_ALIAS);
Authentication authentication = manager.getAuthentication(request,mac_key,alias);
identity = authentication.getIdentity();
email = authentication.getEmail();
username = email;
} catch (Exception e) {
err = "Openid authentication suceeded, creating local user reference failed.";
LOGGER.log(Level.WARNING,err,e);
}
}
// check the parameters
identity = Val.chkStr(identity);
username = Val.chkStr(username);
email = Val.chkStr(email);
LOGGER.finer("User attributes: identity="+identity+", username="+username+", email="+email);
if (identity.length() == 0) {
err = "Your openid idenitfier was not determined.";
} else if (username.length() == 0) {
if (isTwitter) {
err = "Your opennid screen name was not determined.";
} else {
err = "Your opennid email address was not determined.";
}
} else {
// establish the user
identity = "urn:openid:"+identity;
User user = context.getUser();
user.reset();
user.setKey(identity);
user.setDistinguishedName(identity);
user.setName(username);
user.getProfile().setUsername(username);
if (email.length() > 0) {
user.getProfile().setEmailAddress(email);
}
user.getAuthenticationStatus().setWasAuthenticated(true);
// ensure a local reference for the user
try {
LocalDao localDao = new LocalDao(context);
localDao.ensureReferenceToRemoteUser(user);
} catch (Exception e) {
user.reset();
err = "Openid authentication suceeded, creating local user reference failed.";
LOGGER.log(Level.SEVERE,err,e);
}
}
// redirect to the originating page
String url = fwd;
err = Val.chkStr(err);
if (err.length() > 0) {
if (url.indexOf("?") == -1) fwd += "?";
else url += "&";
url += "err="+URLEncoder.encode(err,"UTF-8");
}
response.sendRedirect(url);
// process a request to enter Openid credentials
} else if (op.length() > 0) {
session.setAttribute(ATTR_CBINFO,null);
// determine the provider
OpenProvider provider = providers.get(op);
if (provider == null) {
throw new ServletException("Invalid openid op parameter: "+op);
}
boolean isTwitter = provider.getName().equalsIgnoreCase("Twitter");
// determine the active Geoportal page (forward URL)
String fwd = Val.chkStr(request.getParameter("fwd"));
if (fwd.length() == 0) {
throw new ServletException("Empty openid fwd parameter.");
}
// store the callback info
String cbid = UUID.randomUUID().toString();
long millis = System.currentTimeMillis();
String cbinfo = millis+","+cbid+","+op+","+fwd;
session.setAttribute(ATTR_CBINFO,cbinfo);
// determine the Openid Authentication URL
String url = null;
if (useFacade) {
PrintWriter pw = response.getWriter();
pw.println("<html><head><title>Openid Facade</title></head><body><h1>Openid Facade</h1>");
pw.println("<a href=\""+callbackUrl+"\">Supply credentials step</a>");
pw.println("</body></html>");
pw.flush();
return;
// Twitter
} else if (isTwitter) {
try {
LOGGER.fine("Initiating oAuth request for: "+op+", callback="+callbackUrl);
Twitter twitter = new Twitter();
twitter.setOAuthConsumer(provider.getConsumerKey(),provider.getConsumerSecret());
RequestToken requestToken = twitter.getOAuthRequestToken();
String token = requestToken.getToken();
String tokenSecret = requestToken.getTokenSecret();
session.setAttribute(ATTR_TOKEN,token);
session.setAttribute(ATTR_TOKEN_SECRET,tokenSecret);
url = requestToken.getAuthorizationURL();
} catch (TwitterException e) {
err = "Unable to determine endpoint for: "+op;
LOGGER.log(Level.SEVERE,err,e);
}
// Openid
} else {
try {
callbackUrl += "?cbid="+java.net.URLEncoder.encode(cbid,"UTF-8");
LOGGER.finer("Initiating openid request for: "+op+", callback="+callbackUrl);
OpenIdManager manager = new OpenIdManager();
manager.setRealm(realm);
manager.setReturnTo(callbackUrl);
// There is an issue here. It seems that the only way to set the endpoint
// alias is through the jopenid-1.07.jar openid-providers.properties,
// but we would to to configure the provider properties through gpt.xml
//Endpoint endpoint = manager.lookupEndpoint(provider.getAuthenticationUrl());
Endpoint endpoint = manager.lookupEndpoint(op);
Association association = manager.lookupAssociation(endpoint);
request.getSession().setAttribute(ATTR_MAC,association.getRawMacKey());
request.getSession().setAttribute(ATTR_ALIAS,endpoint.getAlias());
url = manager.getAuthenticationUrl(endpoint,association);
} catch (Exception e) {
err = "Unable to determine Openid endpoint for: "+op;
LOGGER.log(Level.SEVERE,err,e);
}
}
// redirect to the authentication endpoint or to originating page
err = Val.chkStr(err);
if (err.length() > 0) {
url = fwd;
if (url.indexOf("?") == -1) fwd += "?";
else url += "&";
url += "err="+URLEncoder.encode(err,"UTF-8");
}
LOGGER.finer("Redirecting for authentication: "+url);
response.sendRedirect(url);
} else {
throw new ServletException("Empty openid op parameter.");
}
} finally {
if (context != null) context.onExecutionPhaseCompleted();
}
}
/**
* Taken from org.expressme.openid.MainServlet
*/
private void showAuthentication(PrintWriter pw, String identity, String email) {
pw.print("<html><body><h1>Identity</h1><p>");
pw.print(identity);
pw.print("</p><h1>Email</h1><p>");
pw.print(email==null ? "(null)" : email);
pw.print("</p></body></html>");
pw.flush();
}
/**
* Taken from org.expressme.openid.MainServlet
*/
private void checkNonce(String nonce) {
// check response_nonce to prevent replay-attack:
if (nonce==null || nonce.length()<20) throw new OpenIdException("Verify failed.");
long nonceTime = getNonceTime(nonce);
long diff = System.currentTimeMillis() - nonceTime;
if (diff < 0) diff = (-diff);
if (diff > ONE_HOUR) throw new OpenIdException("Bad nonce time.");
if (isNonceExist(nonce)) throw new OpenIdException("Verify nonce failed.");
storeNonce(nonce, nonceTime + TWO_HOUR);
}
/**
* Taken from org.expressme.openid.MainServlet
*/
private boolean isNonceExist(String nonce) {
// check if nonce is exist in database:
return false;
}
/**
* Taken from org.expressme.openid.MainServlet
*/
private void storeNonce(String nonce, long expires) {
// store nonce in database:
}
/**
* Taken from org.expressme.openid.MainServlet
*/
private long getNonceTime(String nonce) {
try {
return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(nonce.substring(0,19)+"+0000").getTime();
} catch(ParseException e) {
throw new OpenIdException("Bad nonce time.");
}
}
}