UNPKG

@azure/msal-browser

Version:
286 lines (249 loc) 8.65 kB
/* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import { ICrypto, IPerformanceClient, JoseHeader, Logger, PerformanceEvents, ShrOptions, SignedHttpRequest, SignedHttpRequestParameters, } from "@azure/msal-common/browser"; import { base64Encode, urlEncode, urlEncodeArr, } from "../encode/Base64Encode.js"; import { base64Decode } from "../encode/Base64Decode.js"; import * as BrowserCrypto from "./BrowserCrypto.js"; import { createBrowserAuthError, BrowserAuthErrorCodes, } from "../error/BrowserAuthError.js"; import { AsyncMemoryStorage } from "../cache/AsyncMemoryStorage.js"; export type CachedKeyPair = { publicKey: CryptoKey; privateKey: CryptoKey; requestMethod?: string; requestUri?: string; }; /** * This class implements MSAL's crypto interface, which allows it to perform base64 encoding and decoding, generating cryptographically random GUIDs and * implementing Proof Key for Code Exchange specs for the OAuth Authorization Code Flow using PKCE (rfc here: https://tools.ietf.org/html/rfc7636). */ export class CryptoOps implements ICrypto { private logger: Logger; /** * CryptoOps can be used in contexts outside a PCA instance, * meaning there won't be a performance manager available. */ private performanceClient: IPerformanceClient | undefined; private static POP_KEY_USAGES: Array<KeyUsage> = ["sign", "verify"]; private static EXTRACTABLE: boolean = true; private cache: AsyncMemoryStorage<CachedKeyPair>; constructor( logger: Logger, performanceClient?: IPerformanceClient, skipValidateSubtleCrypto?: boolean ) { this.logger = logger; // Browser crypto needs to be validated first before any other classes can be set. BrowserCrypto.validateCryptoAvailable( skipValidateSubtleCrypto ?? false ); this.cache = new AsyncMemoryStorage<CachedKeyPair>(this.logger); this.performanceClient = performanceClient; } /** * Creates a new random GUID - used to populate state and nonce. * @returns string (GUID) */ createNewGuid(): string { return BrowserCrypto.createNewGuid(); } /** * Encodes input string to base64. * @param input */ base64Encode(input: string): string { return base64Encode(input); } /** * Decodes input string from base64. * @param input */ base64Decode(input: string): string { return base64Decode(input); } /** * Encodes input string to base64 URL safe string. * @param input */ base64UrlEncode(input: string): string { return urlEncode(input); } /** * Stringifies and base64Url encodes input public key * @param inputKid * @returns Base64Url encoded public key */ encodeKid(inputKid: string): string { return this.base64UrlEncode(JSON.stringify({ kid: inputKid })); } /** * Generates a keypair, stores it and returns a thumbprint * @param request */ async getPublicKeyThumbprint( request: SignedHttpRequestParameters ): Promise<string> { const publicKeyThumbMeasurement = this.performanceClient?.startMeasurement( PerformanceEvents.CryptoOptsGetPublicKeyThumbprint, request.correlationId ); // Generate Keypair const keyPair: CryptoKeyPair = await BrowserCrypto.generateKeyPair( CryptoOps.EXTRACTABLE, CryptoOps.POP_KEY_USAGES ); // Generate Thumbprint for Public Key const publicKeyJwk: JsonWebKey = await BrowserCrypto.exportJwk( keyPair.publicKey ); const pubKeyThumprintObj: JsonWebKey = { e: publicKeyJwk.e, kty: publicKeyJwk.kty, n: publicKeyJwk.n, }; const publicJwkString: string = getSortedObjectString(pubKeyThumprintObj); const publicJwkHash = await this.hashString(publicJwkString); // Generate Thumbprint for Private Key const privateKeyJwk: JsonWebKey = await BrowserCrypto.exportJwk( keyPair.privateKey ); // Re-import private key to make it unextractable const unextractablePrivateKey: CryptoKey = await BrowserCrypto.importJwk(privateKeyJwk, false, ["sign"]); // Store Keypair data in keystore await this.cache.setItem(publicJwkHash, { privateKey: unextractablePrivateKey, publicKey: keyPair.publicKey, requestMethod: request.resourceRequestMethod, requestUri: request.resourceRequestUri, }); if (publicKeyThumbMeasurement) { publicKeyThumbMeasurement.end({ success: true, }); } return publicJwkHash; } /** * Removes cryptographic keypair from key store matching the keyId passed in * @param kid */ async removeTokenBindingKey(kid: string): Promise<boolean> { await this.cache.removeItem(kid); const keyFound = await this.cache.containsKey(kid); return !keyFound; } /** * Removes all cryptographic keys from IndexedDB storage */ async clearKeystore(): Promise<boolean> { // Delete in-memory keystores this.cache.clearInMemory(); /** * There is only one database, so calling clearPersistent on asymmetric keystore takes care of * every persistent keystore */ try { await this.cache.clearPersistent(); return true; } catch (e) { if (e instanceof Error) { this.logger.error( `Clearing keystore failed with error: ${e.message}` ); } else { this.logger.error( "Clearing keystore failed with unknown error" ); } return false; } } /** * Signs the given object as a jwt payload with private key retrieved by given kid. * @param payload * @param kid */ async signJwt( payload: SignedHttpRequest, kid: string, shrOptions?: ShrOptions, correlationId?: string ): Promise<string> { const signJwtMeasurement = this.performanceClient?.startMeasurement( PerformanceEvents.CryptoOptsSignJwt, correlationId ); const cachedKeyPair = await this.cache.getItem(kid); if (!cachedKeyPair) { throw createBrowserAuthError( BrowserAuthErrorCodes.cryptoKeyNotFound ); } // Get public key as JWK const publicKeyJwk = await BrowserCrypto.exportJwk( cachedKeyPair.publicKey ); const publicKeyJwkString = getSortedObjectString(publicKeyJwk); // Base64URL encode public key thumbprint with keyId only: BASE64URL({ kid: "FULL_PUBLIC_KEY_HASH" }) const encodedKeyIdThumbprint = urlEncode(JSON.stringify({ kid: kid })); // Generate header const shrHeader = JoseHeader.getShrHeaderString({ ...shrOptions?.header, alg: publicKeyJwk.alg, kid: encodedKeyIdThumbprint, }); const encodedShrHeader = urlEncode(shrHeader); // Generate payload payload.cnf = { jwk: JSON.parse(publicKeyJwkString), }; const encodedPayload = urlEncode(JSON.stringify(payload)); // Form token string const tokenString = `${encodedShrHeader}.${encodedPayload}`; // Sign token const encoder = new TextEncoder(); const tokenBuffer = encoder.encode(tokenString); const signatureBuffer = await BrowserCrypto.sign( cachedKeyPair.privateKey, tokenBuffer ); const encodedSignature = urlEncodeArr(new Uint8Array(signatureBuffer)); const signedJwt = `${tokenString}.${encodedSignature}`; if (signJwtMeasurement) { signJwtMeasurement.end({ success: true, }); } return signedJwt; } /** * Returns the SHA-256 hash of an input string * @param plainText */ async hashString(plainText: string): Promise<string> { return BrowserCrypto.hashString(plainText); } } function getSortedObjectString(obj: object): string { return JSON.stringify(obj, Object.keys(obj).sort()); }