blob: 83848ec8d7f2f964b0672d8051791309f3798101 [file] [log] [blame]
/*******************************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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 org.apache.ofbiz.passport.user;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.io.IOException;
import java.io.Serializable;
import java.sql.Timestamp;
import javax.transaction.Transaction;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.ofbiz.passport.event.LinkedInEvents;
import org.apache.ofbiz.passport.util.PassportUtil;
import org.apache.ofbiz.common.authentication.api.Authenticator;
import org.apache.ofbiz.common.authentication.api.AuthenticatorException;
import org.apache.ofbiz.service.LocalDispatcher;
import org.apache.ofbiz.service.GenericServiceException;
import org.apache.ofbiz.service.ServiceUtil;
import org.apache.ofbiz.entity.Delegator;
import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.entity.GenericEntityException;
import org.apache.ofbiz.entity.transaction.TransactionUtil;
import org.apache.ofbiz.entity.transaction.GenericTransactionException;
import org.apache.ofbiz.entity.util.EntityUtil;
import org.apache.ofbiz.base.util.UtilProperties;
import org.apache.ofbiz.base.util.Debug;
import org.apache.ofbiz.base.util.UtilMisc;
import org.apache.ofbiz.base.util.UtilDateTime;
import org.apache.ofbiz.base.util.UtilValidate;
import org.apache.ofbiz.base.util.UtilXml;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* LinkedIn OFBiz Authenticator
*/
public class LinkedInAuthenticator implements Authenticator {
private static final String module = LinkedInAuthenticator.class.getName();
public static final String props = "linkedInAuth.properties";
public static final String resource = "PassportUiLabels";
protected LocalDispatcher dispatcher;
protected Delegator delegator;
/**
* Method called when authenticator is first initialized (the delegator
* object can be obtained from the LocalDispatcher)
*
* @param dispatcher The ServiceDispatcher to use for this Authenticator
*/
public void initialize(LocalDispatcher dispatcher) {
this.dispatcher = dispatcher;
this.delegator = dispatcher.getDelegator();
}
/**
* Method to authenticate a user.
*
* For LinkedIn users, we only check if the username(userLoginId) exists an
* externalAuthId, and the externalAuthId has a valid accessToken in
* LinkedInUser entity.
*
* @param userLoginId User's login id
* @param password User's password
* @param isServiceAuth true if authentication is for a service call
* @return true if the user is authenticated
* @throws org.apache.ofbiz.common.authentication.api.AuthenticatorException
* when a fatal error occurs during authentication
*/
public boolean authenticate(String userLoginId, String password, boolean isServiceAuth) throws AuthenticatorException {
Document user = null;
HttpGet getMethod = null;
try {
GenericValue userLogin = delegator.findOne("UserLogin", UtilMisc.toMap("userLoginId", userLoginId), false);
String externalAuthId = userLogin.getString("externalAuthId");
GenericValue linkedInUser = delegator.findOne("LinkedInUser", UtilMisc.toMap("linedInUserId", externalAuthId), false);
if (linkedInUser != null) {
String accessToken = linkedInUser.getString("accessToken");
if (UtilValidate.isNotEmpty(accessToken)) {
getMethod = new HttpGet(LinkedInEvents.TokenEndpoint + LinkedInEvents.UserApiUri + "?oauth2_access_token=" + accessToken);
user = LinkedInAuthenticator.getUserInfo(getMethod, Locale.getDefault());
}
}
} catch (GenericEntityException e) {
throw new AuthenticatorException(e.getMessage(), e);
} catch (IOException e) {
throw new AuthenticatorException(e.getMessage(), e);
} catch (AuthenticatorException e) {
throw new AuthenticatorException(e.getMessage(), e);
} catch (SAXException e) {
throw new AuthenticatorException(e.getMessage(), e);
} catch (ParserConfigurationException e) {
throw new AuthenticatorException(e.getMessage(), e);
} finally {
if (getMethod != null) {
getMethod.releaseConnection();
}
}
Debug.logInfo("LinkedIn auth called; returned user info: " + user, module);
return user != null;
}
/**
* Logs a user out
*
* @param username User's username
* @throws org.apache.ofbiz.common.authentication.api.AuthenticatorException
* when logout fails
*/
public void logout(String username) throws AuthenticatorException {
}
/**
* Reads user information and syncs it to OFBiz (i.e. UserLogin, Person, etc)
*
* @param userLoginId
* @throws org.apache.ofbiz.common.authentication.api.AuthenticatorException
* user synchronization fails
*/
public void syncUser(String userLoginId) throws AuthenticatorException {
Document user = getLinkedInUserinfo(userLoginId);
GenericValue system;
try {
system = delegator.findOne("UserLogin", UtilMisc.toMap("userLoginId", "system"), true);
} catch (GenericEntityException e) {
throw new AuthenticatorException(e.getMessage(), e);
}
GenericValue userLogin;
try {
userLogin = EntityUtil.getFirst(delegator.findByAnd("UserLogin", UtilMisc.toMap("externalAuthId", getLinkedInUserId(user)), null, false));
} catch (GenericEntityException e) {
throw new AuthenticatorException(e.getMessage(), e);
}
// suspend the current transaction and load the user
Transaction parentTx = null;
boolean beganTransaction = false;
try {
try {
parentTx = TransactionUtil.suspend();
} catch (GenericTransactionException e) {
Debug.logError(e, "Could not suspend transaction: " + e.getMessage(), module);
}
try {
beganTransaction = TransactionUtil.begin();
if (userLogin == null) {
// create the user
createUser(user, system);
} else {
// update the user information
updateUser(user, system, userLogin);
}
} catch (GenericTransactionException e) {
Debug.logError(e, "Could not suspend transaction: " + e.getMessage(), module);
} finally {
try {
TransactionUtil.commit(beganTransaction);
} catch (GenericTransactionException e) {
Debug.logError(e, "Could not commit nested transaction: " + e.getMessage(), module);
}
}
} finally {
// resume/restore parent transaction
if (parentTx != null) {
try {
TransactionUtil.resume(parentTx);
Debug.logVerbose("Resumed the parent transaction.", module);
} catch (GenericTransactionException e) {
Debug.logError(e, "Could not resume parent nested transaction: " + e.getMessage(), module);
}
}
}
}
private Document getLinkedInUserinfo(String userLoginId) throws AuthenticatorException {
Document user = null;
HttpGet getMethod = null;
try {
GenericValue userLogin = delegator.findOne("UserLogin", UtilMisc.toMap("userLoginId", userLoginId), false);
String externalAuthId = userLogin.getString("externalAuthId");
GenericValue linkedInUser = delegator.findOne("LinkedInUser", UtilMisc.toMap("linkedInUserId", externalAuthId), false);
if (linkedInUser != null) {
String accessToken = linkedInUser.getString("accessToken");
if (UtilValidate.isNotEmpty(accessToken)) {
getMethod = new HttpGet(LinkedInEvents.TokenEndpoint + LinkedInEvents.UserApiUri + "?oauth2_access_token=" + accessToken);
user = getUserInfo(getMethod, Locale.getDefault());
}
}
} catch (GenericEntityException e) {
throw new AuthenticatorException(e.getMessage(), e);
} catch (IOException e) {
throw new AuthenticatorException(e.getMessage(), e);
} catch (AuthenticatorException e) {
throw new AuthenticatorException(e.getMessage(), e);
} catch (SAXException e) {
throw new AuthenticatorException(e.getMessage(), e);
} catch (ParserConfigurationException e) {
throw new AuthenticatorException(e.getMessage(), e);
} finally {
if (getMethod != null) {
getMethod.releaseConnection();
}
}
return user;
}
public String createUser(Document user) throws AuthenticatorException {
GenericValue system;
try {
system = delegator.findOne("UserLogin", UtilMisc.toMap("userLoginId", "system"), true);
} catch (GenericEntityException e) {
throw new AuthenticatorException(e.getMessage(), e);
}
return createUser(user, system);
}
private String createUser(Document user, GenericValue system) throws AuthenticatorException {
Map<String, String> userInfo = parseLinkedInUserInfo(user);
// create person + userLogin
Map<String, Serializable> createPersonUlMap = new HashMap<String, Serializable>();
String userLoginId = delegator.getNextSeqId("UserLogin");
if (userInfo.containsKey("firstName")) {
createPersonUlMap.put("firstName", userInfo.get("firstName"));
}
if (userInfo.containsKey("lastName")) {
createPersonUlMap.put("lastName", userInfo.get("lastName"));
}
if (userInfo.containsKey("userId")) {
createPersonUlMap.put("externalAuthId", userInfo.get("userId"));
}
// createPersonUlMap.put("externalId", user.getUserId());
createPersonUlMap.put("userLoginId", userLoginId);
createPersonUlMap.put("currentPassword", "[EXTERNAL]");
createPersonUlMap.put("currentPasswordVerify", "[EXTERNAL]");
createPersonUlMap.put("userLogin", system);
Map<String, Object> createPersonResult;
try {
createPersonResult = dispatcher.runSync("createPersonAndUserLogin", createPersonUlMap);
} catch (GenericServiceException e) {
throw new AuthenticatorException(e.getMessage(), e);
}
if (ServiceUtil.isError(createPersonResult)) {
throw new AuthenticatorException(ServiceUtil.getErrorMessage(createPersonResult));
}
String partyId = (String) createPersonResult.get("partyId");
// give this person a role of CUSTOMER
GenericValue partyRole = delegator.makeValue("PartyRole", UtilMisc.toMap("partyId", partyId, "roleTypeId", "CUSTOMER"));
try {
delegator.create(partyRole);
} catch (GenericEntityException e) {
Debug.logError(e, module);
throw new AuthenticatorException(e.getMessage(), e);
}
// create email
if (userInfo.containsKey("emailAddress")) {
Map<String, Serializable> createEmailMap = new HashMap<String, Serializable>();
createEmailMap.put("emailAddress", userInfo.get("emailAddress"));
createEmailMap.put("contactMechPurposeTypeId", "PRIMARY_EMAIL");
createEmailMap.put("partyId", partyId);
createEmailMap.put("userLogin", system);
Map<String, Object> createEmailResult;
try {
createEmailResult = dispatcher.runSync("createPartyEmailAddress", createEmailMap);
} catch (GenericServiceException e) {
throw new AuthenticatorException(e.getMessage(), e);
}
if (ServiceUtil.isError(createEmailResult)) {
throw new AuthenticatorException(ServiceUtil.getErrorMessage(createEmailResult));
}
}
// create security group(s)
Timestamp now = UtilDateTime.nowTimestamp();
for (String securityGroup : (new LinkedInUserGroupMapper(new String[] {"person"}).getSecurityGroups())) {
// check and make sure the security group exists
GenericValue secGroup = null;
try {
secGroup = delegator.findOne("SecurityGroup", UtilMisc.toMap("groupId", securityGroup), true);
} catch (GenericEntityException e) {
Debug.logError(e, e.getMessage(), module);
}
// add it to the user if it exists
if (secGroup != null) {
Map<String, Serializable> createSecGrpMap = new HashMap<String, Serializable>();
createSecGrpMap.put("userLoginId", userLoginId);
createSecGrpMap.put("groupId", securityGroup);
createSecGrpMap.put("fromDate", now);
createSecGrpMap.put("userLogin", system);
Map<String, Object> createSecGrpResult;
try {
createSecGrpResult = dispatcher.runSync("addUserLoginToSecurityGroup", createSecGrpMap);
} catch (GenericServiceException e) {
throw new AuthenticatorException(e.getMessage(), e);
}
if (ServiceUtil.isError(createSecGrpResult)) {
throw new AuthenticatorException(ServiceUtil.getErrorMessage(createSecGrpResult));
}
}
}
return userLoginId;
}
private void updateUser(Document user, GenericValue system, GenericValue userLogin) throws AuthenticatorException {
// TODO implement me
}
/**
* Updates a user's password.
*
* @param username User's username
* @param password User's current password
* @param newPassword User's new password
* @throws org.apache.ofbiz.common.authentication.api.AuthenticatorException
* when update password fails
*/
public void updatePassword(String username, String password, String newPassword) throws AuthenticatorException {
Debug.logInfo("Calling LinkedIn:updatePassword() - ignored!!!", module);
}
/**
* Weight of this authenticator (lower weights are run first)
*
* @return the weight of this Authenicator
*/
public float getWeight() {
return 1;
}
/**
* Is the user synchronzied back to OFBiz
*
* @return true if the user record is copied to the OFB database
*/
public boolean isUserSynchronized() {
return true;
}
/**
* Is this expected to be the only authenticator, if so errors will be thrown when users cannot be found
*
* @return true if this is expected to be the only Authenticator
*/
public boolean isSingleAuthenticator() {
return false;
}
/**
* Flag to test if this Authenticator is enabled
*
* @return true if the Authenticator is enabled
*/
public boolean isEnabled() {
return "true".equalsIgnoreCase(UtilProperties.getPropertyValue(props, "linked.authenticator.enabled", "true"));
}
public static Document getUserInfo(HttpGet httpGet, Locale locale) throws IOException, AuthenticatorException, SAXException, ParserConfigurationException {
Document userInfo = null;
httpGet.setConfig(PassportUtil.StandardRequestConfig);
CloseableHttpClient jsonClient = HttpClients.custom().build();
CloseableHttpResponse getResponse = jsonClient.execute(httpGet);
String responseString = new BasicResponseHandler().handleResponse(getResponse);
if (getResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
// Debug.logInfo("Json Response from LinkedIn: " + responseString, module);
userInfo = UtilXml.readXmlDocument(responseString);
} else {
String errMsg = UtilProperties.getMessage(resource, "GetOAuth2AccessTokenError", UtilMisc.toMap("error", responseString), locale);
throw new AuthenticatorException(errMsg);
}
return userInfo;
}
public static String getLinkedInUserId(Document userInfo) {
NodeList persons = userInfo.getElementsByTagName("person");
if (UtilValidate.isEmpty(persons) || persons.getLength() <= 0) {
return null;
}
Element standardProfileRequest = UtilXml.firstChildElement((Element) persons.item(0), "site-standard-profile-request");
Element url = UtilXml.firstChildElement(standardProfileRequest, "url");
if (UtilValidate.isNotEmpty(url)) {
String urlContent = url.getTextContent();
if (UtilValidate.isNotEmpty(urlContent)) {
String id = urlContent.substring(urlContent.indexOf("?id="));
id = id.substring(0, id.indexOf("&"));
Debug.logInfo("LinkedIn user id: " + id, module);
return id;
}
}
return null;
}
public static Map<String, String> parseLinkedInUserInfo(Document userInfo) {
Map<String, String> results = new HashMap<String, String>();
NodeList persons = userInfo.getElementsByTagName("person");
if (UtilValidate.isEmpty(persons) || persons.getLength() <= 0) {
return results;
}
Element person = (Element) persons.item(0);
Element standardProfileRequest = UtilXml.firstChildElement(person, "site-standard-profile-request");
Element url = UtilXml.firstChildElement(standardProfileRequest, "url");
if (UtilValidate.isNotEmpty(url)) {
String urlContent = url.getTextContent();
if (UtilValidate.isNotEmpty(urlContent)) {
String id = urlContent.substring(urlContent.indexOf("?id="));
id = id.substring(0, id.indexOf("&"));
Debug.logInfo("LinkedIn user id: " + id, module);
results.put("userId", id);
}
}
Element firstNameElement = UtilXml.firstChildElement(person, "first-name");
if (UtilValidate.isNotEmpty(firstNameElement) && UtilValidate.isNotEmpty(firstNameElement.getTextContent())) {
results.put("firstName", firstNameElement.getTextContent());
}
Element lastNameElement = UtilXml.firstChildElement(person, "last-name");
if (UtilValidate.isNotEmpty(lastNameElement) && UtilValidate.isNotEmpty(lastNameElement.getTextContent())) {
results.put("lastName", lastNameElement.getTextContent());
}
Element emailElement = UtilXml.firstChildElement(person, "email-address");
if (UtilValidate.isNotEmpty(emailElement) && UtilValidate.isNotEmpty(emailElement.getTextContent())) {
results.put("emailAddress", emailElement.getTextContent());
}
return results;
}
}