From 4d2dccf5ad47e856b4b82ef92800c8a050f43b7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Cruz?= Date: Tue, 11 Jun 2024 03:51:53 +0100 Subject: [PATCH] Add Java examples and refactored Node.js examples --- docs/webhooks.md | 140 ++++++++++++++++++++++++++----- docs/widgets.md | 192 ++++++++++++++++++++++++++++++++----------- docusaurus.config.js | 3 +- 3 files changed, 263 insertions(+), 72 deletions(-) diff --git a/docs/webhooks.md b/docs/webhooks.md index 5cce1a2..8690f6e 100644 --- a/docs/webhooks.md +++ b/docs/webhooks.md @@ -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. -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}`; + - // Verify the complete token. - await verify(token, publicKey); + + +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; + } + } } ``` diff --git a/docs/widgets.md b/docs/widgets.md index aa243a0..b0aafcd 100644 --- a/docs/widgets.md +++ b/docs/widgets.md @@ -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"} ``` @@ -101,33 +91,46 @@ The _payload_ should also contain configuration specific to the widget for which -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: '' +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 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' @@ -135,15 +138,106 @@ const payload = { target: { address: '0xb794f5ea0ba39494ce839613fffba74279579268', asset: 'ETH', - network: 'ethereum', - label: 'My wallet' + label: 'My wallet', + network: 'ethereum' } -}; +}); + +console.log('Bootstrap token:', bootstrapToken); +``` + + -// Output the signed bootstrap token. -console.log(await sign(payload, privateKey, options)); + + +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 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 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(); + } +} ``` diff --git a/docusaurus.config.js b/docusaurus.config.js index 11d3c71..513a8bc 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -42,7 +42,8 @@ const config = { }, prism: { darkTheme: darkCodeTheme, - theme: lightCodeTheme + theme: lightCodeTheme, + additionalLanguages: ['java'] } }, title: 'Topper - Developer docs',