UNPKG

bpt-pack-two

Version:

Study Passwordless authentication on aws project

244 lines (243 loc) 9.68 kB
/** * Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). You * may not use this file except in compliance with the License. A copy of * the License is located at * * http://aws.amazon.com/apache2.0/ * * or in the "license" file accompanying this file. This file is * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF * ANY KIND, either express or implied. See the License for the specific * language governing permissions and limitations under the License. */ import { configure } from "./config.js"; import { initiateAuth, respondToAuthChallenge, assertIsChallengeResponse, handleAuthResponse, } from "./cognito-api.js"; import { defaultTokensCb } from "./common.js"; import { bufferFromBase64, bufferToBase64 } from "./util.js"; let _CONSTANTS; async function getConstants() { if (!_CONSTANTS) { const g = BigInt(2); const N = BigInt("0x" + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" + "83655D23DCA3AD961C62F356208552BB9ED529077096966D" + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" + "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" + "DE2BCBF6955817183995497CEA956AE515D2261898FA0510" + "15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64" + "ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7" + "ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B" + "F12FFA06D98A0864D87602733EC86A64521F2B18177B200C" + "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31" + "43DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF"); const { crypto } = configure(); const k = arrayBufferToBigInt(await crypto.subtle.digest("SHA-256", hexToArrayBuffer(`${padHex(N.toString(16))}${padHex(g.toString(16))}`))); _CONSTANTS = { g, N, k, }; } return _CONSTANTS; } /** * modulo that works on negative bases too */ function modulo(base, mod) { return ((base % mod) + mod) % mod; } function modPow(base, exp, mod) { // Calculate: (base ** exp) % mod let result = BigInt(1); let x = modulo(base, mod); while (exp > BigInt(0)) { if (modulo(exp, BigInt(2))) { result = modulo(result * x, mod); } exp = exp / BigInt(2); x = modulo(x * x, mod); } return result; } function padHex(hexStr) { hexStr = hexStr.length % 2 ? `0${hexStr}` : hexStr; hexStr = parseInt(hexStr.slice(0, 2), 16) >> 7 ? `00${hexStr}` : hexStr; return hexStr; } function generateSmallA() { const { crypto } = configure(); const randomValues = new Uint8Array(128); crypto.getRandomValues(randomValues); return arrayBufferToBigInt(randomValues.buffer); } async function calculateLargeAHex(smallA) { const { g, N } = await getConstants(); return modPow(g, smallA, N).toString(16); } async function calculateSrpSignature({ smallA, largeAHex, srpBHex, salt, userPoolId, username, password, secretBlock, }) { const { crypto } = configure(); const aPlusBHex = padHex(largeAHex) + padHex(srpBHex); const u = await crypto.subtle.digest("SHA-256", hexToArrayBuffer(aPlusBHex)); const [, userPoolName] = userPoolId.split("_"); const usernamePasswordHash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`${userPoolName}${username}:${password}`)); const x = await crypto.subtle.digest("SHA-256", await new Blob([ hexToArrayBuffer(padHex(salt)), usernamePasswordHash, ]).arrayBuffer()); const { g, N, k } = await getConstants(); const gModPowXN = modPow(g, arrayBufferToBigInt(x), N); const int = BigInt(`0x${srpBHex}`) - k * gModPowXN; const s = modPow(int, smallA + arrayBufferToBigInt(u) * arrayBufferToBigInt(x), N); const ikmHex = padHex(s.toString(16)); const saltHkdfHex = padHex(arrayBufferToHex(u)); const infoBits = new Uint8Array([ ..."Caldera Derived Key".split("").map((c) => c.charCodeAt(0)), 1, ]).buffer; const prkKey = await crypto.subtle.importKey("raw", hexToArrayBuffer(saltHkdfHex), { name: "HMAC", hash: { name: "SHA-256" }, }, false, ["sign"]); const prk = await crypto.subtle.sign("HMAC", prkKey, hexToArrayBuffer(ikmHex)); const hkdfKey = await crypto.subtle.importKey("raw", prk, { name: "HMAC", hash: { name: "SHA-256" }, }, false, ["sign"]); const hkdf = (await crypto.subtle.sign("HMAC", hkdfKey, infoBits)).slice(0, 16); const timestamp = formatDate(new Date()); const parts = [ userPoolName.split("").map((c) => c.charCodeAt(0)), username.split("").map((c) => c.charCodeAt(0)), ...bufferFromBase64(secretBlock), timestamp.split("").map((c) => c.charCodeAt(0)), ].flat(); const msg = new Uint8Array(parts).buffer; const signatureKey = await crypto.subtle.importKey("raw", hkdf, { name: "HMAC", hash: { name: "SHA-256" }, }, false, ["sign"]); const signatureString = await crypto.subtle.sign("HMAC", signatureKey, msg); return { timestamp, passwordClaimSignature: bufferToBase64(signatureString), }; } function hexToArrayBuffer(hexStr) { if (hexStr.length % 2 !== 0) { throw new Error("hex string should have even number of characters"); } const octets = hexStr.match(/.{2}/gi).map((m) => parseInt(m, 16)); return new Uint8Array(octets); } function arrayBufferToHex(arrBuf) { return [...new Uint8Array(arrBuf)] .map((x) => x.toString(16).padStart(2, "0")) .join(""); } function arrayBufferToBigInt(arrBuf) { return BigInt(`0x${arrayBufferToHex(arrBuf)}`); } function formatDate(d) { const parts = new Intl.DateTimeFormat("en-US", { weekday: "short", year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit", timeZone: "UTC", timeZoneName: "short", hour12: false, }).formatToParts(d); const p = (type) => parts.find((part) => part.type === type)?.value; return [ p("weekday"), p("month"), p("day"), [p("hour"), p("minute"), p("second")].join(":"), p("timeZoneName"), p("year"), ].join(" "); } export function authenticateWithSRP({ username, password, smsMfaCode, newPassword, customChallengeAnswer, authflow = "USER_SRP_AUTH", tokensCb, statusCb, clientMetadata, }) { const { userPoolId, debug } = configure(); if (!userPoolId) { throw new Error("UserPoolId must be configured"); } const abort = new AbortController(); const signedIn = (async () => { try { statusCb?.("SIGNING_IN_WITH_PASSWORD"); const smallA = generateSmallA(); const largeAHex = await calculateLargeAHex(smallA); debug?.(`Invoking initiateAuth ...`); const challenge = await initiateAuth({ authflow, authParameters: { SRP_A: largeAHex, USERNAME: username, CHALLENGE_NAME: "SRP_A", }, clientMetadata, abort: abort.signal, }); debug?.(`Response from initiateAuth:`, challenge); assertIsChallengeResponse(challenge); const { SALT: saltHex, SRP_B: srpBHex, SECRET_BLOCK: secretBlockB64, USER_ID_FOR_SRP: userIdForSrp, } = challenge.ChallengeParameters; const { passwordClaimSignature, timestamp } = await calculateSrpSignature({ smallA, largeAHex, srpBHex, salt: saltHex, username: userIdForSrp, userPoolId, password, secretBlock: secretBlockB64, }); debug?.(`Invoking respondToAuthChallenge ...`); const authResult = await respondToAuthChallenge({ challengeName: challenge.ChallengeName, challengeResponses: { USERNAME: username, PASSWORD_CLAIM_SECRET_BLOCK: secretBlockB64, TIMESTAMP: timestamp, PASSWORD_CLAIM_SIGNATURE: passwordClaimSignature, }, clientMetadata, session: challenge.Session, abort: abort.signal, }); debug?.(`Response from respondToAuthChallenge:`, authResult); const tokens = await handleAuthResponse({ authResponse: authResult, username, smsMfaCode, newPassword, customChallengeAnswer, clientMetadata, abort: abort.signal, }); tokensCb ? await tokensCb(tokens) : await defaultTokensCb({ tokens, abort: abort.signal }); statusCb?.("SIGNED_IN_WITH_PASSWORD"); return tokens; } catch (err) { statusCb?.("PASSWORD_SIGNIN_FAILED"); throw err; } })(); return { signedIn, abort: () => abort.abort(), }; }