UNPKG

@azure/msal-browser

Version:
190 lines (187 loc) 7.41 kB
/*! @azure/msal-browser v4.12.0 2025-05-06 */ 'use strict'; import { PerformanceEvents, JoseHeader } from '@azure/msal-common/browser'; import { base64Encode, urlEncode, urlEncodeArr } from '../encode/Base64Encode.mjs'; import { base64Decode } from '../encode/Base64Decode.mjs'; import { validateCryptoAvailable, createNewGuid, generateKeyPair, exportJwk, importJwk, sign, hashString } from './BrowserCrypto.mjs'; import { createBrowserAuthError } from '../error/BrowserAuthError.mjs'; import { AsyncMemoryStorage } from '../cache/AsyncMemoryStorage.mjs'; import { cryptoKeyNotFound } from '../error/BrowserAuthErrorCodes.mjs'; /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ /** * 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). */ class CryptoOps { constructor(logger, performanceClient, skipValidateSubtleCrypto) { this.logger = logger; // Browser crypto needs to be validated first before any other classes can be set. validateCryptoAvailable(skipValidateSubtleCrypto ?? false); this.cache = new AsyncMemoryStorage(this.logger); this.performanceClient = performanceClient; } /** * Creates a new random GUID - used to populate state and nonce. * @returns string (GUID) */ createNewGuid() { return createNewGuid(); } /** * Encodes input string to base64. * @param input */ base64Encode(input) { return base64Encode(input); } /** * Decodes input string from base64. * @param input */ base64Decode(input) { return base64Decode(input); } /** * Encodes input string to base64 URL safe string. * @param input */ base64UrlEncode(input) { return urlEncode(input); } /** * Stringifies and base64Url encodes input public key * @param inputKid * @returns Base64Url encoded public key */ encodeKid(inputKid) { return this.base64UrlEncode(JSON.stringify({ kid: inputKid })); } /** * Generates a keypair, stores it and returns a thumbprint * @param request */ async getPublicKeyThumbprint(request) { const publicKeyThumbMeasurement = this.performanceClient?.startMeasurement(PerformanceEvents.CryptoOptsGetPublicKeyThumbprint, request.correlationId); // Generate Keypair const keyPair = await generateKeyPair(CryptoOps.EXTRACTABLE, CryptoOps.POP_KEY_USAGES); // Generate Thumbprint for Public Key const publicKeyJwk = await exportJwk(keyPair.publicKey); const pubKeyThumprintObj = { e: publicKeyJwk.e, kty: publicKeyJwk.kty, n: publicKeyJwk.n, }; const publicJwkString = getSortedObjectString(pubKeyThumprintObj); const publicJwkHash = await this.hashString(publicJwkString); // Generate Thumbprint for Private Key const privateKeyJwk = await exportJwk(keyPair.privateKey); // Re-import private key to make it unextractable const unextractablePrivateKey = await 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) { await this.cache.removeItem(kid); const keyFound = await this.cache.containsKey(kid); return !keyFound; } /** * Removes all cryptographic keys from IndexedDB storage */ async clearKeystore() { // 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, kid, shrOptions, correlationId) { const signJwtMeasurement = this.performanceClient?.startMeasurement(PerformanceEvents.CryptoOptsSignJwt, correlationId); const cachedKeyPair = await this.cache.getItem(kid); if (!cachedKeyPair) { throw createBrowserAuthError(cryptoKeyNotFound); } // Get public key as JWK const publicKeyJwk = await 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 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) { return hashString(plainText); } } CryptoOps.POP_KEY_USAGES = ["sign", "verify"]; CryptoOps.EXTRACTABLE = true; function getSortedObjectString(obj) { return JSON.stringify(obj, Object.keys(obj).sort()); } export { CryptoOps }; //# sourceMappingURL=CryptoOps.mjs.map