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..604de87 --- /dev/null +++ b/flex-v2-direct/pom.xml @@ -0,0 +1,115 @@ + + + 4.0.0 + + flex-v2-direct + jar + + com.cybersource.examples.flex + cybersource-flex-samples + 1.0 + + + + UTF-8 + 11 + 11 + + 1.13.3.Final + + + + + + 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..c8cffb9 100644 --- a/jsp-flexjs/pom.xml +++ b/jsp-flexjs/pom.xml @@ -1,27 +1,23 @@ - - + 4.0.0 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 - 17 + 8 + 8 @@ -32,8 +28,8 @@ 3.2.0 - - + + javax.servlet @@ -47,7 +43,7 @@ 2.3.1 provided - + com.cybersource flex-server-sdk diff --git a/pom.xml b/pom.xml index ce77db3..530fd80 100644 --- a/pom.xml +++ b/pom.xml @@ -1,22 +1,17 @@ - - + 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 - - org.springframework.boot - spring-boot-starter-parent - 2.7.8 - + Flex Java Samples + jsp-flexjs spring-microform + flex-v2-direct \ No newline at end of file diff --git a/spring-microform/README.md b/spring-microform/README.md index 3074075..9eefe07 100644 --- a/spring-microform/README.md +++ b/spring-microform/README.md @@ -1,25 +1,29 @@ # Flex Microform Sample -Flex Microform is a CyberSource-hosted HTML/JavaScript component that replaces the card number input field on your checkout page +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 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 ``` @@ -34,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 diff --git a/spring-microform/pom.xml b/spring-microform/pom.xml index 75a0eb2..341fdfe 100644 --- a/spring-microform/pom.xml +++ b/spring-microform/pom.xml @@ -1,38 +1,74 @@ - - + 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 17 - + + + + + + org.springframework.boot + spring-boot-maven-plugin + 3.1.2 + + + + repackage + + + + + + + com.google.cloud.tools + appengine-maven-plugin + 2.4.4 + + 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 @@ -43,7 +79,6 @@ org.springframework.boot spring-boot-starter-web - 2.7.8 org.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 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 +