@stacks/auth
Version:
Authentication for Stacks apps.
250 lines (232 loc) • 8.88 kB
text/typescript
import {
bytesToHex,
bytesToUtf8,
getGlobalObject,
hexToBytes,
makeUUID4,
nextMonth,
utf8ToBytes,
} from '@stacks/common';
import {
decryptECIES,
encryptECIES,
makeECPrivateKey,
publicKeyToBtcAddress,
} from '@stacks/encryption';
import { SECP256K1Client, TokenSigner } from 'jsontokens';
import { AuthScope, DEFAULT_SCOPE } from './constants';
import { makeDIDFromAddress } from './dids';
const VERSION = '1.4.0';
type AuthMetadata = {
email?: string;
profileUrl?: string;
};
/**
* Generates a ECDSA keypair to
* use as the ephemeral app transit private key
* @param {SessionData} session - session object in which key will be stored
* @return {String} the hex encoded private key
* @private
* @ignore
*/
export function generateTransitKey() {
const transitKey = makeECPrivateKey();
return transitKey;
}
/** @deprecated {@link makeAuthRequest} was renamed to {@link makeAuthRequestToken} */
export const makeAuthRequest = makeAuthRequestToken;
/**
* Generates an authentication request that can be sent to the Blockstack
* browser for the user to approve sign in. This authentication request can
* then be used for sign in by passing it to the `redirectToSignInWithAuthRequest`
* method.
*
* *Note: This method should only be used if you want to roll your own authentication
* flow. Typically you'd use `redirectToSignIn` which takes care of this
* under the hood.*
*
* @param {String} transitPrivateKey - hex encoded transit private key
* @param {String} redirectURI - location to redirect user to after sign in approval
* @param {String} manifestURI - location of this app's manifest file
* @param {(AuthScope | string)[]} scopes - the permissions this app is requesting
* @param {String} appDomain - the origin of this app
* @param {Number} expiresAt - the time at which this request is no longer valid
* @param {Object} extraParams - Any extra parameters you'd like to pass to the authenticator.
* Use this to pass options that aren't part of the Blockstack auth spec, but might be supported
* by special authenticators.
* @return {String} the authentication request
*/
export function makeAuthRequestToken(
transitPrivateKey: string,
redirectURI?: string,
manifestURI?: string,
scopes: (AuthScope | string)[] = DEFAULT_SCOPE.slice(),
appDomain?: string,
expiresAt: number = nextMonth().getTime(),
extraParams: any = {}
): string {
const getWindowOrigin = (paramName: string) => {
const location = getGlobalObject('location', {
throwIfUnavailable: true,
usageDesc: `makeAuthRequest([${paramName}=undefined])`,
});
return location?.origin;
};
if (!redirectURI) {
redirectURI = `${getWindowOrigin('redirectURI')}/`;
}
if (!manifestURI) {
manifestURI = `${getWindowOrigin('manifestURI')}/manifest.json`;
}
if (!appDomain) {
appDomain = getWindowOrigin('appDomain');
}
/* Create the payload */
const payload = Object.assign({}, extraParams, {
jti: makeUUID4(),
iat: Math.floor(new Date().getTime() / 1000), // JWT times are in seconds
exp: Math.floor(expiresAt / 1000), // JWT times are in seconds
iss: null,
public_keys: [],
domain_name: appDomain,
manifest_uri: manifestURI,
redirect_uri: redirectURI,
version: VERSION,
do_not_include_profile: true,
supports_hub_url: true,
scopes,
});
// Logger.info(`blockstack.js: generating v${VERSION} auth request`)
/* Convert the private key to a public key to an issuer */
const publicKey = SECP256K1Client.derivePublicKey(transitPrivateKey);
payload.public_keys = [publicKey];
const address = publicKeyToBtcAddress(publicKey);
payload.iss = makeDIDFromAddress(address);
/* Sign and return the token */
const tokenSigner = new TokenSigner('ES256k', transitPrivateKey);
const token = tokenSigner.sign(payload);
return token;
}
/**
* Encrypts the private key for decryption by the given
* public key.
* @param {String} publicKey [description]
* @param {String} privateKey [description]
* @return {String} hex encoded ciphertext
* @private
* @ignore
*/
export async function encryptPrivateKey(publicKey: string, privateKey: string): Promise<string> {
const encryptedObj = await encryptECIES(publicKey, utf8ToBytes(privateKey), true);
const encryptedJSON = JSON.stringify(encryptedObj);
return bytesToHex(utf8ToBytes(encryptedJSON));
}
/**
* Decrypts the hex encrypted private key
* @param {String} privateKey the private key corresponding to the public
* key for which the ciphertext was encrypted
* @param {String} hexedEncrypted the ciphertext
* @return {String} the decrypted private key
* @throws {Error} if unable to decrypt
*
* @private
* @ignore
*/
export async function decryptPrivateKey(
privateKey: string,
hexedEncrypted: string
): Promise<string | null> {
const unhexedString = bytesToUtf8(hexToBytes(hexedEncrypted));
const encryptedObj = JSON.parse(unhexedString);
const decrypted = await decryptECIES(privateKey, encryptedObj);
if (typeof decrypted !== 'string') {
throw new Error('Unable to correctly decrypt private key');
} else {
return decrypted;
}
}
/**
* Generates a signed authentication response token for an app. This
* token is sent back to apps which use contents to access the
* resources and data requested by the app.
*
* @param {String} privateKey the identity key of the Blockstack ID generating
* the authentication response
* @param {Object} profile the profile object for the Blockstack ID
* @param {AuthMetadata} metadata an object containing metadata sent as part of the authentication
* response including `email` if requested and available and a URL to the profile
* @param {String} coreToken core session token when responding to a legacy auth request
* or `null` for current direct to gaia authentication requests
* @param {String} appPrivateKey the application private key. This private key is
* unique and specific for every Blockstack ID and application combination.
* @param {Number} expiresAt an integer in the same format as
* `new Date().getTime()`, milliseconds since the Unix epoch
* @param {String} transitPublicKey the public key provide by the app
* in its authentication request with which secrets will be encrypted
* @param {String} hubUrl URL to the write path of the user's Gaia hub
* @param {String} blockstackAPIUrl URL to the API endpoint to use
* @param {String} associationToken JWT that binds the app key to the identity key
* @return {String} signed and encoded authentication response token
* @private
* @ignore
*/
export async function makeAuthResponse(
privateKey: string,
// eslint-disable-next-line @typescript-eslint/ban-types
profile: {} = {},
metadata: AuthMetadata | null,
coreToken: string | null = null,
appPrivateKey: string | null = null,
expiresAt: number = nextMonth().getTime(),
transitPublicKey: string | null = null,
hubUrl: string | null = null,
blockstackAPIUrl: string | null = null,
associationToken: string | null = null,
appPrivateKeyFromWalletSalt: string | null = null
): Promise<string> {
/* Convert the private key to a public key to an issuer */
const publicKey = SECP256K1Client.derivePublicKey(privateKey);
const address = publicKeyToBtcAddress(publicKey);
/* See if we should encrypt with the transit key */
let privateKeyPayload = appPrivateKey;
let coreTokenPayload = coreToken;
let additionalProperties = {};
if (appPrivateKey !== undefined && appPrivateKey !== null) {
// Logger.info(`blockstack.js: generating v${VERSION} auth response`)
if (transitPublicKey !== undefined && transitPublicKey !== null) {
privateKeyPayload = await encryptPrivateKey(transitPublicKey, appPrivateKey);
if (coreToken !== undefined && coreToken !== null) {
coreTokenPayload = await encryptPrivateKey(transitPublicKey, coreToken);
}
}
additionalProperties = {
email: metadata?.email ? metadata.email : null,
profile_url: metadata?.profileUrl ? metadata.profileUrl : null,
hubUrl,
blockstackAPIUrl,
associationToken,
version: VERSION,
};
} else {
// Logger.info('blockstack.js: generating legacy auth response')
}
/* Create the payload */
const payload = Object.assign(
{},
{
jti: makeUUID4(),
iat: Math.floor(new Date().getTime() / 1000), // JWT times are in seconds
exp: Math.floor(expiresAt / 1000), // JWT times are in seconds
iss: makeDIDFromAddress(address),
private_key: privateKeyPayload,
public_keys: [publicKey],
appPrivateKeyFromWalletSalt,
profile,
core_token: coreTokenPayload,
},
additionalProperties
);
/* Sign and return the token */
const tokenSigner = new TokenSigner('ES256k', privateKey);
return tokenSigner.sign(payload);
}