Skip to content

Commit

Permalink
Add Java examples and refactored Node.js examples
Browse files Browse the repository at this point in the history
  • Loading branch information
satazor committed Jun 11, 2024
1 parent 8343ce6 commit 4d2dccf
Show file tree
Hide file tree
Showing 3 changed files with 263 additions and 72 deletions.
140 changes: 118 additions & 22 deletions docs/webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,36 +34,132 @@ Full information about the available events and their associated payloads can be

## Verifying a request

To help you verify that a request has come from Topper, we include a `X-Topper-JWS-Signature` header with each request. This header is a [JSON Web Signature](https://datatracker.ietf.org/doc/html/rfc7515) with [detached content](https://datatracker.ietf.org/doc/html/rfc7515#appendix-F), meaning the payload portion of the token has been removed. This token can be verified using the public key provided when creating the webhook.
To help you verify that a request has come from Topper, we include a `X-Topper-JWS-Signature` header with each request. This header is a [JSON Web Signature](https://datatracker.ietf.org/doc/html/rfc7515) (JWS) with [detached content](https://datatracker.ietf.org/doc/html/rfc7515#appendix-F), meaning the payload portion of the token has been removed. This token can be verified using the public key provided when creating the webhook.

<Tabs>
<TabItem label="Node.js" value="nodejs" default>

Here is a Node.js snippet to verify a signature using the [`jsonwebtoken`](https://github.com/auth0/node-jsonwebtoken) package:
Here is a Node.js snippet to verify a signature using the [`jsonwebtoken`](https://github.com/auth0/node-jsonwebtoken) package. Please note that the code below is for illustrative purposes only, and the integration should be adapted to your application.

```js
const { createPublicKey } = require('crypto');
const { promisify } = require('util');
const jsonwebtoken = require('jsonwebtoken');

// Verify a webhook request.
async function verifyWebhookRequest(request) {
// Load public key in JWK format from an environment variable.
const publicKeyJwk = JSON.parse(process.env.TOPPER_WEBHOOK_PUBLIC_KEY);

// Promisify the `jsonwebtoken.verify()` method for simplicity.
const verify = promisify(jsonwebtoken.verify);

// Parse the JWK formatted key.
const publicKey = createPublicKey({ format: 'jwk', key: publicKeyJwk });
import { createPublicKey } from 'node:crypto';
import { promisify } from 'node:util';
import jsonwebtoken from 'jsonwebtoken';

// Promisify the `jsonwebtoken.verify()` method for simplicity.
const verifyJwt = promisify(jsonwebtoken.verify);

// Function that returns a webhook verifier to be reused across requests.
const createWebhookVerifier = jwk => {
const jwkObject = JSON.parse(jwk);
const publicKey = createPublicKey({ format: 'jwk', key: jwkObject });

return async (body, jws) => {
// Replace the payload portion of the JWS for verification.
const [header, , signature] = jws.split('.');
const payload = Buffer.from(body).toString('base64url');
const token = `${header}.${payload}.${signature}`;

// Verify the complete token.
try {
await verifyJwt(token, publicKey, {
algorithms: [jwkObject.alg]
});
} catch (error) {
if (error instanceof jsonwebtoken.JsonWebTokenError) {
return false;
}

throw error;
}

return true;
};
};

// JWK public key example supplied by Topper.
const jwk = '{"x":"7-INQ150R-MCWlj5X_wyGLRIRYAA-o8NakJiUq7gOGg","y":"dM-GsyJvdDOuALE3l-U9lPL8V3gY_5BPjLH539yTdKU","alg":"ES256","crv":"P-256","kid":"15a5142e-c20f-466e-8132-234dbdae97e7","kty":"EC"}'
// Request body example.
const body = '{"foo":"bar"}';
// X-Topper-JWS-Signature request header example.
const jws = 'eyJhbGciOiJFUzI1NiJ9..2H0Ypm5sVzuSpgyZySdAJan05lYxctqhmO8btghFQQzkisvSlNvNWzQ1kqTPXTLP_dR4zQZrTsSsShAK51I4EQ';

// Verifying webhook request example.
const verifyWebhook = createWebhookVerifier(jwk);
const verified = await verifyWebhook(body, jws);

console.log('Verified:', verified);
```

// Replace the payload portion of the JWS for verification.
const [header,, signature] = request.headers['X-Topper-JWS-Signature'].split('.');
const payload = Buffer.from(JSON.stringify(request.body)).toString('base64url');
const token = `${header}.${payload}.${signature}`;
</TabItem>

// Verify the complete token.
await verify(token, publicKey);
<TabItem label="Java" value="java">

Here is a Java class to verify a signature using the [`nimbus-jose-jwt`](https://mvnrepository.com/artifact/com.nimbusds/nimbus-jose-jwt). Please note that the code below is for illustrative purposes only, and the integration should be adapted to your application.

```java
package example;

import java.security.Key;
import java.text.ParseException;
import java.util.Arrays;

import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.factories.*;
import com.nimbusds.jose.jwk.*;

public class WebhookVerifier {
private final JWK jwk;
private final Key key;

public static void main(String[] args) throws ParseException {
// JWK public key example supplied by Topper.
String jwk = "{\"x\":\"7-INQ150R-MCWlj5X_wyGLRIRYAA-o8NakJiUq7gOGg\",\"y\":\"dM-GsyJvdDOuALE3l-U9lPL8V3gY_5BPjLH539yTdKU\",\"alg\":\"ES256\",\"crv\":\"P-256\",\"kid\":\"15a5142e-c20f-466e-8132-234dbdae97e7\",\"kty\":\"EC\"}";
// Request body example.
String body = "{\"foo\":\"bar\"}";
// X-Topper-JWS-Signature request header example.
String jws = "eyJhbGciOiJFUzI1NiJ9..2H0Ypm5sVzuSpgyZySdAJan05lYxctqhmO8btghFQQzkisvSlNvNWzQ1kqTPXTLP_dR4zQZrTsSsShAK51I4EQ";

// Verifying webhook request example.
WebhookVerifier webhookVerifier = new WebhookVerifier(jwk);
boolean verified = webhookVerifier.verify(body, jws);

System.out.printf("Verified: %b", verified);
}

public WebhookVerifier(String jwkStr) throws ParseException {
// Parse JWK and convert to a Java to be used when verifying signatures.
jwk = JWK.parse(jwkStr);
key = KeyConverter.toJavaKeys(Arrays.asList(new JWK[]{jwk})).get(0);
}

public boolean verify(String requestBody, String jws) {
// Parse JWS into a JWS object.
JWSObject jwsObject;

try {
jwsObject = JWSObject.parse(jws, new Payload(requestBody));
} catch (ParseException e) {
return false;
}

// Check if key referenced in the JWS header matches the JWK.
JWSHeader jwsHeader = jwsObject.getHeader();
JWKMatcher jwsMatcher = JWKMatcher.forJWSHeader(jwsHeader);

if (!jwsMatcher.matches(jwk)) {
return false;
}

// Verify the JWS using the public key.
try {
JWSVerifier jwsVerifier = new DefaultJWSVerifierFactory().createJWSVerifier(jwsHeader, key);

return jwsObject.verify(jwsVerifier);
} catch (JOSEException e) {
return false;
}
}
}
```

Expand Down
192 changes: 143 additions & 49 deletions docs/widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,32 +35,22 @@ Topper supports the following key algorithms:
Here is a quick Node.js snippet to generate an ES256 key pair in Topper's preferred JWK format:

```js
const { generateKeyPairSync } = require('crypto');
import { generateKeyPairSync } from 'crypto';

// Generate a key pair
// Generate a key pair.
const alg = 'ES256';
const { privateKey, publicKey } = generateKeyPairSync('ec', { namedCurve: 'prime256v1' });

// Output in JWK format
console.log({
privateKey: privateKey.export({ format: 'jwk' }),
publicKey: publicKey.export({ format: 'jwk' })
});
// Output in JWK format.
console.log('Private JWK:');
console.log(JSON.stringify({ alg, ...privateKey.export({ format: 'jwk' }) }));
console.log('Public JWK:');
console.log(JSON.stringify({ alg, ...publicKey.export({ format: 'jwk' }) }));

// {
// privateKey: {
// kty: 'EC',
// x: 'zXlHACgk6oYgeTGirjFMToKXPIulH19yB2ywv3ji0L8',
// y: 'CrYvGe6X_A-8YujOMpHlKOmNtUzHXQF4-O0hR6Y9XTo',
// crv: 'P-256',
// d: 'LerkJ-8frEQFj-0Yiw_s6_d8gu0qwha_ita4RIsYO2Q'
// },
// publicKey: {
// kty: 'EC',
// x: 'zXlHACgk6oYgeTGirjFMToKXPIulH19yB2ywv3ji0L8',
// y: 'CrYvGe6X_A-8YujOMpHlKOmNtUzHXQF4-O0hR6Y9XTo',
// crv: 'P-256'
// }
// }
// Private JWK:
// {"alg":"ES256","kty":"EC","x":"dxk0WKKhOyFbU0eZD0plgOB8l9rM-SD5NDgnGpvg99o","y":"nIMebHLyyisqfQKkb-bCp6dVNwVqDR3FLA5ZWUZ_yQ8","crv":"P-256","d":"mdAQEjZBkxtoVeque2wXqebfo1HY0_C2uGApqeKEaX8"}
// Public JWK:
// {"alg":"ES256","kty":"EC","x":"dxk0WKKhOyFbU0eZD0plgOB8l9rM-SD5NDgnGpvg99o","y":"nIMebHLyyisqfQKkb-bCp6dVNwVqDR3FLA5ZWUZ_yQ8","crv":"P-256"}
```

</TabItem>
Expand Down Expand Up @@ -101,49 +91,153 @@ The _payload_ should also contain configuration specific to the widget for which
<Tabs>
<TabItem label="Node.js" value="nodejs" default>

Here is a Node.js snippet to generate a **bootstrap token** for the [crypto on-ramp](./flows/crypto-onramp.mdx) flow using the [`jsonwebtoken`](https://github.com/auth0/node-jsonwebtoken) package:
Here is a Node.js snippet to generate a **bootstrap token** for the [crypto on-ramp](./flows/crypto-onramp.mdx) flow using the [`jsonwebtoken`](https://github.com/auth0/node-jsonwebtoken) package. Please note that the code below is for illustrative purposes only, and the integration should be adapted to your application.

```js
const { createPrivateKey, randomUUID } = require('crypto');
const { promisify } = require('util');
const jsonwebtoken = require('jsonwebtoken');

// Load private key in JWK format from an environment variable.
const privateKeyJwk = JSON.parse(process.env.MY_SIGNING_PRIVATE_KEY);
import { createPrivateKey, randomUUID } from 'node:crypto';
import { promisify } from 'node:util';
import jsonwebtoken from 'jsonwebtoken';

// Promisify the `jsonwebtoken.sign()` method for simplicity.
const sign = promisify(jsonwebtoken.sign);

// Parse the JWK formatted key.
const privateKey = createPrivateKey({ format: 'jwk', key: privateKeyJwk });

// Create the options the `jsonwebtoken.sign()` method.
const options = {
algorithm: 'ES256',
keyid: '<key id>'
const signJwt = promisify(jsonwebtoken.sign);

// Function to create a bootstrap token signer to be reused.
const createBootstrapTokenSigner = (widgetId, keyId, jwk) => {
const jwkObject = JSON.parse(jwk);
const privateKey = createPrivateKey({ format: 'jwk', key: jwkObject });
const options = {
algorithm: jwkObject.alg,
keyid: keyId
};

return async claims => {
claims.sub = widgetId;

if (!claims.jti) {
claims.jti = randomUUID();
}

return await signJwt(claims, privateKey, options);
};
};

// Create the payload for the bootstrap token, note that the
// `jsonwebtoken.sign()` method automatically adds the `iat` claim.
const payload = {
jti: randomUUID(),
sub: '<widget id>',
// Widget id supplied by Topper.
const widgetId = 'd259f6ac-3e8d-46eb-9e6c-c92e138b7660';
// Key id supplied by Topper.
const keyId = 'c084a85d-c486-4035-9c60-8cec81d8b8f5';
// Private JWK you generated.
const jwk = '{"alg":"ES256","crv":"P-256","d":"h-UIda1elff-qw81gsSQakyzOv8Dozv5RcQqFIV6R1Y","kid":"15a5142e-c20f-466e-8132-234dbdae97e7","kty":"EC","x":"7-INQ150R-MCWlj5X_wyGLRIRYAA-o8NakJiUq7gOGg","y":"dM-GsyJvdDOuALE3l-U9lPL8V3gY_5BPjLH539yTdKU"}';

// Signing a bootstrap token example.
const signBootstrapToken = createBootstrapTokenSigner(widgetId, keyId, jwk);
const bootstrapToken = await signBootstrapToken({
source: {
amount: '100.00',
asset: 'USD'
},
target: {
address: '0xb794f5ea0ba39494ce839613fffba74279579268',
asset: 'ETH',
network: 'ethereum',
label: 'My wallet'
label: 'My wallet',
network: 'ethereum'
}
};
});

console.log('Bootstrap token:', bootstrapToken);
```

</TabItem>

// Output the signed bootstrap token.
console.log(await sign(payload, privateKey, options));
<TabItem label="Java" value="java" default>

Here is a Java snippet to generate a **bootstrap token** for the [crypto on-ramp](./flows/crypto-onramp.mdx) flow using [`nimbus-jose-jwt`](https://mvnrepository.com/artifact/com.nimbusds/nimbus-jose-jwt). Please note that the code below is for illustrative purposes only, and the integration should be adapted to your application.

```java
package example;

import java.text.ParseException;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import static java.util.Map.entry;

import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.factories.*;
import com.nimbusds.jose.jwk.*;
import com.nimbusds.jwt.*;

public class BootstrapTokenSigner {
private final String keyId;
private final String widgetId;
private final JWK jwk;
private final JWSSigner jwsSigner;

public static void main(String[] args) throws ParseException, JOSEException {
// Widget id example supplied by Topper.
String widgetId = "d259f6ac-3e8d-46eb-9e6c-c92e138b7660";
// Key id example supplied by Topper.
String keyId = "c084a85d-c486-4035-9c60-8cec81d8b8f5";
// The JWK private key you generated.
String jwk = "{\"alg\":\"ES256\",\"crv\":\"P-256\",\"d\":\"h-UIda1elff-qw81gsSQakyzOv8Dozv5RcQqFIV6R1Y\",\"kid\":\"15a5142e-c20f-466e-8132-234dbdae97e7\",\"kty\":\"EC\",\"x\":\"7-INQ150R-MCWlj5X_wyGLRIRYAA-o8NakJiUq7gOGg\",\"y\":\"dM-GsyJvdDOuALE3l-U9lPL8V3gY_5BPjLH539yTdKU\"}";

// Example of creating a bootstrap token.
BootstrapTokenSigner bootstrapTokenSigner = new BootstrapTokenSigner(widgetId, keyId, jwk);

Map<String, Object> claims = new HashMap<>(Map.ofEntries(
entry("source", Map.ofEntries(
entry("amount", "100.00"),
entry("asset", "USD"))
),
entry("target", Map.ofEntries(
entry("address", "0xb794f5ea0ba39494ce839613fffba74279579268"),
entry("asset", "ETH"),
entry("network", "ethereum"),
entry("label", "My wallet")
))
));

String bootstrapToken = bootstrapTokenSigner.sign(claims);

System.out.printf("Bootstrap token: %s", bootstrapToken);
}

public BootstrapTokenSigner(String widgetId, String keyId, String jwkStr) throws ParseException, JOSEException {
this.widgetId = widgetId;
this.keyId = keyId;

// Parse JWK and create a signer to be used when signing JWTs.
jwk = JWK.parse(jwkStr);
jwsSigner = new DefaultJWSSignerFactory().createJWSSigner(jwk);
}

public String sign(Map<String, Object> claims) throws JOSEException, ParseException {
claims.put("sub", this.widgetId);

// Add iat claim if not present.
if (!claims.containsKey("iat")) {
claims.put("iat", Instant.now().getEpochSecond());
}

// 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImJmYmIxZTBhLTc1ZDYtNDFlYi1hZjY4LTY1ODRlMTY3ZDQwMCJ9.eyJqdGkiOiJmNThmZTk0Yi1kNjUxLTQ4NmYtOTEwYS1jZmMyMWYyZGM1NTciLCJzdWIiOiIyOWQwY2U4Mi02ZTdkLTQ5OGMtYTUxZC03MDcxZGUyYTQ4Y2UiLCJzb3VyY2UiOnsiYW1vdW50IjoiMTAwLjAwIiwiYXNzZXQiOiJVU0QifSwidGFyZ2V0Ijp7ImFkZHJlc3MiOiIweGI3OTRmNWVhMGJhMzk0OTRjZTgzOTYxM2ZmZmJhNzQyNzk1NzkyNjgiLCJhc3NldCI6IkVUSCIsIm5ldHdvcmsiOiJldGhlcmV1bSIsImxhYmVsIjoiTXkgd2FsbGV0In0sImlhdCI6MTY3OTMyMTI0Nn0.uVwWAC37b6qdc74aGSRCcXzNDIzOXCdibFcv6k68tFXCYknItzkoUDapMl798r2nXEq9jq7VSZMuYvbakmo0Hw'
// Add jti claim if not present.
if (!claims.containsKey("jti")) {
claims.put("jti", UUID.randomUUID().toString());
}

JWTClaimsSet jwtClaimsSet = JWTClaimsSet.parse(claims);
JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(jwk.getAlgorithm().getName());
JWSHeader jwsHeader = new JWSHeader.Builder(jwsAlgorithm)
.type(JOSEObjectType.JWT)
.keyID(keyId)
.build();

SignedJWT signedJwt = new SignedJWT(jwsHeader, jwtClaimsSet);

signedJwt.sign(jwsSigner);

return signedJwt.serialize();
}
}
```

</TabItem>
Expand Down
3 changes: 2 additions & 1 deletion docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ const config = {
},
prism: {
darkTheme: darkCodeTheme,
theme: lightCodeTheme
theme: lightCodeTheme,
additionalLanguages: ['java']
}
},
title: 'Topper - Developer docs',
Expand Down

0 comments on commit 4d2dccf

Please sign in to comment.