From 28abaedd840aa74f0341371fa3179c0a10622416 Mon Sep 17 00:00:00 2001 From: Bart Date: Wed, 26 Jul 2023 11:08:34 +0100 Subject: [PATCH 01/17] Adding Flex Direct API sample --- flex-v2-direct/README.md | 66 ++ flex-v2-direct/pom.xml | 115 ++++ flex-v2-direct/src/main/appengine/app.yaml | 5 + .../samples/forms/CaptureDataForm.java | 29 + .../samples/forms/EncryptDataForm.java | 29 + .../forms/RequestCaptureContextForm.java | 19 + .../samples/forms/TokenizeForm.java | 29 + .../handlers/CaptureDataFormHandler.java | 70 +++ ...reateCaptureContextRequestFormHandler.java | 60 ++ .../handlers/EncryptDataFormHandler.java | 80 +++ .../RequestCaptureContextFormHandler.java | 61 ++ .../samples/handlers/TokenizeFormHandler.java | 77 +++ .../services/FlexApiHeaderAuthenticator.java | 155 +++++ .../services/FlexApiPublicKeysResolver.java | 53 ++ .../samples/services/FlexApiService.java | 33 + .../resources/META-INF/resources/index.html | 582 ++++++++++++++++++ .../src/main/resources/application.properties | 5 + .../resources/templates/capture-context.html | 44 ++ .../resources/templates/capture-data.html | 37 ++ .../create-capture-context-request.html | 43 ++ .../src/main/resources/templates/jwe.html | 34 + .../resources/templates/transient-token.html | 44 ++ jsp-flexjs/pom.xml | 11 +- pom.xml | 7 +- 24 files changed, 1677 insertions(+), 11 deletions(-) create mode 100644 flex-v2-direct/README.md create mode 100644 flex-v2-direct/pom.xml create mode 100644 flex-v2-direct/src/main/appengine/app.yaml create mode 100644 flex-v2-direct/src/main/java/com/cybersource/samples/forms/CaptureDataForm.java create mode 100644 flex-v2-direct/src/main/java/com/cybersource/samples/forms/EncryptDataForm.java create mode 100644 flex-v2-direct/src/main/java/com/cybersource/samples/forms/RequestCaptureContextForm.java create mode 100644 flex-v2-direct/src/main/java/com/cybersource/samples/forms/TokenizeForm.java create mode 100644 flex-v2-direct/src/main/java/com/cybersource/samples/handlers/CaptureDataFormHandler.java create mode 100644 flex-v2-direct/src/main/java/com/cybersource/samples/handlers/CreateCaptureContextRequestFormHandler.java create mode 100644 flex-v2-direct/src/main/java/com/cybersource/samples/handlers/EncryptDataFormHandler.java create mode 100644 flex-v2-direct/src/main/java/com/cybersource/samples/handlers/RequestCaptureContextFormHandler.java create mode 100644 flex-v2-direct/src/main/java/com/cybersource/samples/handlers/TokenizeFormHandler.java create mode 100644 flex-v2-direct/src/main/java/com/cybersource/samples/services/FlexApiHeaderAuthenticator.java create mode 100644 flex-v2-direct/src/main/java/com/cybersource/samples/services/FlexApiPublicKeysResolver.java create mode 100644 flex-v2-direct/src/main/java/com/cybersource/samples/services/FlexApiService.java create mode 100644 flex-v2-direct/src/main/resources/META-INF/resources/index.html create mode 100644 flex-v2-direct/src/main/resources/application.properties create mode 100644 flex-v2-direct/src/main/resources/templates/capture-context.html create mode 100644 flex-v2-direct/src/main/resources/templates/capture-data.html create mode 100644 flex-v2-direct/src/main/resources/templates/create-capture-context-request.html create mode 100644 flex-v2-direct/src/main/resources/templates/jwe.html create mode 100644 flex-v2-direct/src/main/resources/templates/transient-token.html diff --git a/flex-v2-direct/README.md b/flex-v2-direct/README.md new file mode 100644 index 0000000..b811c62 --- /dev/null +++ b/flex-v2-direct/README.md @@ -0,0 +1,66 @@ +# Cybersource Flex Direct API Java example. + +Live demo of this application is available here: https://flex-v2-java-direct-use-sample.appspot.com. + +Flex Direct API allows merchants to write their own integration based on Transient Token concept. +For example Flex API can be used to isolate systems that capture *payment credentials* from systems that invoke *card services*. +Flex API facilitates keeping *payment credentials* away from merchant's backend processing, keeping those systems away from PCI compliance. + +---- + +## Running the example code + +### Prerequisites + +- Java 11 SDK +- Maven 3.6.3 + +### Setup Instructions + +I. Clone or download this repo. + +II. Modify ```FlexApiHeaderAuthenticator.java``` with the CyberSource REST credentials created through EBC Portal: + +```java +private final String mid = "YOUR MERCHANT ID"; +private final String kid = "YOUR KEY ID (SHARED SECRET SERIAL NUMBER)"; +private final String secret = "YOUR SHARED SECRET"; +``` + +III. Run sample application locally (in development mode): + +```shell +$ mvn clean compile quarkus:dev +``` + +For details, please consult https://quarkus.io/guides/maven-tooling#dev-mode. + +## Few technical details + +### Technology stack + +This application uses [QUARKUS](https://quarkus.io/) to provide framework for sample application. +Sample application leverages most popular Java standards and frameworks as: + +- JAX-RS (RESTEasy) to implement + - Server side HTTP endpoints used to process HTML forms. + - Rest Client to Flex API endpoints: + - ```GET /flex/v2/public-keys``` to retrieve Flex signing keys. + - ```POST /flex/v2/sessions``` to create Capture Context. + - ```POST /flex/v2/tokens``` to create Transient Token. +- jose4j to implement JWT(s) verification, JWE encryption and JWK operation. +- Qute for HTML rendering. + +### Notable packages / classes + +1. ```com.cybersource.samples.forms``` classes to facilitate HTML form POSTs. +2. ```com.cybersource.samples.handlers``` classes with business logic for Flex Direct API flow, namely: + create Capture Context, capture sensitive information, prepare JWE encrypted payload, invoke tokenization. +3. ```FlexApiHeaderAuthenticator.java``` complete HTTP Signature authentication implementation that can be plugged to any JAX-RS client implementation as an ```@Provider```. +4. ```FlexApiPublicKeysResolver.java``` complete cryptographic Key provider for Jose4J that can retrieve and cache Flex API long living keys. + +# Deployment to Google Cloud + +``` +mvn clean package appengine:deploy +``` diff --git a/flex-v2-direct/pom.xml b/flex-v2-direct/pom.xml new file mode 100644 index 0000000..92832a9 --- /dev/null +++ b/flex-v2-direct/pom.xml @@ -0,0 +1,115 @@ + + + 4.0.0 + flex-v2-direct + 1.0.3 + + com.cybersource.examples.flex + cybersource-flex-samples + 1.0 + + + + 11 + 11 + 1.13.3.Final + UTF-8 + + + + + + io.quarkus + quarkus-bom + ${quarkus.version} + pom + import + + + com.google.cloud + libraries-bom + 20.3.0 + pom + import + + + + + + + io.quarkus + quarkus-resteasy + + + io.quarkus + quarkus-resteasy-qute + + + io.quarkus + quarkus-resteasy-jackson + + + io.quarkus + quarkus-rest-client + + + io.quarkus + quarkus-smallrye-jwt + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + com.google.cloud + google-cloud-storage + + + + + + + io.quarkus + quarkus-maven-plugin + ${quarkus.version} + + + + build + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + + org.jboss.logmanager.LogManager + + + + + + com.google.cloud.tools + appengine-maven-plugin + 2.4.0 + + flex-v2-java-direct-use-sample + 3 + ${project.build.directory}/flex-direct-gae-1.0.3-runner.jar + + + + + diff --git a/flex-v2-direct/src/main/appengine/app.yaml b/flex-v2-direct/src/main/appengine/app.yaml new file mode 100644 index 0000000..aa15618 --- /dev/null +++ b/flex-v2-direct/src/main/appengine/app.yaml @@ -0,0 +1,5 @@ +runtime: java11 + +automatic_scaling: + min_instances: 0 + max_instances: 1 \ No newline at end of file diff --git a/flex-v2-direct/src/main/java/com/cybersource/samples/forms/CaptureDataForm.java b/flex-v2-direct/src/main/java/com/cybersource/samples/forms/CaptureDataForm.java new file mode 100644 index 0000000..2766e04 --- /dev/null +++ b/flex-v2-direct/src/main/java/com/cybersource/samples/forms/CaptureDataForm.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2021 by CyberSource + */ +package com.cybersource.samples.forms; + +import javax.ws.rs.FormParam; + +public class CaptureDataForm { + @FormParam("capture-context") + private String captureContext; + @FormParam("data") + private String data; + + public String getCaptureContext() { + return captureContext; + } + + public void setCaptureContext(String captureContext) { + this.captureContext = captureContext; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } +} diff --git a/flex-v2-direct/src/main/java/com/cybersource/samples/forms/EncryptDataForm.java b/flex-v2-direct/src/main/java/com/cybersource/samples/forms/EncryptDataForm.java new file mode 100644 index 0000000..6da2f71 --- /dev/null +++ b/flex-v2-direct/src/main/java/com/cybersource/samples/forms/EncryptDataForm.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2021 by CyberSource + */ +package com.cybersource.samples.forms; + +import javax.ws.rs.FormParam; + +public class EncryptDataForm { + @FormParam("capture-context") + private String captureContext; + @FormParam("data") + private String data; + + public String getCaptureContext() { + return captureContext; + } + + public void setCaptureContext(String captureContext) { + this.captureContext = captureContext; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } +} diff --git a/flex-v2-direct/src/main/java/com/cybersource/samples/forms/RequestCaptureContextForm.java b/flex-v2-direct/src/main/java/com/cybersource/samples/forms/RequestCaptureContextForm.java new file mode 100644 index 0000000..7e59cf5 --- /dev/null +++ b/flex-v2-direct/src/main/java/com/cybersource/samples/forms/RequestCaptureContextForm.java @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2021 by CyberSource + */ +package com.cybersource.samples.forms; + +import javax.ws.rs.FormParam; + +public class RequestCaptureContextForm { + @FormParam("capture-context-request") + private String captureContextRequest; + + public String getCaptureContextRequest() { + return captureContextRequest; + } + + public void setCaptureContextRequest(String captureContextRequest) { + this.captureContextRequest = captureContextRequest; + } +} diff --git a/flex-v2-direct/src/main/java/com/cybersource/samples/forms/TokenizeForm.java b/flex-v2-direct/src/main/java/com/cybersource/samples/forms/TokenizeForm.java new file mode 100644 index 0000000..f697366 --- /dev/null +++ b/flex-v2-direct/src/main/java/com/cybersource/samples/forms/TokenizeForm.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2021 by CyberSource + */ +package com.cybersource.samples.forms; + +import javax.ws.rs.FormParam; + +public class TokenizeForm { + @FormParam("capture-context") + private String captureContext; + @FormParam("encrypted-payload") + private String encryptedPayload; + + public String getCaptureContext() { + return captureContext; + } + + public void setCaptureContext(String captureContext) { + this.captureContext = captureContext; + } + + public String getEncryptedPayload() { + return encryptedPayload; + } + + public void setEncryptedPayload(String encryptedPayload) { + this.encryptedPayload = encryptedPayload; + } +} diff --git a/flex-v2-direct/src/main/java/com/cybersource/samples/handlers/CaptureDataFormHandler.java b/flex-v2-direct/src/main/java/com/cybersource/samples/handlers/CaptureDataFormHandler.java new file mode 100644 index 0000000..ac6ddc0 --- /dev/null +++ b/flex-v2-direct/src/main/java/com/cybersource/samples/handlers/CaptureDataFormHandler.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2021 by CyberSource + */ +package com.cybersource.samples.handlers; + +import com.cybersource.samples.forms.CaptureDataForm; +import io.quarkus.qute.Location; +import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateInstance; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import org.jboss.resteasy.annotations.Form; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import java.util.Base64; + +@Path("/forms") +public class CaptureDataFormHandler { + + @Inject + @Location("capture-data.html") + Template captureDataTemplate; + + @Path("capture-data") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Produces(MediaType.TEXT_HTML) + public TemplateInstance createCaptureContextRequestForm(@Form final CaptureDataForm captureDataForm) { + return captureDataTemplate + .data("captureContext", captureDataForm.getCaptureContext()) + .data("data", payloadFromCaptureContext(captureDataForm.getCaptureContext())); + } + + private String payloadFromCaptureContext(String captureContext) { + JsonObject payload = payload(captureContext); + final JsonObject fields = new JsonObject(); + payload = payload.getJsonArray("ctx").getJsonObject(0).getJsonObject("data"); + JsonArray requiredFields = payload.getJsonArray("requiredFields"); + requiredFields.forEach(field -> addFieldToRequest(fields, field.toString())); + return fields.encodePrettily(); + } + + private void addFieldToRequest(JsonObject fields, String key) { + String[] split = key.split("\\."); + + for (int i = 0; i < split.length - 1; i++) { + if (fields.containsKey(split[i])) { + fields = fields.getJsonObject(split[i]); + } else { + fields.put(split[i--], new JsonObject()); + } + } + + fields.put(split[split.length - 1], ""); + } + + private JsonObject payload(String jwt) { + // nasty way - do not do this at home + jwt = jwt.substring(jwt.indexOf('.') + 1); + jwt = jwt.substring(0, jwt.indexOf('.')); + jwt = new String(Base64.getDecoder().decode(jwt)); + return new JsonObject(jwt); + } + +} diff --git a/flex-v2-direct/src/main/java/com/cybersource/samples/handlers/CreateCaptureContextRequestFormHandler.java b/flex-v2-direct/src/main/java/com/cybersource/samples/handlers/CreateCaptureContextRequestFormHandler.java new file mode 100644 index 0000000..bf77a68 --- /dev/null +++ b/flex-v2-direct/src/main/java/com/cybersource/samples/handlers/CreateCaptureContextRequestFormHandler.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2021 by CyberSource + */ +package com.cybersource.samples.handlers; + +import io.quarkus.qute.Location; +import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateInstance; +import io.vertx.core.json.JsonObject; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; + +@Path("/forms") +public class CreateCaptureContextRequestFormHandler { + + @Inject + @Location("create-capture-context-request.html") + Template createCaptureContextTemplate; + + @Path("create-capture-context-request") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Produces(MediaType.TEXT_HTML) + public TemplateInstance indexForm(final MultivaluedMap parameters) { + final JsonObject captureContextRequest = new JsonObject(); + final JsonObject fields = new JsonObject(); + captureContextRequest.put("fields", fields); + + parameters.forEach((k, v) -> addFieldToRequest(fields, k, v.get(0))); + return createCaptureContextTemplate.data("capture_context_request", captureContextRequest.encodePrettily()); + } + + private void addFieldToRequest(JsonObject fields, String key, String value) { + if ("off".equals(value)) { + return; + } + String[] split = key.split("\\."); + + for (int i = 0; i < split.length; i++) { + if (fields.containsKey(split[i])) { + fields = fields.getJsonObject(split[i]); + } else { + fields.put(split[i--], new JsonObject()); + } + } + + if ("required".equals(value)) { + return; + } + + fields.put("required", false); + } + +} diff --git a/flex-v2-direct/src/main/java/com/cybersource/samples/handlers/EncryptDataFormHandler.java b/flex-v2-direct/src/main/java/com/cybersource/samples/handlers/EncryptDataFormHandler.java new file mode 100644 index 0000000..efeae4a --- /dev/null +++ b/flex-v2-direct/src/main/java/com/cybersource/samples/handlers/EncryptDataFormHandler.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2021 by CyberSource + */ +package com.cybersource.samples.handlers; + +import com.cybersource.samples.forms.EncryptDataForm; +import com.cybersource.samples.services.FlexApiPublicKeysResolver; +import io.quarkus.qute.Location; +import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateInstance; +import io.vertx.core.json.JsonObject; +import org.jboss.resteasy.annotations.Form; +import org.jose4j.jwe.ContentEncryptionAlgorithmIdentifiers; +import org.jose4j.jwe.JsonWebEncryption; +import org.jose4j.jwe.KeyManagementAlgorithmIdentifiers; +import org.jose4j.jwk.JsonWebKey; +import org.jose4j.jwt.MalformedClaimException; +import org.jose4j.jwt.consumer.InvalidJwtException; +import org.jose4j.jwt.consumer.JwtConsumer; +import org.jose4j.jwt.consumer.JwtConsumerBuilder; +import org.jose4j.lang.JoseException; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import java.util.Map; + +@Path("/forms") +public class EncryptDataFormHandler { + + @Inject + @Location("jwe.html") + Template jweTemplate; + + @Inject + FlexApiPublicKeysResolver flexApiPublicKeysResolver; + + @Path("encrypt-data") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Produces(MediaType.TEXT_HTML) + public TemplateInstance createCaptureContextRequestForm(@Form final EncryptDataForm encryptDataForm) { + final JwtConsumer jwtConsumer = new JwtConsumerBuilder() + .setVerificationKeyResolver(flexApiPublicKeysResolver) + .build(); + + try { + final var captureContextClaims = jwtConsumer.processToClaims(encryptDataForm.getCaptureContext()); + final var flx = captureContextClaims.getClaimValue("flx", Map.class); + final Map jwk = (Map) flx.get("jwk"); + final var encryptionKey = JsonWebKey.Factory.newJwk(jwk); + + final var plainText = new JsonObject(); + plainText.put("context", encryptDataForm.getCaptureContext()); + plainText.put("data", new JsonObject(encryptDataForm.getData())); + plainText.put("index", 0); + + JsonWebEncryption encryptedData = new JsonWebEncryption(); + encryptedData.setPlaintext(plainText.encode()); + encryptedData.setKeyIdHeaderValue(encryptionKey.getKeyId()); + encryptedData.setAlgorithmHeaderValue(KeyManagementAlgorithmIdentifiers.RSA_OAEP_256); + encryptedData.setEncryptionMethodHeaderParameter(ContentEncryptionAlgorithmIdentifiers.AES_256_GCM); + encryptedData.setKey(encryptionKey.getKey()); + + return jweTemplate + .data("captureContext", encryptDataForm.getCaptureContext()) + .data("jwe", encryptedData.getCompactSerialization()); + } catch (InvalidJwtException invalidJwtException) { + throw new RuntimeException("Error when parsing VISA provided JWT", invalidJwtException); // i.e. JWT is tampered in transit + } catch (MalformedClaimException malformedClaimException) { + throw new RuntimeException("Error when parsing VISA provided JWT", malformedClaimException); // i.e. cast operation unsuccessful + } catch (JoseException joseException) { + throw new RuntimeException("Error when parsing VISA provided JWT", joseException); // i.e. when parsing one-time use key + } + } + +} diff --git a/flex-v2-direct/src/main/java/com/cybersource/samples/handlers/RequestCaptureContextFormHandler.java b/flex-v2-direct/src/main/java/com/cybersource/samples/handlers/RequestCaptureContextFormHandler.java new file mode 100644 index 0000000..3bcfe2d --- /dev/null +++ b/flex-v2-direct/src/main/java/com/cybersource/samples/handlers/RequestCaptureContextFormHandler.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2021 by CyberSource + */ +package com.cybersource.samples.handlers; + +import com.cybersource.samples.forms.RequestCaptureContextForm; +import com.cybersource.samples.services.FlexApiPublicKeysResolver; +import com.cybersource.samples.services.FlexApiService; +import io.quarkus.qute.Location; +import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateInstance; +import io.vertx.core.json.JsonObject; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.resteasy.annotations.Form; +import org.jose4j.jwt.consumer.InvalidJwtException; +import org.jose4j.jwt.consumer.JwtConsumerBuilder; + +import javax.inject.Inject; +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; + +@Path("/forms") +public class RequestCaptureContextFormHandler { + + @Inject + @Location("capture-context.html") + Template captureContextTemplate; + + @Inject + @RestClient + FlexApiService flexApiService; + + @Inject + FlexApiPublicKeysResolver flexApiPublicKeysResolver; + + @Path("request-capture-context") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Produces(MediaType.TEXT_HTML) + public TemplateInstance createCaptureContextRequestForm(@Form final RequestCaptureContextForm requestCaptureContextForm) { + final var payload = requestCaptureContextForm.getCaptureContextRequest(); + + try { + final var session = flexApiService.createSession(payload); + final var jwtConsumer = new JwtConsumerBuilder() + .setVerificationKeyResolver(flexApiPublicKeysResolver) + .build(); + + final var captureContextClaims = jwtConsumer.processToClaims(session); + + return captureContextTemplate + .data("capture-context", session) + .data("capture-context-claims", new JsonObject(captureContextClaims.toJson()).encodePrettily()); + } catch (WebApplicationException webApplicationException) { + throw new RuntimeException("Network error when requesting session from VISA", webApplicationException); + } catch (InvalidJwtException invalidJwtException) { + throw new RuntimeException("Error when parsing VISA provided JWT", invalidJwtException); // i.e. JWT has expired + } + } + +} diff --git a/flex-v2-direct/src/main/java/com/cybersource/samples/handlers/TokenizeFormHandler.java b/flex-v2-direct/src/main/java/com/cybersource/samples/handlers/TokenizeFormHandler.java new file mode 100644 index 0000000..80ee48f --- /dev/null +++ b/flex-v2-direct/src/main/java/com/cybersource/samples/handlers/TokenizeFormHandler.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2021 by CyberSource + */ +package com.cybersource.samples.handlers; + +import com.cybersource.samples.forms.TokenizeForm; +import com.cybersource.samples.services.FlexApiPublicKeysResolver; +import com.cybersource.samples.services.FlexApiService; +import io.quarkus.qute.Location; +import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateInstance; +import io.vertx.core.json.JsonObject; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.resteasy.annotations.Form; +import org.jose4j.jwk.JsonWebKey; +import org.jose4j.jwt.MalformedClaimException; +import org.jose4j.jwt.consumer.InvalidJwtException; +import org.jose4j.jwt.consumer.JwtConsumerBuilder; +import org.jose4j.lang.JoseException; + +import javax.inject.Inject; +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import java.util.Map; + +@Path("/forms") +public class TokenizeFormHandler { + + @Inject + @Location("transient-token.html") + Template transientTokenTemplate; + + @Inject + @RestClient + FlexApiService flexApiService; + + @Inject + FlexApiPublicKeysResolver flexApiPublicKeysResolver; + + @Path("tokenize") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Produces(MediaType.TEXT_HTML) + public TemplateInstance createCaptureContextRequestForm(@Form final TokenizeForm tokenizeForm) { + final var ccConsumer = new JwtConsumerBuilder() + .setVerificationKeyResolver(flexApiPublicKeysResolver) + .build(); + + try { + final var transientToken = flexApiService.tokenize(tokenizeForm.getEncryptedPayload()); + + final var captureContextClaims = ccConsumer.processToClaims(tokenizeForm.getCaptureContext()); + final var flx = captureContextClaims.getClaimValue("flx", Map.class); + final Map jwk = (Map) flx.get("jwk"); + final var ttValidationKey = JsonWebKey.Factory.newJwk(jwk); + + final var ttConsumer = new JwtConsumerBuilder() + .setVerificationKey(ttValidationKey.getKey()) + .build(); + + final var transientTokenClaims = ttConsumer.processToClaims(transientToken); + + return transientTokenTemplate + .data("transientToken", transientToken) + .data("claims", new JsonObject(transientTokenClaims.toJson()).encodePrettily()); + } catch (WebApplicationException webApplicationException) { + throw new RuntimeException("Error when trying to tokenize data.", webApplicationException); // Network error when tokenizing + } catch (InvalidJwtException invalidJwtException) { + throw new RuntimeException("Error when parsing VISA provided JWT", invalidJwtException); // i.e. JWT (cc) is tampered in transit + } catch (MalformedClaimException malformedClaimException) { + throw new RuntimeException("Error when parsing VISA provided JWT", malformedClaimException); // i.e. cast operation unsuccessful + } catch (JoseException joseException) { + throw new RuntimeException("Error when parsing VISA provided JWT", joseException); // i.e. when parsing one-time use key + } + } + +} diff --git a/flex-v2-direct/src/main/java/com/cybersource/samples/services/FlexApiHeaderAuthenticator.java b/flex-v2-direct/src/main/java/com/cybersource/samples/services/FlexApiHeaderAuthenticator.java new file mode 100644 index 0000000..e46208f --- /dev/null +++ b/flex-v2-direct/src/main/java/com/cybersource/samples/services/FlexApiHeaderAuthenticator.java @@ -0,0 +1,155 @@ +/** + * Copyright (c) 2021 by CyberSource + */ +package com.cybersource.samples.services; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import javax.ws.rs.ConstrainedTo; +import javax.ws.rs.RuntimeType; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.Provider; +import javax.ws.rs.ext.WriterInterceptor; +import javax.ws.rs.ext.WriterInterceptorContext; +import java.io.IOException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.SimpleDateFormat; +import java.util.*; + +@Provider +@ConstrainedTo(RuntimeType.CLIENT) +public class FlexApiHeaderAuthenticator implements WriterInterceptor { + + /** + * Cybersource test credentials - please replace with your CyberSource REST credentials created through EBC Portal. + */ + private final String mid = ""; + private final String kid = ""; + private final String secret = ""; + + private final String host = "apitest.cybersource.com"; + + private static final String HTTP_REQHDR_CONTENTYPE = "content-type"; + /** + * The name of HTTP header carrying the Merchant ID + */ + private static final String HTTP_REQHDR_MIDHEADER = "v-c-merchant-id"; + /** + * The name of HTTP header carrying the current timestamp, please see + * https://tools.ietf.org/html/rfc7231#section-7.1.1.1 + */ + private static final String HTTP_REQHDR_DATE = "date"; + /** + * The name of HTTP header carrying the host part of URL + */ + private static final String HTTP_REQHDR_HOST = "host"; + /** + * The name of HTTP header carrying the http verb and path. Please see + */ + private static final String HTTP_REQHDR_REQUEST_TARGET = "(request-target)"; + /** + * The name of HTTP header carrying the body digest. Please see RFC 3230 + */ + private static final String HTTP_REQHDR_DIGEST = "digest"; + /** + * The name of HTTP header carrying the body digest. Please see RFC 7230 and + * RFC 7540 + */ + private static final String HTTP_REQHDR_SIGNATURE = "signature"; + /** + * Hashing algorithm used for signing HTTP requests. + */ + private static final String HMAC_ALG = "HmacSHA256"; + + private static String getDigest(String payload) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] digest = md.digest(payload.getBytes(StandardCharsets.UTF_8)); + String shaValue = Base64.getEncoder().encodeToString(digest); + return "SHA-256=" + shaValue; + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(); + } + } + + private static String getServerTime() { + Calendar calendar = Calendar.getInstance(); + SimpleDateFormat dateFormat = new SimpleDateFormat( + "EEE, dd MMM yyyy HH:mm:ss z", Locale.US); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + return dateFormat.format(calendar.getTime()); + } + + private static String sign(Map headers, final String keyId, final SecretKeySpec secretKey) { + try { + final Mac sha256HMAC = Mac.getInstance(HMAC_ALG); + sha256HMAC.init(secretKey); + + final StringBuilder signatureString = new StringBuilder(); + final StringBuilder headersString = new StringBuilder(); + + for (Map.Entry e : headers.entrySet()) { + signatureString.append('\n').append(e.getKey().toLowerCase()).append(": ").append(e.getValue()); + headersString.append(' ').append(e.getKey().toLowerCase()); + } + signatureString.delete(0, 1); + headersString.delete(0, 1); + + final StringBuilder signature = new StringBuilder(); + sha256HMAC.update(signatureString.toString().getBytes(StandardCharsets.UTF_8)); + final byte[] hashBytes = sha256HMAC.doFinal(); + + signature.append("keyid=\"").append(keyId).append("\", ") + .append("algorithm=\"HmacSHA256\", ") + .append("headers=\"").append(headersString).append("\", ") + .append("signature=\"").append(Base64.getEncoder().encodeToString(hashBytes)).append('\"'); + + return signature.toString(); + } catch (NoSuchAlgorithmException | InvalidKeyException | IllegalStateException e) { + throw new RuntimeException(e); + } + } + + @Override + public void aroundWriteTo(WriterInterceptorContext writerInterceptorContext) throws IOException, WebApplicationException { + final Method clientMethod = (Method) writerInterceptorContext.getProperty("org.eclipse.microprofile.rest.client.invokedMethod"); + + if ("createSession".equals(clientMethod.getName())) { + final var headers = writerInterceptorContext.getHeaders(); + addAuthHeaders(headers, "post /flex/v2/sessions", writerInterceptorContext.getEntity().toString()); + } + + writerInterceptorContext.proceed(); + } + + private void addAuthHeaders( + final MultivaluedMap headers, + final String requestTarget, + final String payload) { + final Map signedHeaders = new HashMap<>(); + final var date = getServerTime(); + final var digest = getDigest(payload); + + signedHeaders.put(HTTP_REQHDR_HOST, host); + signedHeaders.put(HTTP_REQHDR_DATE, date); + signedHeaders.put(HTTP_REQHDR_REQUEST_TARGET, requestTarget); + signedHeaders.put(HTTP_REQHDR_DIGEST, digest); + signedHeaders.put(HTTP_REQHDR_MIDHEADER, mid); + signedHeaders.put(HTTP_REQHDR_CONTENTYPE, headers.getFirst(HttpHeaders.CONTENT_TYPE).toString()); + + final var secretKey = new SecretKeySpec(Base64.getDecoder().decode(secret), HMAC_ALG); + final var signature = sign(signedHeaders, kid, secretKey); + + headers.add(HttpHeaders.DATE, date); + headers.add("digest", digest); + headers.add("signature", signature); + headers.add("v-c-merchant-id", mid); + } + +} \ No newline at end of file diff --git a/flex-v2-direct/src/main/java/com/cybersource/samples/services/FlexApiPublicKeysResolver.java b/flex-v2-direct/src/main/java/com/cybersource/samples/services/FlexApiPublicKeysResolver.java new file mode 100644 index 0000000..3390f20 --- /dev/null +++ b/flex-v2-direct/src/main/java/com/cybersource/samples/services/FlexApiPublicKeysResolver.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2021 by CyberSource + */ +package com.cybersource.samples.services; + +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jose4j.jwk.PublicJsonWebKey; +import org.jose4j.jwk.RsaJsonWebKey; +import org.jose4j.jws.JsonWebSignature; +import org.jose4j.jwx.JsonWebStructure; +import org.jose4j.keys.resolvers.VerificationKeyResolver; +import org.jose4j.lang.JoseException; +import org.jose4j.lang.UnresolvableKeyException; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.ws.rs.WebApplicationException; +import java.security.Key; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Singleton +public class FlexApiPublicKeysResolver implements VerificationKeyResolver { + + private final Map cache = new ConcurrentHashMap<>(); + + @Inject + @RestClient + FlexApiService flexApiService; + + private PublicJsonWebKey getPublicKey(String kid) throws UnresolvableKeyException { + if (cache.containsKey(kid)) + return cache.get(kid); + + try { + final Map publicKey = flexApiService.publicKey(kid); + final RsaJsonWebKey rsaJsonWebKey = new RsaJsonWebKey(publicKey); + cache.put(kid, rsaJsonWebKey); + return rsaJsonWebKey; + } catch (WebApplicationException webApplicationException) { + throw new UnresolvableKeyException("Network error when retrieving Flex API key from VISA", webApplicationException); + } catch (JoseException joseException) { + throw new UnresolvableKeyException("Unable to parse public key value retrieved from VISA", joseException); + } + } + + @Override + public Key resolveKey(JsonWebSignature jws, List nestingContext) throws UnresolvableKeyException { + final var publicKey = getPublicKey(jws.getKeyIdHeaderValue()); + return publicKey.getKey(); + } +} diff --git a/flex-v2-direct/src/main/java/com/cybersource/samples/services/FlexApiService.java b/flex-v2-direct/src/main/java/com/cybersource/samples/services/FlexApiService.java new file mode 100644 index 0000000..3b64e28 --- /dev/null +++ b/flex-v2-direct/src/main/java/com/cybersource/samples/services/FlexApiService.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2021 by CyberSource + */ +package com.cybersource.samples.services; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import java.util.Map; + +@Path("/flex/v2") +@RegisterRestClient(baseUri = "https://apitest.cybersource.com") +public interface FlexApiService { + + @GET + @Path("/public-keys/{kid}") + @Produces(MediaType.APPLICATION_JSON) + Map publicKey(@PathParam("kid") String kid); + + @POST + @Path("/sessions") + @Consumes(MediaType.APPLICATION_JSON) + @Produces("application/jwt") + String createSession(String request); + + @POST + @Path("/tokens") + @Consumes("application/jwt") + @Produces("application/jwt") + String tokenize(String jwe); + +} diff --git a/flex-v2-direct/src/main/resources/META-INF/resources/index.html b/flex-v2-direct/src/main/resources/META-INF/resources/index.html new file mode 100644 index 0000000..7fd7ae7 --- /dev/null +++ b/flex-v2-direct/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,582 @@ + + + + + + + Flex Direct Demo + + + + + +
+
+

Express intent

+

+ Every Flex Direct API transaction starts with merchant sending Capture Context Request to Flex API "create session" endpoint. + This page allows merchant to express intent in regard to the scope of data captured. + Controls below drive generation of "Capture Context Request" JSON payload and visualize Flex Direct API capture capabilities. +

+
+ +
+ + +

Payment Credentials

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#FieldOptions
1paymentInformation.card.number +
+ + + + + + + + +
+
2paymentInformation.card.securityCode +
+ + + + + + + + +
+
3paymentInformation.card.expirationMonth +
+ + + + + + + + +
+
4paymentInformation.card.expirationYear +
+ + + + + + + + +
+
5paymentInformation.card.type +
+ + + + + + + + +
+
+ +

Amount and Currency

+ + + + + + + + + + + + + + + + + + + + +
#FieldOptions
1orderInformation.amountDetails.totalAmount +
+ + + + + + + + +
+
2orderInformation.amountDetails.currency +
+ + + + + + + + +
+
+ +

Billing Address

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#FieldOptions
1orderInformation.billTo.address1 +
+ + + + + + + + +
+
2orderInformation.billTo.address2 +
+ + + + + + + + +
+
3orderInformation.billTo.administrativeArea +
+ + + + + + + + +
+
4orderInformation.billTo.buildingNumber +
+ + + + + + + + +
+
5orderInformation.billTo.country +
+ + + + + + + + +
+
6orderInformation.billTo.district +
+ + + + + + + + +
+
7orderInformation.billTo.locality +
+ + + + + + + + +
+
8orderInformation.billTo.postalCode +
+ + + + + + + + +
+
9orderInformation.billTo.email +
+ + + + + + + + +
+
10orderInformation.billTo.firstName +
+ + + + + + + + +
+
11orderInformation.billTo.lastName +
+ + + + + + + + +
+
12orderInformation.billTo.phoneNumber +
+ + + + + + + + +
+
13orderInformation.billTo.company +
+ + + + + + + + +
+
+ +

Shipping Address

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#FieldOptions
1orderInformation.shipTo.address1 +
+ + + + + + + + +
+
2orderInformation.shipTo.address2 +
+ + + + + + + + +
+
3orderInformation.shipTo.administrativeArea +
+ + + + + + + + +
+
4orderInformation.shipTo.buildingNumber +
+ + + + + + + + +
+
5orderInformation.shipTo.country +
+ + + + + + + + +
+
6orderInformation.shipTo.district +
+ + + + + + + + +
+
7orderInformation.shipTo.locality +
+ + + + + + + + +
+
8orderInformation.shipTo.postalCode +
+ + + + + + + + +
+
9orderInformation.shipTo.firstName +
+ + + + + + + + +
+
10orderInformation.shipTo.lastName +
+ + + + + + + + +
+
11orderInformation.shipTo.company +
+ + + + + + + + +
+
+ + +
+
+ + + + \ No newline at end of file diff --git a/flex-v2-direct/src/main/resources/application.properties b/flex-v2-direct/src/main/resources/application.properties new file mode 100644 index 0000000..f8032b2 --- /dev/null +++ b/flex-v2-direct/src/main/resources/application.properties @@ -0,0 +1,5 @@ +# Set the port to the PORT environment variable +quarkus.http.port=${PORT:8080} + +# Set up your application to be packaged as an uber-jar +quarkus.package.type=uber-jar diff --git a/flex-v2-direct/src/main/resources/templates/capture-context.html b/flex-v2-direct/src/main/resources/templates/capture-context.html new file mode 100644 index 0000000..37251ef --- /dev/null +++ b/flex-v2-direct/src/main/resources/templates/capture-context.html @@ -0,0 +1,44 @@ + + + + + + + Unified Payments v. 2 + + + + + +
+
+

Launch Checkout

+

+ The Capture Context is encoded as JWT. You need to use it later to prepare encrypted Tokenization request. + JWT also contains all information to facilitate tokenization in further call to /flex/v2/tokens service. + There is one time use Public Encryption Key and list of required and optional fields, all cryptographically signed by VISA. +

+
+ +
+
+ + +
+ +
+ + +
+ + + Home +
+
+ + + + + \ No newline at end of file diff --git a/flex-v2-direct/src/main/resources/templates/capture-data.html b/flex-v2-direct/src/main/resources/templates/capture-data.html new file mode 100644 index 0000000..3c6c722 --- /dev/null +++ b/flex-v2-direct/src/main/resources/templates/capture-data.html @@ -0,0 +1,37 @@ + + + + + + + Unified Payments Demo no. 1 + + + + + +
+
+

Capture data

+

+ Payload to be encrypted was generated using Capture Context contained definition of payload structure. + With Flex Direct API you're in control what untrusted device can send to +

+
+ +
+
+ +
+

Please try 4444333322221111 as card number and 001 as card type.

+
+ + Home +
+
+ + + + \ No newline at end of file diff --git a/flex-v2-direct/src/main/resources/templates/create-capture-context-request.html b/flex-v2-direct/src/main/resources/templates/create-capture-context-request.html new file mode 100644 index 0000000..fd7f1e6 --- /dev/null +++ b/flex-v2-direct/src/main/resources/templates/create-capture-context-request.html @@ -0,0 +1,43 @@ + + + + + + + Unified Payments Demo no. 1 + + + + + +
+
+

Request Capture Context

+

+ Once you are ready to capture sensitive information, you need to request Capture Context from Flex API. + The JSON payload below was created based on your intent from previous step. + Once you send authenticated request to CyberSource Flex API /flex/v2/sessions service, + it will produce JWT that can later be used to create encrypted tokenization request. +

+
+ +
+
+ + +
+
+

Note that this is an authenticated call. This code demonstrates how to add relevant HTTP Signature header to make successful request to Flex API service.

+
+
+ + Home +
+
+
+ + + + \ No newline at end of file diff --git a/flex-v2-direct/src/main/resources/templates/jwe.html b/flex-v2-direct/src/main/resources/templates/jwe.html new file mode 100644 index 0000000..5764aa9 --- /dev/null +++ b/flex-v2-direct/src/main/resources/templates/jwe.html @@ -0,0 +1,34 @@ + + + + + + + Unified Payments Demo no. 1 + + + +
+
+

Encrypted Payload

+

+ This JSON Web Token (JWE - encrypted) needs to be send to /flex/v2/tokens. The payload below is AES256GCM + encrypted. The content key is encrypted using RSA OAEP. Initialization Vector and authentication tag are provided too. +

+
+ +
+

Encrypted Payload

+ + +
+

The encryption code sample can be found on our GitHub.

+
+ + Home +
+
+ + + + \ No newline at end of file diff --git a/flex-v2-direct/src/main/resources/templates/transient-token.html b/flex-v2-direct/src/main/resources/templates/transient-token.html new file mode 100644 index 0000000..09dec75 --- /dev/null +++ b/flex-v2-direct/src/main/resources/templates/transient-token.html @@ -0,0 +1,44 @@ + + + + + + + Unified Payments v. 2 + + + + + +
+
+

Data Captured

+

+ Cryptographically signed Transient Token is confirmation from VISA that your customer's Payment Credentials were securely captured by VISA. + They will never leave our secure enclave and will also expire in 15 minutes. + Now it is time to create Authorization request with Transient Token in lieu of sensitive PII and PAI data. +

+
+ +
+
+ + +
+
+ + +
+
+

Flow complete.

+
+ Home +
+
+ + + + + \ No newline at end of file diff --git a/jsp-flexjs/pom.xml b/jsp-flexjs/pom.xml index db3161d..e1af8e4 100644 --- a/jsp-flexjs/pom.xml +++ b/jsp-flexjs/pom.xml @@ -3,21 +3,20 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 + war jsp-flexjs 1.0 - war - - Flex JS Demo - A mocked merchant checkout and payment page using Flex JS tokenization - https://github.com/CyberSource/cybersource-flex-samples/java/jsp-flexjs - com.cybersource.examples.flex cybersource-flex-samples 1.0 + Flex JS Demo + A mocked merchant checkout and payment page using Flex JS tokenization + https://github.com/CyberSource/cybersource-flex-samples/java/jsp-flexjs + UTF-8 17 diff --git a/pom.xml b/pom.xml index ce77db3..abb2f1c 100644 --- a/pom.xml +++ b/pom.xml @@ -10,13 +10,10 @@ Flex Samples An out-of-the-box application to run Flex Microform or Flex API locally - - org.springframework.boot - spring-boot-starter-parent - 2.7.8 - + jsp-flexjs spring-microform + flex-v2-direct \ No newline at end of file From 32668d419c6233b5f93b28110b7d616122aba512 Mon Sep 17 00:00:00 2001 From: Bart Date: Wed, 26 Jul 2023 11:40:53 +0000 Subject: [PATCH 02/17] JSP is better on Java 8 --- jsp-flexjs/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jsp-flexjs/pom.xml b/jsp-flexjs/pom.xml index e1af8e4..9459d13 100644 --- a/jsp-flexjs/pom.xml +++ b/jsp-flexjs/pom.xml @@ -19,8 +19,8 @@ UTF-8 - 17 - 17 + 8 + 8 From a296bd0ca67c97e19ae6a07ff23957f34d6cf7de Mon Sep 17 00:00:00 2001 From: Bart Date: Wed, 26 Jul 2023 23:15:01 +0100 Subject: [PATCH 03/17] POM clean-up --- flex-v2-direct/pom.xml | 12 ++++++------ jsp-flexjs/pom.xml | 13 +++++-------- pom.xml | 10 ++++------ spring-microform/pom.xml | 13 +++++-------- 4 files changed, 20 insertions(+), 28 deletions(-) diff --git a/flex-v2-direct/pom.xml b/flex-v2-direct/pom.xml index 92832a9..604de87 100644 --- a/flex-v2-direct/pom.xml +++ b/flex-v2-direct/pom.xml @@ -1,10 +1,9 @@ - - + + 4.0.0 + flex-v2-direct - 1.0.3 + jar com.cybersource.examples.flex cybersource-flex-samples @@ -12,10 +11,11 @@ + UTF-8 11 11 + 1.13.3.Final - UTF-8 diff --git a/jsp-flexjs/pom.xml b/jsp-flexjs/pom.xml index 9459d13..c8cffb9 100644 --- a/jsp-flexjs/pom.xml +++ b/jsp-flexjs/pom.xml @@ -1,12 +1,9 @@ - - + 4.0.0 - war jsp-flexjs - 1.0 + war com.cybersource.examples.flex cybersource-flex-samples @@ -31,8 +28,8 @@ 3.2.0 - - + + javax.servlet @@ -46,7 +43,7 @@ 2.3.1 provided - + com.cybersource flex-server-sdk diff --git a/pom.xml b/pom.xml index abb2f1c..530fd80 100644 --- a/pom.xml +++ b/pom.xml @@ -1,15 +1,13 @@ - - + 4.0.0 + com.cybersource.examples.flex cybersource-flex-samples - 1.0 + 1.1.0 pom - Flex Samples - An out-of-the-box application to run Flex Microform or Flex API locally + Flex Java Samples jsp-flexjs diff --git a/spring-microform/pom.xml b/spring-microform/pom.xml index 75a0eb2..7777cdf 100644 --- a/spring-microform/pom.xml +++ b/spring-microform/pom.xml @@ -1,22 +1,19 @@ - - + 4.0.0 spring-microform - 1.0 jar - - Flex Microform Spring Demo - A mocked merchant checkout and payment pages using Flex API and Microform tokenization - https://github.com/CyberSource/cybersource-flex-samples/java/jsp-microform com.cybersource.examples.flex cybersource-flex-samples 1.0 + Flex Microform Spring Demo + A mocked merchant checkout and payment pages using Flex API and Microform tokenization + https://github.com/CyberSource/cybersource-flex-samples/java/jsp-microform + UTF-8 17 From bff150c10e6fda48738128ae70050820fd578c70 Mon Sep 17 00:00:00 2001 From: Bart Date: Wed, 26 Jul 2023 23:20:38 +0100 Subject: [PATCH 04/17] Adding Google Maven plugin --- spring-microform/pom.xml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/spring-microform/pom.xml b/spring-microform/pom.xml index 7777cdf..3c86563 100644 --- a/spring-microform/pom.xml +++ b/spring-microform/pom.xml @@ -19,7 +19,23 @@ 17 17 - + + + + + + com.google.cloud.tools + appengine-maven-plugin + 2.4.0 + + flex-v2-java-direct-use-sample + 3 + ${project.build.directory}/flex-direct-gae-1.0.3-runner.jar + + + + + org.springframework.boot From 1d3bdc7c83a5357600fc43bc4cb32495ed243012 Mon Sep 17 00:00:00 2001 From: Bart Date: Wed, 26 Jul 2023 23:26:10 +0100 Subject: [PATCH 05/17] Adding Google App Engine configuration file --- spring-microform/src/main/appengine/app.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 spring-microform/src/main/appengine/app.yaml diff --git a/spring-microform/src/main/appengine/app.yaml b/spring-microform/src/main/appengine/app.yaml new file mode 100644 index 0000000..de0bb9c --- /dev/null +++ b/spring-microform/src/main/appengine/app.yaml @@ -0,0 +1,5 @@ +runtime: java17 + +automatic_scaling: + min_instances: 0 + max_instances: 1 \ No newline at end of file From 542ab9fa8eb1166f09794c4a6451ded4b2e929dc Mon Sep 17 00:00:00 2001 From: Bart Date: Wed, 26 Jul 2023 23:28:29 +0100 Subject: [PATCH 06/17] Setting GAE project name --- spring-microform/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-microform/pom.xml b/spring-microform/pom.xml index 3c86563..c986fd7 100644 --- a/spring-microform/pom.xml +++ b/spring-microform/pom.xml @@ -28,7 +28,7 @@ appengine-maven-plugin 2.4.0 - flex-v2-java-direct-use-sample + flex-mf-springboot-sample 3 ${project.build.directory}/flex-direct-gae-1.0.3-runner.jar From 1a9ac47c19dca1d3443ac61bb3cc9f26a7bf9e1b Mon Sep 17 00:00:00 2001 From: Bart Date: Wed, 26 Jul 2023 23:39:38 +0100 Subject: [PATCH 07/17] rectyfying wrong artifact path --- spring-microform/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-microform/pom.xml b/spring-microform/pom.xml index c986fd7..67eb41c 100644 --- a/spring-microform/pom.xml +++ b/spring-microform/pom.xml @@ -26,11 +26,11 @@ com.google.cloud.tools appengine-maven-plugin - 2.4.0 + 2.4.4 flex-mf-springboot-sample 3 - ${project.build.directory}/flex-direct-gae-1.0.3-runner.jar + From 49393c6703928ef0d450197f709a53d93412aecc Mon Sep 17 00:00:00 2001 From: Bart Date: Thu, 27 Jul 2023 00:32:18 +0100 Subject: [PATCH 08/17] Cleaning POM with single and consistent Spring Boot version --- spring-microform/pom.xml | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/spring-microform/pom.xml b/spring-microform/pom.xml index 67eb41c..be4ffbf 100644 --- a/spring-microform/pom.xml +++ b/spring-microform/pom.xml @@ -22,6 +22,19 @@ + + + org.springframework.boot + spring-boot-maven-plugin + 2.7.13 + + + + repackage + + + + com.google.cloud.tools @@ -30,22 +43,32 @@ flex-mf-springboot-sample 3 - + + + + + org.springframework.boot + spring-boot-dependencies + 2.7.13 + pom + import + + + + org.springframework.boot spring-boot-starter-actuator - 3.0.4 org.springframework.boot spring-boot-starter-thymeleaf - 2.7.5 org.projectlombok @@ -56,7 +79,6 @@ org.springframework.boot spring-boot-starter-web - 2.7.8 org.yaml From 77262ed5f7f56823f8f3c44e5f2b1b890dd54b75 Mon Sep 17 00:00:00 2001 From: Bart Date: Thu, 27 Jul 2023 00:53:02 +0100 Subject: [PATCH 09/17] Adding public URL --- spring-microform/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spring-microform/README.md b/spring-microform/README.md index 3074075..d4b2835 100644 --- a/spring-microform/README.md +++ b/spring-microform/README.md @@ -1,5 +1,7 @@ # Flex Microform Sample +Live demo of this application is available here: https://flex-mf-springboot-sample.appspot.com/. + Flex Microform is a CyberSource-hosted HTML/JavaScript component that replaces the card number input field on your checkout page and calls the Flex API on your behalf. This simple example integration demonstrates using Flex Microform SDK to embed this PCI SAQ-A level component in your form. For more details on this see our Developer Guide at: From b0dc35d32a1809e135456e6202d9acdefde9c546 Mon Sep 17 00:00:00 2001 From: Bart Date: Thu, 27 Jul 2023 01:09:10 +0100 Subject: [PATCH 10/17] Allow application access resource in classpath when resource is not in filesystem, but only in FATJAR' --- .../com/cybersource/example/web/MicroformController.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/spring-microform/src/main/java/com/cybersource/example/web/MicroformController.java b/spring-microform/src/main/java/com/cybersource/example/web/MicroformController.java index 04bc490..589950d 100644 --- a/spring-microform/src/main/java/com/cybersource/example/web/MicroformController.java +++ b/spring-microform/src/main/java/com/cybersource/example/web/MicroformController.java @@ -50,10 +50,7 @@ public class MicroformController { @SneakyThrows public String index(final Model model) { // Just setting some variables to render the landing page, nothing Microform-specific here - try (final Stream lines = Files.lines(captureContextRequestJson.getFile().toPath())) { - final long lineCount = lines.count(); - model.addAttribute("requestLineCount", lineCount); - } + model.addAttribute("requestLineCount", new String(captureContextRequestJson.getInputStream().readAllBytes(), "UTF-8")); model.addAttribute("requestJson", IOUtils.toString(captureContextRequestJson.getInputStream(), UTF_8)); model.addAttribute("bootstrapVersion", BOOTSTRAP_VERSION); model.addAttribute("captureContextRequest", new GenerateCaptureContextRequest()); From c87930bcf35bfd8928d1ab0d5e37e781a5df06bf Mon Sep 17 00:00:00 2001 From: Bart Date: Thu, 27 Jul 2023 11:25:42 +0000 Subject: [PATCH 11/17] Update to target origin --- .../src/main/resources/capture-context-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-microform/src/main/resources/capture-context-request.json b/spring-microform/src/main/resources/capture-context-request.json index 24217f4..3ec9441 100644 --- a/spring-microform/src/main/resources/capture-context-request.json +++ b/spring-microform/src/main/resources/capture-context-request.json @@ -1,5 +1,5 @@ { - "targetOrigins": ["http://localhost:8080"], + "targetOrigins": ["https://flex-mf-springboot-sample.appspot.com"], "allowedCardNetworks": ["VISA", "MASTERCARD", "AMEX"], "clientVersion": "v2" -} \ No newline at end of file +} From e6e3f414df2437b81c67adbde18b3068da8773ea Mon Sep 17 00:00:00 2001 From: Bart Date: Thu, 27 Jul 2023 13:22:13 +0000 Subject: [PATCH 12/17] fixing TextArea hight --- .../src/main/resources/application.properties | 8 ++++---- spring-microform/src/main/resources/templates/index.html | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spring-microform/src/main/resources/application.properties b/spring-microform/src/main/resources/application.properties index 04513e1..3ff8acb 100644 --- a/spring-microform/src/main/resources/application.properties +++ b/spring-microform/src/main/resources/application.properties @@ -3,10 +3,10 @@ spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html # Recommend filling these properties out in application-local.properties, which is added to the .gitignore -#app.merchantID= -#app.merchantKeyId= -#app.merchantSecretKey= +app.merchantID=ps_hpa +app.merchantKeyId=b9b52f6f-8ccf-40c0-921f-2c933393ffe0 +app.merchantSecretKey=pfq05GP4o+rJKoJTY11b6VMSIASQOOHIbVZyGfBzsio= app.requestHost=apitest.cybersource.com app.userAgent=Mozilla/5.0 app.runEnvironment=apitest.cybersource.com -app.authenticationType=http_signature \ No newline at end of file +app.authenticationType=http_signature diff --git a/spring-microform/src/main/resources/templates/index.html b/spring-microform/src/main/resources/templates/index.html index 653322d..92acba6 100644 --- a/spring-microform/src/main/resources/templates/index.html +++ b/spring-microform/src/main/resources/templates/index.html @@ -29,10 +29,10 @@

Request Capture Context

-
- \ No newline at end of file + From db4b672f41a1bd5008c2129543d9d5d3547031b8 Mon Sep 17 00:00:00 2001 From: Bart Date: Thu, 27 Jul 2023 13:22:13 +0000 Subject: [PATCH 13/17] fixing TextArea hight --- spring-microform/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-microform/pom.xml b/spring-microform/pom.xml index be4ffbf..341fdfe 100644 --- a/spring-microform/pom.xml +++ b/spring-microform/pom.xml @@ -26,7 +26,7 @@ org.springframework.boot spring-boot-maven-plugin - 2.7.13 + 3.1.2 From e0627d72a27c361f34f6d59c5e3b182c4c95f614 Mon Sep 17 00:00:00 2001 From: Bart Date: Thu, 27 Jul 2023 13:22:13 +0000 Subject: [PATCH 14/17] fixing TextArea hight --- spring-microform/pom.xml | 2 +- spring-microform/src/main/resources/application.properties | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-microform/pom.xml b/spring-microform/pom.xml index be4ffbf..341fdfe 100644 --- a/spring-microform/pom.xml +++ b/spring-microform/pom.xml @@ -26,7 +26,7 @@ org.springframework.boot spring-boot-maven-plugin - 2.7.13 + 3.1.2 diff --git a/spring-microform/src/main/resources/application.properties b/spring-microform/src/main/resources/application.properties index 3ff8acb..3062d77 100644 --- a/spring-microform/src/main/resources/application.properties +++ b/spring-microform/src/main/resources/application.properties @@ -3,9 +3,9 @@ spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html # Recommend filling these properties out in application-local.properties, which is added to the .gitignore -app.merchantID=ps_hpa -app.merchantKeyId=b9b52f6f-8ccf-40c0-921f-2c933393ffe0 -app.merchantSecretKey=pfq05GP4o+rJKoJTY11b6VMSIASQOOHIbVZyGfBzsio= +app.merchantID= +app.merchantKeyId= +app.merchantSecretKey= app.requestHost=apitest.cybersource.com app.userAgent=Mozilla/5.0 app.runEnvironment=apitest.cybersource.com From d89b8a5fa690bdaae18e4d5991761e074e7f14e5 Mon Sep 17 00:00:00 2001 From: Bart Date: Tue, 8 Aug 2023 12:41:18 +0000 Subject: [PATCH 15/17] Spring Boot 3 upgrade --- spring-microform/pom.xml | 2 +- .../src/main/resources/application.properties | 8 ++++---- .../src/main/resources/capture-context-request.json | 4 ++-- spring-microform/src/main/resources/templates/index.html | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/spring-microform/pom.xml b/spring-microform/pom.xml index be4ffbf..341fdfe 100644 --- a/spring-microform/pom.xml +++ b/spring-microform/pom.xml @@ -26,7 +26,7 @@ org.springframework.boot spring-boot-maven-plugin - 2.7.13 + 3.1.2 diff --git a/spring-microform/src/main/resources/application.properties b/spring-microform/src/main/resources/application.properties index 04513e1..3062d77 100644 --- a/spring-microform/src/main/resources/application.properties +++ b/spring-microform/src/main/resources/application.properties @@ -3,10 +3,10 @@ spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html # Recommend filling these properties out in application-local.properties, which is added to the .gitignore -#app.merchantID= -#app.merchantKeyId= -#app.merchantSecretKey= +app.merchantID= +app.merchantKeyId= +app.merchantSecretKey= app.requestHost=apitest.cybersource.com app.userAgent=Mozilla/5.0 app.runEnvironment=apitest.cybersource.com -app.authenticationType=http_signature \ No newline at end of file +app.authenticationType=http_signature diff --git a/spring-microform/src/main/resources/capture-context-request.json b/spring-microform/src/main/resources/capture-context-request.json index 24217f4..3ec9441 100644 --- a/spring-microform/src/main/resources/capture-context-request.json +++ b/spring-microform/src/main/resources/capture-context-request.json @@ -1,5 +1,5 @@ { - "targetOrigins": ["http://localhost:8080"], + "targetOrigins": ["https://flex-mf-springboot-sample.appspot.com"], "allowedCardNetworks": ["VISA", "MASTERCARD", "AMEX"], "clientVersion": "v2" -} \ No newline at end of file +} diff --git a/spring-microform/src/main/resources/templates/index.html b/spring-microform/src/main/resources/templates/index.html index 653322d..92acba6 100644 --- a/spring-microform/src/main/resources/templates/index.html +++ b/spring-microform/src/main/resources/templates/index.html @@ -29,10 +29,10 @@

Request Capture Context

-
- \ No newline at end of file + From 9e9fe195b9f1222be911d5ef7e96a61532be69ba Mon Sep 17 00:00:00 2001 From: Bart Date: Tue, 8 Aug 2023 14:41:14 +0000 Subject: [PATCH 16/17] Spring Boot 3 upgrade --- spring-microform/README.md | 2 ++ spring-microform/pom.xml | 30 ++++++++++++++++--- .../example/web/MicroformController.java | 5 +--- .../src/main/resources/application.properties | 8 ++--- .../resources/capture-context-request.json | 4 +-- .../src/main/resources/templates/index.html | 4 +-- 6 files changed, 37 insertions(+), 16 deletions(-) diff --git a/spring-microform/README.md b/spring-microform/README.md index 3074075..d4b2835 100644 --- a/spring-microform/README.md +++ b/spring-microform/README.md @@ -1,5 +1,7 @@ # Flex Microform Sample +Live demo of this application is available here: https://flex-mf-springboot-sample.appspot.com/. + Flex Microform is a CyberSource-hosted HTML/JavaScript component that replaces the card number input field on your checkout page and calls the Flex API on your behalf. This simple example integration demonstrates using Flex Microform SDK to embed this PCI SAQ-A level component in your form. For more details on this see our Developer Guide at: diff --git a/spring-microform/pom.xml b/spring-microform/pom.xml index 67eb41c..341fdfe 100644 --- a/spring-microform/pom.xml +++ b/spring-microform/pom.xml @@ -22,6 +22,19 @@ + + + org.springframework.boot + spring-boot-maven-plugin + 3.1.2 + + + + repackage + + + + com.google.cloud.tools @@ -30,22 +43,32 @@ flex-mf-springboot-sample 3 - + + + + + org.springframework.boot + spring-boot-dependencies + 2.7.13 + pom + import + + + + org.springframework.boot spring-boot-starter-actuator - 3.0.4 org.springframework.boot spring-boot-starter-thymeleaf - 2.7.5 org.projectlombok @@ -56,7 +79,6 @@ org.springframework.boot spring-boot-starter-web - 2.7.8 org.yaml diff --git a/spring-microform/src/main/java/com/cybersource/example/web/MicroformController.java b/spring-microform/src/main/java/com/cybersource/example/web/MicroformController.java index 04bc490..589950d 100644 --- a/spring-microform/src/main/java/com/cybersource/example/web/MicroformController.java +++ b/spring-microform/src/main/java/com/cybersource/example/web/MicroformController.java @@ -50,10 +50,7 @@ public class MicroformController { @SneakyThrows public String index(final Model model) { // Just setting some variables to render the landing page, nothing Microform-specific here - try (final Stream lines = Files.lines(captureContextRequestJson.getFile().toPath())) { - final long lineCount = lines.count(); - model.addAttribute("requestLineCount", lineCount); - } + model.addAttribute("requestLineCount", new String(captureContextRequestJson.getInputStream().readAllBytes(), "UTF-8")); model.addAttribute("requestJson", IOUtils.toString(captureContextRequestJson.getInputStream(), UTF_8)); model.addAttribute("bootstrapVersion", BOOTSTRAP_VERSION); model.addAttribute("captureContextRequest", new GenerateCaptureContextRequest()); diff --git a/spring-microform/src/main/resources/application.properties b/spring-microform/src/main/resources/application.properties index 04513e1..3062d77 100644 --- a/spring-microform/src/main/resources/application.properties +++ b/spring-microform/src/main/resources/application.properties @@ -3,10 +3,10 @@ spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html # Recommend filling these properties out in application-local.properties, which is added to the .gitignore -#app.merchantID= -#app.merchantKeyId= -#app.merchantSecretKey= +app.merchantID= +app.merchantKeyId= +app.merchantSecretKey= app.requestHost=apitest.cybersource.com app.userAgent=Mozilla/5.0 app.runEnvironment=apitest.cybersource.com -app.authenticationType=http_signature \ No newline at end of file +app.authenticationType=http_signature diff --git a/spring-microform/src/main/resources/capture-context-request.json b/spring-microform/src/main/resources/capture-context-request.json index 24217f4..3ec9441 100644 --- a/spring-microform/src/main/resources/capture-context-request.json +++ b/spring-microform/src/main/resources/capture-context-request.json @@ -1,5 +1,5 @@ { - "targetOrigins": ["http://localhost:8080"], + "targetOrigins": ["https://flex-mf-springboot-sample.appspot.com"], "allowedCardNetworks": ["VISA", "MASTERCARD", "AMEX"], "clientVersion": "v2" -} \ No newline at end of file +} diff --git a/spring-microform/src/main/resources/templates/index.html b/spring-microform/src/main/resources/templates/index.html index 653322d..92acba6 100644 --- a/spring-microform/src/main/resources/templates/index.html +++ b/spring-microform/src/main/resources/templates/index.html @@ -29,10 +29,10 @@

Request Capture Context

-
- \ No newline at end of file + From 74b3e187e2afe7d154998a658539380a40efa784 Mon Sep 17 00:00:00 2001 From: Bart Date: Thu, 10 Aug 2023 11:55:49 +0100 Subject: [PATCH 17/17] Updating readme --- spring-microform/README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/spring-microform/README.md b/spring-microform/README.md index d4b2835..9eefe07 100644 --- a/spring-microform/README.md +++ b/spring-microform/README.md @@ -1,27 +1,29 @@ # Flex Microform Sample -Live demo of this application is available here: https://flex-mf-springboot-sample.appspot.com/. +A live demo of this application is available here: https://flex-mf-springboot-sample.appspot.com/. -Flex Microform is a CyberSource-hosted HTML/JavaScript component that replaces the card number input field on your checkout page +Flex Microform is a CyberSource-hosted HTML/JavaScript component that replaces the card number input field on your checkout page and calls the Flex API on your behalf. This simple example integration demonstrates using Flex Microform SDK to embed this PCI SAQ-A level component in your form. For more details on this see our Developer Guide at: https://developer.cybersource.com/api/developer-guides/dita-flex/SAFlexibleToken/FlexMicroform.html ## Prerequisites -- [Java 14](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) +- [Java 14](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) (the application uses records but this could be refactored into standard POJOs easily) - [Maven](https://maven.apache.org/install.html) ## Setup Instructions 1. Modify `./src/main/resources/application-local.properties` with CyberSource REST credentials created through the - [EBC Portal](https://ebc2test.cybersource.com/). Learn more about how to get an account [here](https://developer.cybersource.com/hello-world.html). + [EBC Portal](https://ebc2test.cybersource.com/). Learn more about how to get an account [here](https://developer.cybersource.com/hello-world.html). ```properties merchantId=YOUR MERCHANT ID keyId=YOUR KEY ID (SHARED SECRET SERIAL NUMBER) sharedSecret=YOUR SHARED SECRET ``` -2. Build and run the application using Maven. Spring will automatically deploy a local Tomcat server with port 8080 exposed. +2. Replace the `targetOrigins` value in `./src/main/resources/capture-context-request.json` with the domain where Microform +will be served (e.g.`http://localhost8080` if deploying locally). +3. To deploy locally, simply build and run the application using Maven. Spring will automatically deploy a local Tomcat server with port 8080 exposed. ```bash mvn spring-boot:run ``` @@ -36,7 +38,7 @@ https://developer.cybersource.com/api/developer-guides/dita-flex/SAFlexibleToken Navigate to http://localhost:8080 and proceed through the various screens to understand how things work under the hood. - + ## Tips - If you are having issues, check out the full [Flex Microform documentation](https://developer.cybersource.com/api/developer-guides/dita-flex/SAFlexibleToken/FlexMicroform.html). -- Safari version 10 and below does not support `RsaOaep256` encryption schema, for those browser please specify encryption type `RsaOaep` when making a call to the `/keys` endpoint. For a detailed example please see [JwtProcessorService.java](./src/main/java/com/cybersource/example/service/JwtProcessorService.java), line 47. +- Safari version 10 and below does not support `RsaOaep256` encryption schema, for those browser please specify encryption type `RsaOaep` when making a call to the `/keys` endpoint. For a detailed example please see [JwtProcessorService.java](./src/main/java/com/cybersource/example/service/JwtProcessorService.java), line 47. \ No newline at end of file