| /******************************************************************************* |
| * 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; |
| } |
| } |