-
Notifications
You must be signed in to change notification settings - Fork 39
Update flex examples #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
28abaed
32668d4
a296bd0
bff150c
1d3bdc7
542ab9f
1a9ac47
49393c6
77262ed
b0dc35d
c87930b
e6e3f41
db4b672
e0627d7
43ee852
d89b8a5
f82848a
9e9fe19
f3374e0
74b3e18
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could remove the word "Few" and just have "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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be h2? |
||
|
|
||
| ``` | ||
| mvn clean package appengine:deploy | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
| <modelVersion>4.0.0</modelVersion> | ||
|
|
||
| <artifactId>flex-v2-direct</artifactId> | ||
| <packaging>jar</packaging> | ||
| <parent> | ||
| <groupId>com.cybersource.examples.flex</groupId> | ||
| <artifactId>cybersource-flex-samples</artifactId> | ||
| <version>1.0</version> | ||
| </parent> | ||
|
|
||
| <properties> | ||
| <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | ||
| <maven.compiler.target>11</maven.compiler.target> | ||
| <maven.compiler.source>11</maven.compiler.source> | ||
|
|
||
| <quarkus.version>1.13.3.Final</quarkus.version> | ||
| </properties> | ||
|
|
||
| <dependencyManagement> | ||
| <dependencies> | ||
| <dependency> | ||
| <groupId>io.quarkus</groupId> | ||
| <artifactId>quarkus-bom</artifactId> | ||
| <version>${quarkus.version}</version> | ||
| <type>pom</type> | ||
| <scope>import</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>com.google.cloud</groupId> | ||
| <artifactId>libraries-bom</artifactId> | ||
| <version>20.3.0</version> | ||
| <type>pom</type> | ||
| <scope>import</scope> | ||
| </dependency> | ||
| </dependencies> | ||
| </dependencyManagement> | ||
|
|
||
| <dependencies> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just a note that not all of these are available on the CYBS maven mirror, unless you have a different one?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll answer this internally. |
||
| <dependency> | ||
| <groupId>io.quarkus</groupId> | ||
| <artifactId>quarkus-resteasy</artifactId> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>io.quarkus</groupId> | ||
| <artifactId>quarkus-resteasy-qute</artifactId> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>io.quarkus</groupId> | ||
| <artifactId>quarkus-resteasy-jackson</artifactId> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>io.quarkus</groupId> | ||
| <artifactId>quarkus-rest-client</artifactId> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>io.quarkus</groupId> | ||
| <artifactId>quarkus-smallrye-jwt</artifactId> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>io.quarkus</groupId> | ||
| <artifactId>quarkus-junit5</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>io.rest-assured</groupId> | ||
| <artifactId>rest-assured</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
|
|
||
| <dependency> | ||
| <groupId>com.google.cloud</groupId> | ||
| <artifactId>google-cloud-storage</artifactId> | ||
| </dependency> | ||
| </dependencies> | ||
|
|
||
| <build> | ||
| <plugins> | ||
| <plugin> | ||
| <groupId>io.quarkus</groupId> | ||
| <artifactId>quarkus-maven-plugin</artifactId> | ||
| <version>${quarkus.version}</version> | ||
| <executions> | ||
| <execution> | ||
| <goals> | ||
| <goal>build</goal> | ||
| </goals> | ||
| </execution> | ||
| </executions> | ||
| </plugin> | ||
| <plugin> | ||
| <groupId>org.apache.maven.plugins</groupId> | ||
| <artifactId>maven-surefire-plugin</artifactId> | ||
| <version>2.22.2</version> | ||
| <configuration> | ||
| <systemProperties> | ||
| <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager> | ||
| </systemProperties> | ||
| </configuration> | ||
| </plugin> | ||
| <!-- Plugin needed to use `mvn clean package appengine:deploy` command --> | ||
| <plugin> | ||
| <groupId>com.google.cloud.tools</groupId> | ||
| <artifactId>appengine-maven-plugin</artifactId> | ||
| <version>2.4.0</version> | ||
| <configuration> | ||
| <projectId>flex-v2-java-direct-use-sample</projectId> | ||
| <version>3</version> | ||
| <artifact>${project.build.directory}/flex-direct-gae-1.0.3-runner.jar</artifact> | ||
| </configuration> | ||
| </plugin> | ||
| </plugins> | ||
| </build> | ||
| </project> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| runtime: java11 | ||
|
|
||
| automatic_scaling: | ||
| min_instances: 0 | ||
| max_instances: 1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we give this a more readable name since we're just trying to isolate the payload? Still gross but might be slightly cleaner to do something like final String[] jwtChunks = jwt.split("\\.");
return new JsonObject(Base64.getDecoder().decode(jwtChunks[1]));? |
||
| jwt = jwt.substring(0, jwt.indexOf('.')); | ||
| jwt = new String(Base64.getDecoder().decode(jwt)); | ||
| return new JsonObject(jwt); | ||
| } | ||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"reducing PCI scope." may be more accurate?