UNPKG

@stacks/auth

Version:

Authentication for Stacks apps.

220 lines (208 loc) 7.16 kB
import { isSameOriginAbsoluteUrl } from '@stacks/common'; import { publicKeyToBtcAddress } from '@stacks/encryption'; import { decodeToken, TokenVerifier } from 'jsontokens'; import { getAddressFromDID } from './dids'; import { fetchAppManifest } from './provider'; /** * Checks if the ES256k signature on passed `token` match the claimed public key * in the payload key `public_keys`. * * @param {String} token encoded and signed authentication token * @return {Boolean} Returns `true` if the signature matches the claimed public key * @throws {Error} if `token` contains multiple public keys * @private * @ignore */ export function doSignaturesMatchPublicKeys(token: string): boolean { const payload = decodeToken(token).payload; if (typeof payload === 'string') { throw new Error('Unexpected token payload type of string'); } const publicKeys = payload.public_keys as string[]; if (publicKeys.length === 1) { const publicKey = publicKeys[0]; try { const tokenVerifier = new TokenVerifier('ES256k', publicKey); return tokenVerifier.verify(token); } catch (e) { return false; } } else { throw new Error('Multiple public keys are not supported'); } } /** * Makes sure that the identity address portion of * the decentralized identifier passed in the issuer `iss` * key of the token matches the public key * * @param {String} token encoded and signed authentication token * @return {Boolean} if the identity address and public keys match * @throws {Error} if ` token` has multiple public keys * @private * @ignore */ export function doPublicKeysMatchIssuer(token: string): boolean { const payload = decodeToken(token).payload; if (typeof payload === 'string') { throw new Error('Unexpected token payload type of string'); } const publicKeys = payload.public_keys as string[]; const addressFromIssuer = getAddressFromDID(payload.iss); if (publicKeys.length === 1) { const addressFromPublicKeys = publicKeyToBtcAddress(publicKeys[0]); if (addressFromPublicKeys === addressFromIssuer) { return true; } } else { throw new Error('Multiple public keys are not supported'); } return false; } /** * Checks if the if the token issuance time and date is after the * current time and date. * * @param {String} token encoded and signed authentication token * @return {Boolean} `true` if the token was issued after the current time, * otherwise returns `false` * @private * @ignore */ export function isIssuanceDateValid(token: string) { const payload = decodeToken(token).payload; if (typeof payload === 'string') { throw new Error('Unexpected token payload type of string'); } if (payload.iat) { if (typeof payload.iat !== 'number') { return false; } const issuedAt = new Date(payload.iat * 1000); // JWT times are in seconds if (new Date().getTime() < issuedAt.getTime()) { return false; } else { return true; } } else { return true; } } /** * Checks if the expiration date of the `token` is before the current time * @param {String} token encoded and signed authentication token * @return {Boolean} `true` if the `token` has not yet expired, `false` * if the `token` has expired * * @private * @ignore */ export function isExpirationDateValid(token: string) { const payload = decodeToken(token).payload; if (typeof payload === 'string') { throw new Error('Unexpected token payload type of string'); } if (payload.exp) { if (typeof payload.exp !== 'number') { return false; } const expiresAt = new Date(payload.exp * 1000); // JWT times are in seconds if (new Date().getTime() > expiresAt.getTime()) { return false; } else { return true; } } else { return true; } } /** * Makes sure the `manifest_uri` is a same origin absolute URL. * @param {String} token encoded and signed authentication token * @return {Boolean} `true` if valid, otherwise `false` * @private * @ignore */ export function isManifestUriValid(token: string) { const payload = decodeToken(token).payload; if (typeof payload === 'string') { throw new Error('Unexpected token payload type of string'); } return isSameOriginAbsoluteUrl(payload.domain_name as string, payload.manifest_uri as string); } /** * Makes sure the `redirect_uri` is a same origin absolute URL. * @param {String} token encoded and signed authentication token * @return {Boolean} `true` if valid, otherwise `false` * @private * @ignore */ export function isRedirectUriValid(token: string) { const payload = decodeToken(token).payload; if (typeof payload === 'string') { throw new Error('Unexpected token payload type of string'); } return isSameOriginAbsoluteUrl(payload.domain_name as string, payload.redirect_uri as string); } /** * Verify authentication request is valid. This function performs a number * of checks on the authentication request token: * * Checks that `token` has a valid issuance date & is not expired * * Checks that `token` has a valid signature that matches the public key it claims * * Checks that both the manifest and redirect URLs are absolute and conform to * the same origin policy * * @param {String} token encoded and signed authentication request token * @return {Promise} that resolves to true if the auth request * is valid and false if it does not. It rejects with a String if the * token is not signed * @private * @ignore */ export async function verifyAuthRequest(token: string): Promise<boolean> { if (decodeToken(token).header.alg === 'none') { throw new Error('Token must be signed in order to be verified'); } const values = await Promise.all([ isExpirationDateValid(token), isIssuanceDateValid(token), doSignaturesMatchPublicKeys(token), doPublicKeysMatchIssuer(token), isManifestUriValid(token), isRedirectUriValid(token), ]); return values.every(val => val); } /** * Verify the authentication request is valid and * fetch the app manifest file if valid. Otherwise, reject the promise. * @param {String} token encoded and signed authentication request token * @return {Promise} that resolves to the app manifest file in JSON format * or rejects if the auth request or app manifest file is invalid * @private * @ignore */ export async function verifyAuthRequestAndLoadManifest(token: string): Promise<any> { const valid = await verifyAuthRequest(token); if (!valid) { throw new Error('Token is an invalid auth request'); } return fetchAppManifest(token); } /** * Verify the authentication response is valid. * @param {String} token the authentication response token * @return {Promise} that resolves to true if auth response * is valid and false if it does not * @private * @ignore */ export async function verifyAuthResponse(token: string): Promise<boolean> { const conditions = await Promise.all([ isExpirationDateValid(token), isIssuanceDateValid(token), doSignaturesMatchPublicKeys(token), doPublicKeysMatchIssuer(token), ]); return conditions.every(val => val); }