Skip to content

Instantly share code, notes, and snippets.

@AdamJLemmon
Created February 24, 2022 14:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AdamJLemmon/9e6b1dcbf879530429efa7e780740e06 to your computer and use it in GitHub Desktop.
Save AdamJLemmon/9e6b1dcbf879530429efa7e780740e06 to your computer and use it in GitHub Desktop.
const axios = require('axios');
const {
uuid,
} = require('short-uuid');
const open = require('open');
const base64url = require('base64url');
const bs58 = require('bs58');
const didKeyDriver = require('did-method-key')
.driver();
const forge = require('node-forge');
const ED25519 = forge.pki.ed25519;
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
const API_URL = 'https://api.staging.trybe.id' || process.env.API_URL;
const API_KEY = process.env.API_KEY;
const ORG_ID = '620d28861f2eab003fe92ea7' || process.env.ORG_ID;
const OIDC_CONFIG_PATH = '.well-known/openid-configuration';
/**
* Most commonly the process is kicked off through credential issuance
* ie. A request to /provider/issueCustomCredential
*/
const issueCustomCredential = async () => {
const credential = {
'@context': [
'https://www.w3.org/2018/credentials/v1',
'https://w3id.org/vc-status-list-2021/v1'
],
'name': 'credentialName',
'type': [ 'VerifiableCredential' ],
'credentialSubject': {
'name': 'subjectName'
},
'issuer': {
'name': 'issuerName',
'url': 'issuerUrl'
},
'additionalData': {
'resources': [{
'id': 'Image 1',
'uri': 'base64 data uri'
}]
}
};
const options = {
'responseType': [ 'walletInstallAndCredentialDownloadUrl' ]
};
const { data } = await axios.post(
`${API_URL}/provider/issueCustomCredential`,
{
orgId: ORG_ID,
credential,
options
},
{
headers: {
'vcx-api-key': API_KEY
}
}
);
return {
url: data[0].url
};
}
const registerClient = async registrationEndpoint => {
const registrationPayload = {
redirect_uris: [
'https://url-to-wallet/callback'
]
};
const { data } = await axios.post(
registrationEndpoint,
registrationPayload,
{
headers: {
accept: 'application/json',
'Content-Type': 'application/json'
}
}
);
return {
clientId: data.client_id
};
};
const authenticateClient = async (clientId, loginHint, authorizationEndpoint) => {
const query = {
client_id: clientId, // gotten during registration
scope: 'openid openid_credential', // static
redirect_uri: 'https://url-to-wallet/callback', // actual url to our wallet
response_type: 'code', // static
nonce: uuid(), // generate each time
login_hint: loginHint,
state: uuid(), // generate each time
code_challenge: 'r5MqyqJlfTKs4_peigzwA4nVck9K8N37dy-H3wM47_0', // will need to generate this properly
code_challenge_method: 'S256', // static
};
const urlQuery = new URLSearchParams(query);
const queryString = urlQuery.toString();
const redirectUrl = `${authorizationEndpoint}?${queryString}`;
console.log('COPY THE CODE FROM THE BROWSER URL!!');
open(redirectUrl);
// ISSUER will redirect back to the wallet with a code
// ie. https://url-to-wallet/callback?code=0afe4b5f-a49d-4ce5-9e1a-7bfef280d24c&state=b9c4721b-d176-4506-8f4f-21d8597b8112
// code = 0afe4b5f-a49d-4ce5-9e1a-7bfef280d24c
const code = await new Promise((resolve, reject) => {
readline.question('Please end the code from the redirect URL here: ', code => {
readline.close();
resolve(code);
});
});
return {
code
};
};
const exchangeCodeForAccessToken = async (code, clientId, tokenEndpoint) => {
const payload = {
code,
grant_type: 'authorization_code', // static
client_id: clientId,
redirect_url: 'https://url-to-wallet/callback', // of our actual wallet, but is not used
code_verifier: 'oZdZCJH5j18zICowuBPplVRfS3lXQJ0omz19bXZtfjEwLxt6' // needs to be entered correct that was generated during the auth request
};
const { data } = await axios.post(
tokenEndpoint,
payload,
{
headers: {
accept: 'application/json',
'Content-Type': 'application/json'
}
}
);
return {
accessToken: data.access_token
};
};
const getCredential = async (accessToken, credentialEndpoint) => {
const keyPair = await generateKeyPair();
const request = await createSignedRequest({
publicKey: keyPair.didDocument.publicKey[0],
privateKey: keyPair.privateKey,
sub: keyPair.id,
aud: API_URL
});
const { data } = await axios.post(
credentialEndpoint,
{
request
},
{
headers: {
accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${accessToken}`
}
}
);
return {
credential: data.credential
};
};
const generateKeyPair = async () => {
const didDocument = await didKeyDriver.generate();
const { id, publicKey } = didDocument;
const {
privateKeyBase58,
} = didDocument.keys[publicKey[0].id];
const privateKeyBase64 = bs58.decode(privateKeyBase58);
const privateKey = Buffer.from(privateKeyBase64, 'base64');
return {
id,
didDocument,
privateKey
};
};
const createSignedRequest = async ({ publicKey, privateKey, sub, aud }) => {
const payload = {
iss: 'https://wallet.example.com', // URL of the wallet... wallet.convergence.tech...
sub, // keypair id, controller of the EDV
aud, // api.trybe.id
exp: (Date.now() / 1000) + 1000000,
iat: Date.now() / 1000,
nonce: uuid(),
};
// ED25519 signing: https://tools.ietf.org/html/rfc8037#appendix-A.4
const header = {
alg: 'EdDSA',
typ: 'JWT',
kid: publicKey.id,
};
const b64UrlHeader = base64url.fromBase64(
Buffer.from(JSON.stringify(header), 'utf8')
.toString('base64'),
);
const b64UrlPayload = base64url.fromBase64(
Buffer.from(JSON.stringify(payload), 'utf8')
.toString('base64'),
);
const signature = ED25519.sign({
message: Buffer.from(`${b64UrlHeader}.${b64UrlPayload}`, 'utf8'),
privateKey,
});
// Built JWT
const b64UrlSignature = base64url.fromBase64(signature.toString('base64'));
const jwt = `${b64UrlHeader}.${b64UrlPayload}.${b64UrlSignature}`;
return jwt;
}
const run = async () => {
// 1. ISSUER Issue the credential
// This is the 3rd party login url that may be used to get the credential
const { url } = await issueCustomCredential();
// This URL is commonly enocded into a QR code and then shared with the Holder to pick up the credential
// The wallet that will pick up the credential needs to parse iss and login_hint from the url
// 2. WALLET parse the iss and login_hint
const query = (new URL(url)).searchParams;
const iss = query.get('iss');
const loginHint = query.get('login_hint');
// 3. WALLET get oidc config from iss
const oidcConfig = (await axios.get(`${iss}/${OIDC_CONFIG_PATH}`)).data;
const {
registration_endpoint: registrationEndpoint,
authorization_endpoint: authorizationEndpoint,
token_endpoint: tokenEndpoint,
credential_endpoint: credentialEndpoint
} = oidcConfig;
// 4. WALLET register itself with the issuer as a client
const { clientId } = await registerClient(registrationEndpoint);
// 5. WALLET send authentication request to issuer
// This will physically open up a browser and do the redirects
const { code } = await authenticateClient(clientId, loginHint, authorizationEndpoint);
// 6. Exchange the code for a token
const { accessToken } = await exchangeCodeForAccessToken(code, clientId, tokenEndpoint);
// 7. Send the request to get the credential
const { credential } = await getCredential(accessToken, credentialEndpoint);
console.log({ credential });
};
run();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment