Skip to content
Merged
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
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,15 @@ and on our [website](https://51degrees.com/documentation/_examples__device_detec
| pipeline.developer-examples.onpremise-engine | Shows how to modify SimpleFlowElement to make use of the 'engine' functionality and use a custom data file to map dates to star signs rather than relying on hard coded data. |
| pipeline.developer-examples.clientside-element | Shows how to modify SimpleFlowElement to request the data of birth from the user using client-side JavaScript. |
| pipeline.developer-examples.clientside-element-mvc | An example project showing how to use the code from SimpleClientSideElement in a Java web application using the Model-View-Controller Pattern. |
| pipeline.developer-examples.cloud-engine | Shows how to modify SimpleFlowElement to perform the star sign lookup via a cloud service rather than locally. |
| pipeline.developer-examples.cloud-engine | Shows how to modify SimpleFlowElement to perform the star sign lookup via a cloud service rather than locally. The endpoint and resource key come from `FOD_CLOUD_API_URL` and `_51DEGREES_RESOURCE_KEY` when set, otherwise the star sign service the example was written for. |
| pipeline.developer-examples.usage-sharing | Shows how to share usage with 51Degrees. This helps us to keep our products up to date and accurate. |
| pipeline.developer-examples.fodid | Shows how to read a 51Did, and, in a web demo, how a browser creates and verifies one against the 51Degrees cloud and the server redeems the encrypted result, so that the creator context proves the identifier is being presented by the browser it was created on. The module README describes the flow, the environment variables, what a run costs and the redeem call to copy into your own server. |

### Pointing the cloud examples at another host

Every example that calls the 51Degrees cloud takes its endpoint from the `FOD_CLOUD_API_URL` environment variable, which is the cloud API base including the `/api/v4/` segment and defaults to `https://cloud.51degrees.com/api/v4/` (the cloud-engine example defaults to the star sign service it was written for, as its row above says). This is the same variable the cloud request engine in this repository honours, so setting it once points every example, and any pipeline built with the engine, at the same place. The resource key comes from `_51DEGREES_RESOURCE_KEY`, or the older `RESOURCE_KEY`.

A host other than cloud.51degrees.com would be used to (a) use an on premise web server, or (b) use a privately hosted version of the 51Degrees cloud for performance reasons. This is the private hosting option of the cloud service. Both run the same service, so the examples work unchanged.



Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,66 @@ public class Main {
private static final ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory();
private static final HttpClient httpClient = new HttpClientDefault();

/**
* The star sign service this example was written for, and its
* resource key, used when nothing else is set.
*/
private static final String STAR_SIGN_ENDPOINT =
"https://51degrees.com/starsign/api/";
private static final String STAR_SIGN_RESOURCE_KEY = "cloudexample";

/**
* The cloud endpoint, taken from FOD_CLOUD_API_URL when that is set,
* which is the variable every 51Degrees cloud example honours and the
* one the cloud request engine builder reads by itself when no
* endpoint is given. A host other than cloud.51degrees.com would be
* used to (a) use an on premise web server, or (b) use a privately
* hosted version of the 51Degrees cloud for performance reasons,
* which is the private hosting option of the cloud service. Both run
* the same service, so the example works unchanged. Normalised to
* end in one slash so the builder appends its three paths to it
* directly.
*/
static String endpoint() {
String value = env("FOD_CLOUD_API_URL");
if (value == null) {
return STAR_SIGN_ENDPOINT;
}
while (value.endsWith("/")) {
value = value.substring(0, value.length() - 1);
}
return value + "/";
}

/**
* The resource key from _51DEGREES_RESOURCE_KEY, or the older
* RESOURCE_KEY, so the example can be pointed at a host whose keys
* are its own. Otherwise the key of the star sign service.
*/
static String resourceKey() {
String value = env("_51DEGREES_RESOURCE_KEY");
if (value == null) {
value = env("RESOURCE_KEY");
}
return value == null ? STAR_SIGN_RESOURCE_KEY : value;
}

static String env(String name) {
String value = System.getenv(name);
return value == null || value.trim().isEmpty() ? null : value;
}

public static class Example {
public void run() throws Exception {
//! [usage]
// The endpoint comes from FOD_CLOUD_API_URL when it is set, so
// the example can be pointed at an on premise web server or a
// privately hosted version of the 51Degrees cloud, and
// otherwise at the star sign service it was written for.
CloudRequestEngine cloudRequestEngine =
new CloudRequestEngineBuilder(loggerFactory, httpClient)
.setEndpoint("http://51degrees.com/starsign/api/")
.setResourceKey("cloudexample")
.setEndpoint(endpoint())
.setResourceKey(resourceKey())
.build();

SimpleCloudEngine ageElement =
Expand All @@ -64,10 +117,22 @@ public void run() throws Exception {
.addEvidence("cookie.date-of-birth", dob)
.process();

System.out.println("With a date of birth of " +
dob +
", your star sign is " +
flowData.getFromElement(ageElement).getStarSign() + ".");
String starSign =
flowData.getFromElement(ageElement).getStarSign();
if (starSign == null) {
// The service answered but offers no star sign
// product, which is what the 51Degrees cloud says,
// because star signs are only served by the example
// service. The connection, the property and evidence
// key negotiation and the request itself all worked.
System.out.println("The cloud service at " + endpoint()
+ " does not offer the star sign product, so no "
+ "star sign is available for a date of birth of "
+ dob + ".");
} else {
System.out.println("With a date of birth of " + dob
+ ", your star sign is " + starSign + ".");
}
}
//! [usage]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,19 @@ protected void processEngine(FlowData data, StarSignData aspectData) {
CloudRequestData requestData = data.getFromElement(cloudRequestEngine);
String json = requestData.getJsonResponse();

// Extract data from json to the aspectData instance.
// Extract data from json to the aspectData instance. A cloud
// service answers only for the products the resource key covers,
// so a response without the star sign product leaves the value
// unset rather than failing the request, in the same way the
// 51Degrees cloud engines report a property the key does not
// cover as not available.
JSONObject jsonObj = new JSONObject(json);
JSONObject deviceObj = jsonObj.getJSONObject("starsign");

starSignData.setStarSign(deviceObj.getString("starsign"));
if (jsonObj.has("starsign")) {
JSONObject starSignObj = jsonObj.getJSONObject("starsign");
starSignData.setStarSign(starSignObj.optString("starsign", null));
} else {
starSignData.setStarSign(null);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# 51Did examples

This module holds the developer examples for the 51Did package
(`pipeline.did`). There are two programs, both in the package
`pipeline.developerexamples.fodid`.

| Program | What it shows |
| --- | --- |
| `Main` | Reads a 51Did offline. Builds a sample identifier in process, parses it back with `FodId` and shows that the value is stable while the envelope changes on every issue. Needs no cloud access. |
| `CreatorContextDemoServer` | Serves a small web page that creates a 51Did in the browser, verifies it from the browser, and redeems the encrypted creator context result on this server with `DidClient`, which is the only place the licence key lives. |

## Creator context

Every 51Did the 51Degrees cloud issues carries a creator context, which
binds the identifier to the browser and connection it was created on.
The creator context only makes sense from a browser, because a program
verifying its own connection checks itself against itself, so the demo
is a web page and a server. The flow against the cloud has three steps:

1. **Create** a 51Did by calling the `json` endpoint, which issues an
identifier for the calling connection.
2. **Verify** it with `verify-full`, which returns both the signature
outcome and the creator context verdict only as an encrypted
`result` that the caller cannot read or forge. (A deployment
holding no context secret answers in the open instead.)
3. **Redeem** the encrypted result with `redeem`, presenting the 51Did,
the encrypted result and the account's licence key, and receive the
true creator context verdict, when the verification happened
(`verifiedAt`) and how long ago that was (`secondsSinceVerified`).

Steps 1 and 2 run in the visitor's browser, so the cloud observes the
browser's live connection, and the page relays the encrypted result to
your server. Step 3 runs on your server, which is the party holding
the licence key. A single-use `challenge` issued by the server per page
load is bound through both steps by the cloud.

The demo server uses the web server that ships with the JDK, the
`pipeline.did` package from this repository for the 51Did handling, and
`org.json` to build the JSON it answers the page with.

### What you copy into your own server

The only server-side part of the flow is the redeem call, which adds
the licence key the browser never sees. The `redeem` method in
`CreatorContextDemoServer.java` is that call, and these are its
essential lines. `DidClient` is built once at start-up from the
resource key and the licence key (it reads `FOD_CLOUD_API_URL` itself
and falls back to the public cloud), and `did`, `result` and
`challenge` are what the page passed on from `verify-full`:

```java
DidClient client = new DidClient(resourceKey, licenceKey);
```

```java
// The 51Did arrives from the page in the URL-safe base64 alphabet.
FodId fodId = FodId.fromBase64(did);

// Offline, against the cloud's published key for the identifier's date.
// No use is charged, because the client holds the keys.
boolean genuine = client.verifySignature(fodId);

// Server side, with the licence key. One use.
RedeemResult redeemed = client.redeem(fodId, result, challenge);
RedeemResult.Context context = redeemed.getContext();
```

The handler answers the page in the cloud's own shape, `signature`,
`context`, `factors` when present, `verifiedAt` and
`secondsSinceVerified`, built from the typed result, with one field
added, `serverSignature`, being the offline check. A malformed 51Did
answers 400, a host without the creator context answers 404 with a
text body, and an unreachable cloud answers 502 with `{ "error": ... }`.
A production server would also remember the challenge it issued and
reject a redemption carrying any other, which the demo keeps out of
scope.

### Environment variables

| Variable | Meaning |
| --- | --- |
| `_51DEGREES_RESOURCE_KEY` | Required. The resource key of the page, public by nature. The older name `RESOURCE_KEY` is read when this one is not set. |
| `_51DEGREES_LICENSE_KEY` | Optional. A licence key of the same account, server side only. The older name `LICENSE_KEY` is read when this one is not set. Only an account that holds licence keys needs one to redeem, because the licence key is what keeps redemption to the acting party's own servers, so an account holding none redeems without one. |
| `FOD_CLOUD_API_URL` | Optional. The cloud API base including the `/api/v4/` segment, defaulting to `https://cloud.51degrees.com/api/v4/`. This is the same variable the cloud request engine in this repository honours, so setting it once points every 51Degrees example at the same place. A trailing slash is added where missing. A host other than cloud.51degrees.com would be used to (a) use an on premise web server, or (b) use a privately hosted version of the 51Degrees cloud for performance reasons, which is the private hosting option of the cloud service. Both run the same service, so the examples work unchanged. |
| `PORT` | The port to listen on, defaulting to `5100`. |

### How to run

With Maven, from the root of the repository, build the module and run
the demo server by its class name:

```sh
mvn -pl pipeline.developer-examples/pipeline.developer-examples.fodid -am -DskipTests compile
mvn -pl pipeline.developer-examples/pipeline.developer-examples.fodid exec:java -Dexec.mainClass=pipeline.developerexamples.fodid.CreatorContextDemoServer
```

The module depends on `pipeline.did` from this repository, so the
first command builds that package too.

The demo server prints the address to open, `http://localhost:5100/`
by default. A creator context verdict of `nocontext` is a normal
outcome rather than an error, because a self-hosted container may be
configured not to emit the creator context, so an identifier it issued
has none to check, and the page shows it as the verdict. A 404 from
`verify-full` or `redeem` means the host answering does not support
the creator context at all, and the page reports the check as not
supported by this host. Any other status outside 2xx, or a body that
is not JSON, is shown on the page as a failure naming the status and
what the service said.

### What a run costs

Every call the demo makes to the cloud is one use against the
subscription behind the resource key. Checking a 51Did from the
browser makes two, verify-full from the page and redeem from the
server, so a browser-based context check is two uses every time.
Checking only the signature with `verify` is one use. The server's
offline signature check costs nothing beyond the one fetch of the
public keys the client makes on first use.

### The web demo, and the copy-and-paste proof

The demo server serves `page.html`, injecting a fresh challenge per
page load, and holds the licence key. The browser creates the 51Did
and calls `verify-full`, the first verification step, so the cloud
observes the browser's live connection, then the page hands the
encrypted result to its own server, which redeems it with the licence
key as the second step.

The creation call requests every 51Did identifier in one request, and
the page shows all six in a table: the probabilistic pair
(`IdProbGlobal` and `IdProbLic`) derived from the connection, the
deterministic hashed-email pair (`IdHemGlobal` and `IdHemLic`) derived
from email evidence supplied as `id.email` (the demo sends
`demo@51did.example`, so the pair is the same on every device that
email appears on), and the random pair (`IdRandGlobal` and
`IdRandLic`). Global identifiers are shared across customers, licensed
ones are scoped to the licence key. The verification and creator
context flow then carries the licensed probabilistic identifier through
both steps, or the global one where the account holds no licence keys.

Once the 51Did has fully validated, the page shows a **copy-and-paste
section** with a link carrying the same 51Did, and an explanation of
what will happen next. Open that link in a **different browser** and
the same page loads with the same identifier: the signature still
verifies and the identifier unpacks, because it is genuine, but the
creator context does **not** validate, because the context binds the
identifier to the browser and connection it was created on. That
visible failure is the demonstration that matters, a copied or stolen
identifier caught at presentation with nothing stored server side.
Opening the link in the same browser is not the demonstration, since
the same browser presents the same context and may still verify.

To demonstrate across two devices, serve on an address both can reach
and open the copied link on the second device.

### The stylesheet

The vendored `examples-main.min.css` beside `page.html` under
`src/main/resources/fodid/creator-context/` is the design system build
and is refreshed by the `update-example-assets` step of common-ci.
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@
<version>${project.version}</version>
<artifactId>pipeline.did</artifactId>
</dependency>
<!-- Used directly by the demo server to build the JSON it answers
the page with. Managed by the root pom at the version the
pipeline uses. -->
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
Expand Down
Loading
Loading