Created
February 24, 2022 14:35
-
-
Save AdamJLemmon/9e6b1dcbf879530429efa7e780740e06 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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