Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions flex-v2-direct/README.md
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.

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?


----

## 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

Choose a reason for hiding this comment

The 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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be h2?


```
mvn clean package appengine:deploy
```
115 changes: 115 additions & 0 deletions flex-v2-direct/pom.xml
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>

Choose a reason for hiding this comment

The 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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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>
5 changes: 5 additions & 0 deletions flex-v2-direct/src/main/appengine/app.yaml
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);

Choose a reason for hiding this comment

The 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);
}

}
Loading