From 3519f9a71684c3a4f1f1785b4618ce8a7c588e86 Mon Sep 17 00:00:00 2001 From: Travis Masselink Date: Wed, 3 Aug 2016 16:58:31 -0600 Subject: [PATCH 1/2] Multi-device support for AuthenticateOnline and UnpairDevice operations --- pom.xml | 2 +- .../developer/pingid/Operation.java | 66 +++++++++++++------ .../pingidentity/developer/pingid/User.java | 4 ++ .../playground/pingid/APIHandlerServlet.java | 10 ++- src/main/webapp/index.jsp | 7 ++ 5 files changed, 65 insertions(+), 24 deletions(-) diff --git a/pom.xml b/pom.xml index 070da9e..438daf3 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.pingidentity.developer pingid-api-playground war - 1.0.0 + 1.0.1 pingid-api-playground http://maven.apache.org diff --git a/src/main/java/com/pingidentity/developer/pingid/Operation.java b/src/main/java/com/pingidentity/developer/pingid/Operation.java index 9d70dbf..b783a29 100644 --- a/src/main/java/com/pingidentity/developer/pingid/Operation.java +++ b/src/main/java/com/pingidentity/developer/pingid/Operation.java @@ -5,8 +5,10 @@ import java.net.HttpURLConnection; import java.net.URL; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.TimeZone; @@ -16,6 +18,7 @@ import org.jose4j.jws.JsonWebSignature; import org.jose4j.keys.HmacKey; import org.jose4j.lang.JoseException; +import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; @@ -23,7 +26,9 @@ public class Operation { - + + private static final String ENDPOINT_BASE = "https://idpxnyl3m.pingidentity.com/pingid/rest/4/"; + private String name; private String endpoint; private String requestToken; @@ -47,7 +52,7 @@ public class Operation { private String clientData; private User user; - private final String apiVersion = "4.6"; + private final String apiVersion = "4.9"; public Operation() { } @@ -86,7 +91,7 @@ public Operation(String orgAlias, String token, String useBase64Key) { public void AddUser(Boolean activateUser) { this.name = "AddUser"; - this.endpoint = "https://idpxnyl3m.pingidentity.com/pingid/rest/4/adduser/do"; + this.endpoint = ENDPOINT_BASE + "adduser/do"; JSONObject reqBody = new JSONObject(); reqBody.put("activateUser", activateUser); @@ -111,7 +116,7 @@ public void AddUser(Boolean activateUser) { public void EditUser(Boolean activateUser) { this.name = "EditUser"; - this.endpoint = "https://idpxnyl3m.pingidentity.com/pingid/rest/4/edituser/do"; + this.endpoint = ENDPOINT_BASE + "edituser/do"; JSONObject reqBody = new JSONObject(); reqBody.put("activateUser", activateUser); @@ -136,7 +141,7 @@ public void EditUser(Boolean activateUser) { public void GetUserDetails() { this.name = "GetUserDetails"; - this.endpoint = "https://idpxnyl3m.pingidentity.com/pingid/rest/4/getuserdetails/do"; + this.endpoint = ENDPOINT_BASE + "getuserdetails/do"; JSONObject reqBody = new JSONObject(); reqBody.put("getSameDeviceUsers", false); @@ -152,15 +157,26 @@ public void GetUserDetails() { JSONObject userDetails = (JSONObject)response.get("userDetails"); this.user = new User(userDetails); - DeviceDetails deviceDetails = new DeviceDetails((JSONObject)userDetails.get("deviceDetails")); + DeviceDetails deviceDetails = new DeviceDetails(); + List devices = new ArrayList(); + if(userDetails != null) { + JSONArray devicesArray = (JSONArray)userDetails.get("devicesDetails"); + if(devicesArray != null) { + for(int i = 0; i < devicesArray.size(); i++) { + devices.add(new DeviceDetails((JSONObject)devicesArray.get(i))); + } + } + deviceDetails = new DeviceDetails((JSONObject)userDetails.get("deviceDetails")); + } this.user.setDeviceDetails(deviceDetails); + this.user.setDevices(devices); } @SuppressWarnings("unchecked") public void DeleteUser() { this.name = "DeleteUser"; - this.endpoint = "https://idpxnyl3m.pingidentity.com/pingid/rest/4/deleteuser/do"; + this.endpoint = ENDPOINT_BASE + "deleteuser/do"; JSONObject reqBody = new JSONObject(); reqBody.put("userName", this.user.getUserName()); @@ -177,7 +193,7 @@ public void DeleteUser() { public void SuspendUser() { this.name = "SuspendUser"; - this.endpoint = "https://idpxnyl3m.pingidentity.com/pingid/rest/4/suspenduser/do"; + this.endpoint = ENDPOINT_BASE + "suspenduser/do"; JSONObject reqBody = new JSONObject(); reqBody.put("userName", this.user.getUserName()); @@ -194,7 +210,7 @@ public void SuspendUser() { public void ActivateUser() { this.name = "ActivateUser"; - this.endpoint = "https://idpxnyl3m.pingidentity.com/pingid/rest/4/activateuser/do"; + this.endpoint = ENDPOINT_BASE + "activateuser/do"; JSONObject reqBody = new JSONObject(); reqBody.put("userName", this.user.getUserName()); @@ -211,7 +227,7 @@ public void ActivateUser() { public void ToggleUserBypass(long until) { this.name = "ToggleUserBypass"; - this.endpoint = "https://idpxnyl3m.pingidentity.com/pingid/rest/4/userbypass/do"; + this.endpoint = ENDPOINT_BASE + "userbypass/do"; JSONObject reqBody = new JSONObject(); reqBody.put("userName", this.user.getUserName()); @@ -226,15 +242,18 @@ public void ToggleUserBypass(long until) { } @SuppressWarnings("unchecked") - public void UnpairDevice() { + public void UnpairDevice(long deviceId) { this.name = "UnpairDevice"; - this.endpoint = "https://idpxnyl3m.pingidentity.com/pingid/rest/4/unpairdevice/do"; + this.endpoint = ENDPOINT_BASE + "unpairdevice/do"; JSONObject reqBody = new JSONObject(); reqBody.put("userName", this.user.getUserName()); reqBody.put("clientData", this.clientData); + if(deviceId > 0) + reqBody.put("deviceId", deviceId); + this.requestToken = buildRequestToken(reqBody); sendRequest(); @@ -246,7 +265,7 @@ public void UnpairDevice() { public void GetPairingStatus(String activationCode) { this.name = "GetPairingStatus"; - this.endpoint = "https://idpxnyl3m.pingidentity.com/pingid/rest/4/pairingstatus/do"; + this.endpoint = ENDPOINT_BASE + "pairingstatus/do"; JSONObject reqBody = new JSONObject(); reqBody.put("activationCode", activationCode); @@ -264,7 +283,7 @@ public void GetPairingStatus(String activationCode) { public void PairYubiKey(String otp) { this.name = "PairYubiKey"; - this.endpoint = "https://idpxnyl3m.pingidentity.com/pingid/rest/4/pairyubikey/do"; + this.endpoint = ENDPOINT_BASE + "pairyubikey/do"; JSONObject reqBody = new JSONObject(); reqBody.put("otp", otp); @@ -282,7 +301,7 @@ public void PairYubiKey(String otp) { public void StartOfflinePairing(OfflinePairingMethod pairingMethod) { this.name = "StartOfflinePairing"; - this.endpoint = "https://idpxnyl3m.pingidentity.com/pingid/rest/4/startofflinepairing/do"; + this.endpoint = ENDPOINT_BASE + "startofflinepairing/do"; JSONObject reqBody = new JSONObject(); @@ -311,7 +330,7 @@ public void StartOfflinePairing(OfflinePairingMethod pairingMethod) { public void FinalizeOfflinePairing(String sessionId, String otp) { this.name = "FinalizeOfflinePairing"; - this.endpoint = "https://idpxnyl3m.pingidentity.com/pingid/rest/4/finalizeofflinepairing/do"; + this.endpoint = ENDPOINT_BASE + "finalizeofflinepairing/do"; JSONObject reqBody = new JSONObject(); reqBody.put("otp", otp); @@ -329,7 +348,7 @@ public void FinalizeOfflinePairing(String sessionId, String otp) { public void GetActivationCode() { this.name = "GetActivationCode"; - this.endpoint = "https://idpxnyl3m.pingidentity.com/pingid/rest/4/getactivationcode/do"; + this.endpoint = ENDPOINT_BASE + "getactivationcode/do"; JSONObject reqBody = new JSONObject(); reqBody.put("userName", this.user.getUserName()); @@ -342,12 +361,16 @@ public void GetActivationCode() { values.clear(); this.lastActivationCode = (String)response.get("activationCode"); } + + public void AuthenticateOnline(Application application, String authType) { + AuthenticateOnline(application, authType, 0); + } @SuppressWarnings("unchecked") - public void AuthenticateOnline(Application application, String authType) { + public void AuthenticateOnline(Application application, String authType, long deviceId) { this.name = "AuthenticateOnline"; - this.endpoint = "https://idpxnyl3m.pingidentity.com/pingid/rest/4/authonline/do"; + this.endpoint = ENDPOINT_BASE + "authonline/do"; JSONObject reqBody = new JSONObject(); reqBody.put("authType", authType); @@ -355,6 +378,9 @@ public void AuthenticateOnline(Application application, String authType) { reqBody.put("userName", this.user.getUserName()); reqBody.put("clientData", this.clientData); + if(deviceId > 0) + reqBody.put("deviceId", deviceId); + JSONObject formParameters = new JSONObject(); formParameters.put("sp_name", application.getName()); if (application.getLogoUrl() != null || !application.getLogoUrl().isEmpty()) { @@ -378,7 +404,7 @@ public void AuthenticateOnline(Application application, String authType) { public void AuthenticateOffline(String sessionId, String otp) { this.name = "AuthenticateOffline"; - this.endpoint = "https://idpxnyl3m.pingidentity.com/pingid/rest/4/authoffline/do"; + this.endpoint = ENDPOINT_BASE + "authoffline/do"; JSONObject reqBody = new JSONObject(); reqBody.put("spAlias", "web"); diff --git a/src/main/java/com/pingidentity/developer/pingid/User.java b/src/main/java/com/pingidentity/developer/pingid/User.java index 7f2ad76..c6ed159 100644 --- a/src/main/java/com/pingidentity/developer/pingid/User.java +++ b/src/main/java/com/pingidentity/developer/pingid/User.java @@ -4,6 +4,7 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.TimeZone; @@ -20,6 +21,7 @@ public class User { private String phoneNumber; private DeviceDetails deviceDetails; + private List devices; private Date lastAuthentication; private Boolean enabled; @@ -106,6 +108,7 @@ public User(JSONObject userDetailsJSON) { public UserStatus getStatus() { return this.status; } public Date getBypassedUntil(String spAlias) { return this.bypassInfo.get(spAlias); } public Date getLastAuthentication() { return this.lastAuthentication; } + public List getDevices() { return devices; } public void setUserName(String userName) { this.userName = userName; } public void setFirstName(String firstName) { this.fName = firstName; } @@ -114,6 +117,7 @@ public User(JSONObject userDetailsJSON) { public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; } public void setRole(UserRole role) { this.role = role; } public void setDeviceDetails(DeviceDetails deviceDetails) { this.deviceDetails = deviceDetails; } + public void setDevices(List devices) { this.devices = devices; } @SuppressWarnings("unused") private Date parseDate(String dateToParse) { diff --git a/src/main/java/com/pingidentity/developer/playground/pingid/APIHandlerServlet.java b/src/main/java/com/pingidentity/developer/playground/pingid/APIHandlerServlet.java index 51bd352..e530d7c 100644 --- a/src/main/java/com/pingidentity/developer/playground/pingid/APIHandlerServlet.java +++ b/src/main/java/com/pingidentity/developer/playground/pingid/APIHandlerServlet.java @@ -14,8 +14,8 @@ import com.cedarsoftware.util.io.JsonWriter; import com.pingidentity.developer.pingid.Application; import com.pingidentity.developer.pingid.OfflinePairingMethod; -import com.pingidentity.developer.pingid.User; import com.pingidentity.developer.pingid.Operation; +import com.pingidentity.developer.pingid.User; public class APIHandlerServlet extends HttpServlet { @@ -28,13 +28,17 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) Operation operation = new Operation((String)session.getAttribute("pingid_org_alias"), (String)session.getAttribute("pingid_token"), (String)session.getAttribute("pingid_use_base64_key"));; operation.setTargetUser(targetUsername); + String deviceIdObject = (String)request.getParameter("deviceId"); + long deviceId = deviceIdObject != null && !deviceIdObject.isEmpty() ? Long.parseLong(deviceIdObject) : 0; + switch (requestedOperation) { case "AuthenticateOnline": Application app = new Application(request.getParameter("applicationName")); app.setLogoUrl(request.getParameter("applicationIconUrl")); app.setSpAlias("web"); - operation.AuthenticateOnline(app, request.getParameter("authType")); + + operation.AuthenticateOnline(app, request.getParameter("authType"), deviceId); request.setAttribute("lastSessionId", operation.getLastSessionId()); break; @@ -139,7 +143,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) break; case "UnpairDevice": - operation.UnpairDevice(); + operation.UnpairDevice(deviceId); break; default: diff --git a/src/main/webapp/index.jsp b/src/main/webapp/index.jsp index 5c3fb05..2fad4c9 100644 --- a/src/main/webapp/index.jsp +++ b/src/main/webapp/index.jsp @@ -79,7 +79,11 @@ + + + @@ -330,6 +334,9 @@ + + From 6f47579d683f0f47a25ccedd3dd7f41063a19439 Mon Sep 17 00:00:00 2001 From: Travis Masselink Date: Fri, 19 Aug 2016 09:54:07 -0600 Subject: [PATCH 2/2] QR code generation with the activation code --- pom.xml | 10 ++++ .../playground/pingid/APIHandlerServlet.java | 2 + .../pingid/PropertiesFileUploadServlet.java | 6 +-- .../pingid/StatusPollingServlet.java | 47 ++++++++++++++++++ src/main/webapp/WEB-INF/web.xml | 10 ++++ src/main/webapp/api-operation.jsp | 40 +++++++++++++++ src/main/webapp/assets/ping/img/success.png | Bin 0 -> 7313 bytes 7 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/pingidentity/developer/playground/pingid/StatusPollingServlet.java create mode 100644 src/main/webapp/assets/ping/img/success.png diff --git a/pom.xml b/pom.xml index 438daf3..a6af719 100644 --- a/pom.xml +++ b/pom.xml @@ -20,6 +20,16 @@ 3.0.1 provided + + jstl + jstl + 1.2 + + + taglibs + standard + 1.1.2 + com.googlecode.json-simple json-simple diff --git a/src/main/java/com/pingidentity/developer/playground/pingid/APIHandlerServlet.java b/src/main/java/com/pingidentity/developer/playground/pingid/APIHandlerServlet.java index e530d7c..ee07b8e 100644 --- a/src/main/java/com/pingidentity/developer/playground/pingid/APIHandlerServlet.java +++ b/src/main/java/com/pingidentity/developer/playground/pingid/APIHandlerServlet.java @@ -19,6 +19,8 @@ public class APIHandlerServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String requestedOperation = request.getParameter("operation"); diff --git a/src/main/java/com/pingidentity/developer/playground/pingid/PropertiesFileUploadServlet.java b/src/main/java/com/pingidentity/developer/playground/pingid/PropertiesFileUploadServlet.java index 6795e26..1edf708 100644 --- a/src/main/java/com/pingidentity/developer/playground/pingid/PropertiesFileUploadServlet.java +++ b/src/main/java/com/pingidentity/developer/playground/pingid/PropertiesFileUploadServlet.java @@ -2,7 +2,6 @@ import java.io.IOException; import java.io.InputStream; -import java.io.PrintWriter; import java.util.Properties; import javax.servlet.RequestDispatcher; @@ -16,12 +15,12 @@ import org.apache.commons.fileupload.FileItemStream; import org.apache.commons.fileupload.FileUploadException; import org.apache.commons.fileupload.servlet.ServletFileUpload; -import org.apache.commons.fileupload.util.Streams; -import org.apache.commons.io.IOUtils; public class PropertiesFileUploadServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { if (ServletFileUpload.isMultipartContent(request)) { @@ -32,7 +31,6 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) while(fii.hasNext()) { FileItemStream item = fii.next(); - String name = item.getFieldName(); InputStream is = item.openStream(); if (item.isFormField()) { diff --git a/src/main/java/com/pingidentity/developer/playground/pingid/StatusPollingServlet.java b/src/main/java/com/pingidentity/developer/playground/pingid/StatusPollingServlet.java new file mode 100644 index 0000000..ae5fa7c --- /dev/null +++ b/src/main/java/com/pingidentity/developer/playground/pingid/StatusPollingServlet.java @@ -0,0 +1,47 @@ +package com.pingidentity.developer.playground.pingid; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Map; + +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 com.pingidentity.developer.pingid.Operation; + +public class StatusPollingServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + HttpSession session = request.getSession(false); + String orgAlias = (String) session.getAttribute("pingid_org_alias"); + String token = (String) session.getAttribute("pingid_token"); + String base64Key = (String) session.getAttribute("pingid_use_base64_key"); + + response.setHeader("Content-Type", "text/plain"); + response.setHeader("success", "yes"); + String activationCode = request.getParameter("activationCode"); + Operation operation = new Operation(orgAlias, token, base64Key); + operation.GetPairingStatus(activationCode); + Map values = operation.getReturnValues(); + String returnValue = "NOT_CLAIMED"; + if (values != null && values.containsKey("pairingStatus")) { + returnValue = values.get("pairingStatus").toString(); + } + PrintWriter writer = response.getWriter(); + writer.write(returnValue); + writer.close(); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + doPost(req, resp); + } + +} diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index 1a908da..39ba29f 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -17,6 +17,12 @@ com.pingidentity.developer.playground.pingid.APIHandlerServlet + + StatusPollingServlet + StatusPollingServlet + com.pingidentity.developer.playground.pingid.StatusPollingServlet + + PropertiesFileUploadServlet /handle-upload-props @@ -27,4 +33,8 @@ /api-handler + + StatusPollingServlet + /status + \ No newline at end of file diff --git a/src/main/webapp/api-operation.jsp b/src/main/webapp/api-operation.jsp index 043946c..e25c842 100644 --- a/src/main/webapp/api-operation.jsp +++ b/src/main/webapp/api-operation.jsp @@ -1,5 +1,6 @@ <%@page contentType="text/html" pageEncoding="UTF-8"%> <%@taglib prefix="t" tagdir="/WEB-INF/tags"%> +<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> @@ -36,5 +37,44 @@
${responseTokenHeaderJSON}
.
${responseTokenPayloadJSON}
.
${responseTokenSignatureJSON}
+ + +
+
QR Code
+
+
The activation code (a.k.a the pairing code) can be manually entered into either the mobile apps or the desktop app or it can be encoded in such a way as to produce a QR code. View the source of this page to see how to do that entirely in JavaScript. Note that this also includes a polling process to test if the user has used the QR code or entered in the pairing code manually.
+
+
+
Activation Code: ${lastActivationCode}
+
+
+
+ +
+
+
\ No newline at end of file diff --git a/src/main/webapp/assets/ping/img/success.png b/src/main/webapp/assets/ping/img/success.png new file mode 100644 index 0000000000000000000000000000000000000000..a9ed16daad88641d7220a19ceeab3834bd69f974 GIT binary patch literal 7313 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%0A5{7m?C8&)`P9;zD zn-CLjul%u;BtnHGwm+^$Dr{N)1oqAf&vLRkV)afG#t6aR9mcIbm-VSgS*}kP9x-^+ zLP$sU!|;)sB-f2KcJ?zEx-@-dS|AaQJ1o74ZUIi1q+T97(z4gH>+#YXsl7LSez{ z#MBEdcncbfy@P|oLJ_0T-2X09nJ+TRU7^2cnSsX6=4ca!8I z*^sJZmnM$rP;;#9m|5O6K?VtK1>B=y8Ks8x9)BT8S8If zq&0YiTIUr4hh_~0M^*6QN8E;T(+Y`SQdfWT)@>Oe8gumRR?;wE6g~z|H?zLKXBg5n z8|ZS_oKjWvBuas}garXEg|On&lJ<`MWZt%MV|AMsqj>+u3?L0*su#5>T&%oUy$L^= z6Y{I+)62{fk+b!39e85~lWShq@lUW=2ry{Oqh#bjXl_zXo{b+J@)KcEtvovHW=DA? zCou!?Uyp$*)aU!3-#z{J!qmT<+dZ#eohOXX%UUR||GvES&mi`HQygoyKBZ6F{r+^- Tha7ue8PrnOS1VU}^77vRbK1N| literal 0 HcmV?d00001