/*
* JBoss, Home of Professional Open Source
*
* Copyright 2013 Red Hat, Inc. and/or its affiliates.
*
* Licensed 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 org.picketlink.social.standalone.login;
import org.apache.log4j.Logger;
import org.json.JSONException;
import org.json.JSONObject;
import org.openid4java.consumer.ConsumerManager;
import org.openid4java.consumer.VerificationResult;
import org.openid4java.discovery.DiscoveryException;
import org.openid4java.discovery.DiscoveryInformation;
import org.openid4java.discovery.Identifier;
import org.openid4java.message.AuthRequest;
import org.openid4java.message.AuthSuccess;
import org.openid4java.message.MessageException;
import org.openid4java.message.ParameterList;
import org.openid4java.message.ax.AxMessage;
import org.openid4java.message.ax.FetchRequest;
import org.openid4java.message.ax.FetchResponse;
import org.picketlink.social.standalone.fb.FacebookConstants;
import org.picketlink.social.standalone.fb.FacebookPrincipal;
import org.picketlink.social.standalone.fb.FacebookProcessor;
import org.picketlink.social.standalone.oauth.OAuthConstants;
import org.picketlink.social.standalone.oauth.OpenIDProcessor;
import org.picketlink.social.standalone.oauth.OpenIdPrincipal;
import org.picketlink.social.standalone.oauth.StringUtil;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.Principal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Perform external authentication using Facebook Connect and Google OpenID
*
* @author anil saldhana
* @since Sep 19, 2012
*/
public class ExternalAuthentication {
protected static Logger log = Logger.getLogger(ExternalAuthentication.class);
protected boolean trace = log.isTraceEnabled();
private enum AUTH_PROVIDERS {
FACEBOOK, OPENID;
}
private ConsumerManager openIdConsumerManager;
private FetchRequest fetchRequest;
private String openIdServiceUrl = null;
public static final String AUTH_TYPE = "authType";
protected FacebookProcessor facebookProcessor;
protected OpenIDProcessor openidProcessor;
protected String returnURL;
protected String clientID;
protected String clientSecret;
protected String facebookScope = "email";
private String requiredAttributes = "name,email,ax_firstName,ax_lastName,ax_fullName,ax_email";
private String optionalAttributes = null;
// Whether the authenticator has to to save and restore request
protected boolean saveRestoreRequest = true;
private enum STATES {
AUTH, AUTHZ, FINISH
}
;
private enum Providers {
GOOGLE("https://www.google.com/accounts/o8/id"), YAHOO("https://me.yahoo.com/"), MYSPACE("myspace.com"), MYOPENID(
"https://myopenid.com/");
private String name;
Providers(String name) {
this.name = name;
}
String get() {
return name;
}
}
/**
* A comma separated string that represents the roles the web app needs to pass authorization
*
* @param roleStr
*/
public void setRoleString(String roleStr) {
if (roleStr == null)
throw new RuntimeException("Role String is null in configuration");
StringTokenizer st = new StringTokenizer(getSystemPropertyAsString(roleStr), ",");
while (st.hasMoreElements()) {
roles.add(st.nextToken());
}
}
public void setSaveRestoreRequest(boolean saveRestoreRequest) {
this.saveRestoreRequest = saveRestoreRequest;
}
protected List<String> roles = new ArrayList<String>();
/**
* Set the url where the 3rd party authentication service will redirect after authentication
*
* @param returnURL
*/
public void setReturnURL(String returnURL) {
this.returnURL = getSystemPropertyAsString(returnURL);
}
/**
* Set the client id for facebook
*
* @param clientID
*/
public void setClientID(String clientID) {
this.clientID = getSystemPropertyAsString(clientID);
}
/**
* Set the client secret for facebook
*
* @param clientSecret
*/
public void setClientSecret(String clientSecret) {
this.clientSecret = getSystemPropertyAsString(clientSecret);
}
/**
* Set the scope for facebook (Default: email)
*
* @param facebookScope
*/
public void setFacebookScope(String facebookScope) {
this.facebookScope = getSystemPropertyAsString(facebookScope);
}
/**
* Authenticate the request
*
* @param request
* @param response
*
* @return
*
* @throws IOException
* @throws {@link RuntimeException} when the response is not of type catalina response object
*/
public boolean authenticate(HttpServletRequest request, HttpServletResponse response) throws IOException {
if (facebookProcessor == null)
facebookProcessor = new FacebookProcessor(clientID, clientSecret, facebookScope, returnURL, roles);
if (openidProcessor == null)
openidProcessor = new OpenIDProcessor(returnURL, requiredAttributes, optionalAttributes);
HttpSession session = request.getSession();
// Determine the type of service based on request param
String authType = request.getParameter(AUTH_TYPE);
if (authType != null && authType.length() > 0) {
// Place it on the session
session.setAttribute(AUTH_TYPE, authType);
}
if (authType == null || authType.length() == 0) {
authType = (String) session.getAttribute(AUTH_TYPE);
}
if (authType == null) {
authType = AUTH_PROVIDERS.FACEBOOK.name();
}
if (authType != null && authType.equals(AUTH_PROVIDERS.FACEBOOK.name())) {
return processFacebook(request, response);
} else {
return processOpenID(request, response);
}
}
protected boolean processFacebook(HttpServletRequest request, HttpServletResponse response) throws IOException {
HttpSession session = request.getSession();
String state = (String) session.getAttribute("STATE");
if (STATES.FINISH.name().equals(state)) {
Principal principal = request.getUserPrincipal();
if (principal == null) {
principal = getFacebookPrincipal(request, response);
}
if (principal == null) {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return false;
}
return dealWithFacebookPrincipal(request, response, principal);
}
if (state == null || state.isEmpty()) {
return initialFacebookInteraction(request, response);
}
// We have sent an auth request
if (state.equals(STATES.AUTH.name())) {
return facebookProcessor.handleAuthStage(request, response);
}
// Principal facebookPrincipal = null;
if (state.equals(STATES.AUTHZ.name())) {
Principal principal = getFacebookPrincipal(request, response);
if (principal == null) {
log.error("Principal was null. Maybe login modules need to be configured properly. Or user chose no data");
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return false;
}
return dealWithFacebookPrincipal(request, response, principal);
}
return false;
}
protected boolean processOpenID(HttpServletRequest request, HttpServletResponse response) throws IOException {
Principal userPrincipal = request.getUserPrincipal();
if (userPrincipal != null) {
if (trace)
log.trace("Logged in as:" + userPrincipal);
return true;
}
if (!openidProcessor.isInitialized()) {
try {
openidProcessor.initialize(roles);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
HttpSession httpSession = request.getSession();
String state = (String) httpSession.getAttribute("STATE");
if (trace)
log.trace("state=" + state);
if (STATES.FINISH.name().equals(state)) {
// This is a replay. We need to resend a request back to the OpenID provider
httpSession.setAttribute("STATE", STATES.AUTH.name());
return prepareAndSendAuthRequest(request, response);
}
if (state == null || state.isEmpty()) {
return prepareAndSendAuthRequest(request, response);
}
// We have sent an auth request
if (state.equals(STATES.AUTH.name())) {
Principal principal = processIncomingAuthResult(request, response);
if (principal == null) {
log.error("Principal was null. Maybe login modules need to be configured properly. Or user chose no data");
return false;
}
return dealWithOpenIDPrincipal(request, response, principal);
}
return false;
}
public boolean initialFacebookInteraction(HttpServletRequest request, HttpServletResponse response) throws IOException {
HttpSession session = request.getSession();
Map<String, String> params = new HashMap<String, String>();
params.put(OAuthConstants.REDIRECT_URI_PARAMETER, returnURL);
params.put(OAuthConstants.CLIENT_ID_PARAMETER, clientID);
if (facebookScope != null) {
params.put(OAuthConstants.SCOPE_PARAMETER, facebookScope);
}
String location = new StringBuilder(FacebookConstants.SERVICE_URL).append("?").append(createFacebookQueryString(params))
.toString();
try {
session.setAttribute("STATE", STATES.AUTH.name());
if (trace)
log.trace("Redirect:" + location);
response.sendRedirect(location);
return false;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private boolean dealWithFacebookPrincipal(HttpServletRequest request, HttpServletResponse response, Principal principal) throws IOException {
SocialRequestWrapper requestWrapper = (SocialRequestWrapper) request;
requestWrapper.setUserPrincipal(principal);
request.getSession().setAttribute("STATE", STATES.FINISH.name());
return true;
}
private boolean dealWithOpenIDPrincipal(HttpServletRequest request, HttpServletResponse response, Principal principal) throws IOException {
HttpSession httpSession = request.getSession();
SocialRequestWrapper requestWrapper = (SocialRequestWrapper) request;
requestWrapper.setUserPrincipal(principal);
if (trace)
log.trace("Logged in as:" + principal);
httpSession.setAttribute("STATE", STATES.FINISH.name());
return true;
}
public Principal getFacebookPrincipal(HttpServletRequest request, HttpServletResponse response) {
Principal facebookPrincipal = handleFacebookAuthenticationResponse(request, response);
if (facebookPrincipal == null)
return null;
request.getSession().setAttribute("PRINCIPAL", facebookPrincipal);
return facebookPrincipal;
}
protected Principal handleFacebookAuthenticationResponse(HttpServletRequest request, HttpServletResponse response) {
String error = request.getParameter(OAuthConstants.ERROR_PARAMETER);
if (error != null) {
throw new RuntimeException("error:" + error);
} else {
String returnUrl = returnURL;
String authorizationCode = request.getParameter(OAuthConstants.CODE_PARAMETER);
if (authorizationCode == null) {
log.error("Authorization code parameter not found");
return null;
}
URLConnection connection = sendFacebookAccessTokenRequest(returnUrl, authorizationCode, response);
Map<String, String> params = formUrlDecode(readUrlContent(connection));
String accessToken = params.get(OAuthConstants.ACCESS_TOKEN_PARAMETER);
String expires = params.get(FacebookConstants.EXPIRES);
if (trace)
log.trace("Access Token=" + accessToken + " :: Expires=" + expires);
if (accessToken == null) {
throw new RuntimeException("No access token found");
}
return readInIdentity(request, response, accessToken, returnUrl);
}
}
protected URLConnection sendFacebookAccessTokenRequest(String returnUrl, String authorizationCode, HttpServletResponse response) {
String returnUri = returnURL;
Map<String, String> params = new HashMap<String, String>();
params.put(OAuthConstants.REDIRECT_URI_PARAMETER, returnUri);
params.put(OAuthConstants.CLIENT_ID_PARAMETER, clientID);
params.put(OAuthConstants.CLIENT_SECRET_PARAMETER, clientSecret);
params.put(OAuthConstants.CODE_PARAMETER, authorizationCode);
String location = new StringBuilder(FacebookConstants.ACCESS_TOKEN_ENDPOINT_URL).append("?")
.append(createFacebookQueryString(params)).toString();
try {
if (trace)
log.trace("AccessToken Request=" + location);
URL url = new URL(location);
URLConnection connection = url.openConnection();
return connection;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
private boolean prepareAndSendAuthRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {
// Figure out the service url
String authType = request.getParameter(AUTH_TYPE);
if (authType == null || authType.length() == 0) {
authType = (String) request.getSession().getAttribute(AUTH_TYPE);
}
determineServiceUrl(authType);
String openId = openIdServiceUrl;
HttpSession session = request.getSession(true);
if (openId != null) {
session.setAttribute("openid", openId);
List<DiscoveryInformation> discoveries;
try {
discoveries = openIdConsumerManager.discover(openId);
} catch (DiscoveryException e) {
throw new RuntimeException(e);
}
DiscoveryInformation discovered = openIdConsumerManager.associate(discoveries);
session.setAttribute("discovery", discovered);
try {
AuthRequest authReq = openIdConsumerManager.authenticate(discovered, returnURL);
// Add in required attributes
authReq.addExtension(fetchRequest);
String url = authReq.getDestinationUrl(true);
response.sendRedirect(url);
request.getSession().setAttribute("STATE", STATES.AUTH.name());
return false;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return false;
}
private void determineServiceUrl(String service) {
openIdServiceUrl = Providers.GOOGLE.get();
if (StringUtil.isNotNull(service)) {
if ("google".equals(service))
openIdServiceUrl = Providers.GOOGLE.get();
else if ("yahoo".equals(service))
openIdServiceUrl = Providers.YAHOO.get();
else if ("myspace".equals(service))
openIdServiceUrl = Providers.MYSPACE.get();
else if ("myopenid".equals(service))
openIdServiceUrl = Providers.MYOPENID.get();
}
}
private String createFacebookQueryString(Map<String, String> params) {
StringBuilder queryString = new StringBuilder();
boolean first = true;
for (Map.Entry<String, String> entry : params.entrySet()) {
String paramName = entry.getKey();
String paramValue = entry.getValue();
if (first) {
first = false;
} else {
queryString.append("&");
}
queryString.append(paramName).append("=");
String encodedParamValue;
try {
if (paramValue == null)
throw new RuntimeException("paramValue is null");
encodedParamValue = URLEncoder.encode(paramValue, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
queryString.append(encodedParamValue);
}
return queryString.toString();
}
private Principal readInIdentity(HttpServletRequest request, HttpServletResponse response, String accessToken,
String returnUrl) {
FacebookPrincipal facebookPrincipal = null;
try {
String urlString = new StringBuilder(FacebookConstants.PROFILE_ENDPOINT_URL).append("?access_token=")
.append(URLEncoder.encode(accessToken, "UTF-8")).toString();
if (trace)
log.trace("Profile read:" + urlString);
URL profileUrl = new URL(urlString);
String profileContent = readUrlContent(profileUrl.openConnection());
JSONObject jsonObject = new JSONObject(profileContent);
facebookPrincipal = new FacebookPrincipal();
facebookPrincipal.setAccessToken(accessToken);
facebookPrincipal.setId(jsonObject.getString("id"));
facebookPrincipal.setName(jsonObject.getString("name"));
facebookPrincipal.setFirstName(jsonObject.getString("first_name"));
facebookPrincipal.setLastName(jsonObject.getString("last_name"));
facebookPrincipal.setGender(jsonObject.getString("gender"));
facebookPrincipal.setTimezone(jsonObject.getString("timezone"));
facebookPrincipal.setLocale(jsonObject.getString("locale"));
if (jsonObject.getString("email") != null) {
facebookPrincipal.setEmail(jsonObject.getString("email"));
}
} catch (JSONException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
return facebookPrincipal;
}
private String readUrlContent(URLConnection connection) {
StringBuilder result = new StringBuilder();
Reader reader = null;
try {
reader = new InputStreamReader(connection.getInputStream());
char[] buffer = new char[50];
int nrOfChars;
while ((nrOfChars = reader.read(buffer)) != -1) {
result.append(buffer, 0, nrOfChars);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally{
try{
if(reader != null){
reader.close();
}
}catch (IOException ignore){
}
}
return result.toString();
}
private Map<String, String> formUrlDecode(String encodedData) {
Map<String, String> params = new HashMap<String, String>();
String[] elements = encodedData.split("&");
for (String element : elements) {
String[] pair = element.split("=");
if (pair.length == 2) {
String paramName = pair[0];
String paramValue;
try {
paramValue = URLDecoder.decode(pair[1], "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
params.put(paramName, paramValue);
} else {
throw new RuntimeException("Unexpected name-value pair in response: " + element);
}
}
return params;
}
/**
* <p>
* Get the system property value if the string is of the format ${sysproperty}
* </p>
* <p>
* You can insert default value when the system property is not set, by separating it at the beginning with ::
* </p>
* <p>
* <b>Examples:</b>
* </p>
*
* <p>
* ${idp} should resolve to a value if the system property "idp" is set.
* </p>
* <p>
* ${idp::http://localhost:8080} will resolve to http://localhost:8080 if the system property "idp" is not set.
* </p>
*
* @param str
*
* @return
*/
private String getSystemPropertyAsString(String str) {
if (str.contains("${")) {
Pattern pattern = Pattern.compile("\\$\\{([^}]+)}");
Matcher matcher = pattern.matcher(str);
StringBuffer buffer = new StringBuffer();
String sysPropertyValue = null;
while (matcher.find()) {
String subString = matcher.group(1);
String defaultValue = "";
// Look for default value
if (subString.contains("::")) {
int index = subString.indexOf("::");
defaultValue = subString.substring(index + 2);
subString = subString.substring(0, index);
}
sysPropertyValue = SecurityActions.getSystemProperty(subString, defaultValue);
matcher.appendReplacement(buffer, sysPropertyValue);
}
matcher.appendTail(buffer);
str = buffer.toString();
}
return str;
}
@SuppressWarnings("unchecked")
public Principal processIncomingAuthResult(HttpServletRequest request, HttpServletResponse response) throws IOException {
Principal principal = null;
HttpSession session = request.getSession(false);
if (session == null)
throw new RuntimeException("wrong lifecycle: session was null");
// extract the parameters from the authentication response
// (which comes in as a HTTP request from the OpenID provider)
ParameterList responseParamList = new ParameterList(request.getParameterMap());
// retrieve the previously stored discovery information
DiscoveryInformation discovered = (DiscoveryInformation) session.getAttribute("discovery");
if (discovered == null)
throw new RuntimeException("discovered information was null");
// extract the receiving URL from the HTTP request
StringBuffer receivingURL = request.getRequestURL();
String queryString = request.getQueryString();
if (queryString != null && queryString.length() > 0)
receivingURL.append("?").append(request.getQueryString());
// verify the response; ConsumerManager needs to be the same
// (static) instance used to place the authentication request
VerificationResult verification;
try {
verification = openIdConsumerManager.verify(receivingURL.toString(), responseParamList, discovered);
} catch (Exception e) {
throw new RuntimeException(e);
}
// examine the verification result and extract the verified identifier
Identifier identifier = verification.getVerifiedId();
if (identifier != null) {
AuthSuccess authSuccess = (AuthSuccess) verification.getAuthResponse();
Map<String, List<String>> attributes = null;
if (authSuccess.hasExtension(AxMessage.OPENID_NS_AX)) {
FetchResponse fetchResp;
try {
fetchResp = (FetchResponse) authSuccess.getExtension(AxMessage.OPENID_NS_AX);
} catch (MessageException e) {
throw new RuntimeException(e);
}
attributes = fetchResp.getAttributes();
}
principal = createOpenIDPrincipal(identifier.getIdentifier(), discovered.getOPEndpoint(),
attributes);
request.getSession().setAttribute("PRINCIPAL", principal);
if (trace)
log.trace("Logged in as:" + principal);
} else {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
return principal;
}
private OpenIdPrincipal createOpenIDPrincipal(String identifier, URL openIdProvider, Map<String, List<String>> attributes) {
return new OpenIdPrincipal(identifier, openIdProvider, attributes);
}
}