From 53d8f5ea4d0e29364e08d41b0a943397cc462c99 Mon Sep 17 00:00:00 2001 From: jrmccannon Date: Tue, 18 Jun 2024 11:56:18 -0500 Subject: [PATCH] Added new testing ability for manually generated tokens and magic links. Showing claims on hello world to show which claims the user has. --- .../Pages/Account/Login.cshtml | 5 +- .../Pages/Account/Logout.cshtml.cs | 7 + .../Pages/Account/Recovery.cshtml | 11 +- .../Pages/Account/Recovery.cshtml.cs | 39 +- .../Pages/Authorized/HelloWorld.cshtml | 4 +- .../Pages/Authorized/HelloWorld.cshtml.cs | 23 +- .../Pages/Authorized/Stepup.cshtml | 5 +- .../Pages/Shared/_Layout.cshtml | 1 - .../wwwroot/js/site.js | 716 +++++++++--------- 9 files changed, 437 insertions(+), 374 deletions(-) diff --git a/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Login.cshtml b/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Login.cshtml index 2d6c7cf..0c3545f 100644 --- a/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Login.cshtml +++ b/examples/Passwordless.AspNetIdentity.Example/Pages/Account/Login.cshtml @@ -33,12 +33,11 @@ @if (canLogin) { + - @await RenderSectionAsync("Scripts", required: false) diff --git a/examples/Passwordless.AspNetIdentity.Example/wwwroot/js/site.js b/examples/Passwordless.AspNetIdentity.Example/wwwroot/js/site.js index 6e51ff9..b7e9afb 100644 --- a/examples/Passwordless.AspNetIdentity.Example/wwwroot/js/site.js +++ b/examples/Passwordless.AspNetIdentity.Example/wwwroot/js/site.js @@ -1,390 +1,398 @@ -export class Client { - constructor(config) { - this.config = { - apiUrl: 'https://v4.passwordless.dev', - apiKey: '', - origin: window.location.origin, - rpid: window.location.hostname - }; - this.abortController = new AbortController(); - Object.assign(this.config, config); - } - /** - * Register a new credential to a user - * - * @param {string} token Token generated by your backend and the Passwordless API - * @param {string} credentialNickname A nickname for the passkey credential being created - */ - async register(token, credentialNickname) { - var _a; - try { - this.assertBrowserSupported(); - const registration = await this.registerBegin(token); - if (registration.error) { - console.error(registration.error); - return { error: registration.error }; +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Passwordless = {})); +}(this, (function (exports) { 'use strict'; + + class Client { + constructor(config) { + this.config = { + apiUrl: 'https://v4.passwordless.dev', + apiKey: '', + origin: window.location.origin, + rpid: window.location.hostname + }; + this.abortController = new AbortController(); + Object.assign(this.config, config); + } + /** + * Register a new credential to a user + * + * @param {string} token Token generated by your backend and the Passwordless API + * @param {string} credentialNickname A nickname for the passkey credential being created + */ + async register(token, credentialNickname) { + var _a; + try { + this.assertBrowserSupported(); + const registration = await this.registerBegin(token); + if (registration.error) { + console.error(registration.error); + return { error: registration.error }; + } + registration.data.challenge = base64UrlToArrayBuffer(registration.data.challenge); + registration.data.user.id = base64UrlToArrayBuffer(registration.data.user.id); + (_a = registration.data.excludeCredentials) === null || _a === void 0 ? void 0 : _a.forEach((cred) => { + cred.id = base64UrlToArrayBuffer(cred.id); + }); + const credential = (await navigator.credentials.create({ + publicKey: registration.data + })); + if (!credential) { + const error = { + from: 'client', + errorCode: 'failed_create_credential', + title: 'Failed to create credential (navigator.credentials.create returned null)' + }; + console.error(error); + return { error }; + } + return await this.registerComplete(credential, registration.session, credentialNickname); + // next steps + // return a token from the API + // Add a type to the token (method/action) } - registration.data.challenge = base64UrlToArrayBuffer(registration.data.challenge); - registration.data.user.id = base64UrlToArrayBuffer(registration.data.user.id); - (_a = registration.data.excludeCredentials) === null || _a === void 0 ? void 0 : _a.forEach((cred) => { - cred.id = base64UrlToArrayBuffer(cred.id); - }); - const credential = (await navigator.credentials.create({ - publicKey: registration.data - })); - if (!credential) { + catch (caughtError) { + const errorMessage = getErrorMessage(caughtError); const error = { from: 'client', - errorCode: 'failed_create_credential', - title: 'Failed to create credential (navigator.credentials.create returned null)' + errorCode: 'unknown', + title: errorMessage }; + console.error(caughtError); console.error(error); return { error }; } - return await this.registerComplete(credential, registration.session, credentialNickname); - // next steps - // return a token from the API - // Add a type to the token (method/action) } - catch (caughtError) { - const errorMessage = getErrorMessage(caughtError); - const error = { - from: 'client', - errorCode: 'unknown', - title: errorMessage - }; - console.error(caughtError); - console.error(error); - return { error }; + /** + * Sign in a user using the userid + * @param {string} userId + * @returns + */ + async signinWithId(userId) { + return this.signin({ userId }); } - } - /** - * Sign in a user using the userid - * @param {string} userId - * @returns - */ - async signinWithId(userId) { - return this.signin({ userId }); - } - /** - * Sign in a user using an alias - * @param {string} alias - * @returns a verify_token - */ - async signinWithAlias(alias) { - return this.signin({ alias }); - } - /** - * Sign in a user using autofill UI (a.k.a conditional) sign in - * @returns a verify_token - */ - async signinWithAutofill() { - if (!(await isAutofillSupported())) { - throw new Error('Autofill authentication (conditional meditation) is not supported in this browser'); + /** + * Sign in a user using an alias + * @param {string} alias + * @returns a verify_token + */ + async signinWithAlias(alias) { + return this.signin({ alias }); } - return this.signin({ autofill: true }); - } - /** - * Sign in a user using discoverable credentials - * @returns a verify_token - */ - async signinWithDiscoverable() { - return this.signin({ discoverable: true }); - } - abort() { - if (this.abortController) { - this.abortController.abort(); + /** + * Sign in a user using autofill UI (a.k.a conditional) sign in + * @returns a verify_token + */ + async signinWithAutofill() { + if (!(await isAutofillSupported())) { + throw new Error('Autofill authentication (conditional meditation) is not supported in this browser'); + } + return this.signin({ autofill: true }); } - } - isPlatformSupported() { - return isPlatformSupported(); - } - isBrowserSupported() { - return isBrowserSupported(); - } - isAutofillSupported() { - return isAutofillSupported(); - } - async registerBegin(token) { - const response = await fetch(`${this.config.apiUrl}/register/begin`, { - method: 'POST', - headers: this.createHeaders(), - body: JSON.stringify({ - token, - RPID: this.config.rpid, - Origin: this.config.origin - }) - }); - const res = await response.json(); - if (response.ok) { - return res; + /** + * Sign in a user using discoverable credentials + * @returns a verify_token + */ + async signinWithDiscoverable() { + return this.signin({ discoverable: true }); } - return { error: { ...res, from: 'server' } }; - } - async registerComplete(credential, session, credentialNickname) { - const attestationResponse = credential.response; - const response = await fetch(`${this.config.apiUrl}/register/complete`, { - method: 'POST', - headers: this.createHeaders(), - body: JSON.stringify({ - session: session, - response: { - id: credential.id, - rawId: arrayBufferToBase64Url(credential.rawId), - type: credential.type, - clientExtensionResults: credential.getClientExtensionResults(), - response: { - AttestationObject: arrayBufferToBase64Url(attestationResponse.attestationObject), - clientDataJson: arrayBufferToBase64Url(attestationResponse.clientDataJSON) - } - }, - nickname: credentialNickname, - RPID: this.config.rpid, - Origin: this.config.origin - }) - }); - const res = await response.json(); - if (response.ok) { - return res; + abort() { + if (this.abortController) { + this.abortController.abort(); + } } - return { error: { ...res, from: 'server' } }; - } - /** - * Sign in a user - * - * @param {SigninMethod} Object containing either UserID or Alias - * @returns - */ - async signin(signinMethod) { - try { - this.assertBrowserSupported(); - this.handleAbort(); - // if signinMethod is undefined, set it to an empty object - // this will cause a login using discoverable credentials - if (!signinMethod) { - signinMethod = { discoverable: true }; + isPlatformSupported() { + return isPlatformSupported(); + } + isBrowserSupported() { + return isBrowserSupported(); + } + isAutofillSupported() { + return isAutofillSupported(); + } + async registerBegin(token) { + const response = await fetch(`${this.config.apiUrl}/register/begin`, { + method: 'POST', + headers: this.createHeaders(), + body: JSON.stringify({ + token, + RPID: this.config.rpid, + Origin: this.config.origin + }) + }); + const res = await response.json(); + if (response.ok) { + return res; } - const signin = await this.signinBegin(signinMethod); - if (signin.error) { - return signin; + return { error: { ...res, from: 'server' } }; + } + async registerComplete(credential, session, credentialNickname) { + const attestationResponse = credential.response; + const response = await fetch(`${this.config.apiUrl}/register/complete`, { + method: 'POST', + headers: this.createHeaders(), + body: JSON.stringify({ + session: session, + response: { + id: credential.id, + rawId: arrayBufferToBase64Url(credential.rawId), + type: credential.type, + clientExtensionResults: credential.getClientExtensionResults(), + response: { + AttestationObject: arrayBufferToBase64Url(attestationResponse.attestationObject), + clientDataJson: arrayBufferToBase64Url(attestationResponse.clientDataJSON) + } + }, + nickname: credentialNickname, + RPID: this.config.rpid, + Origin: this.config.origin + }) + }); + const res = await response.json(); + if (response.ok) { + return res; } - const credential = (await navigator.credentials.get({ - publicKey: signin.data, - mediation: 'autofill' in signinMethod - ? 'conditional' - : undefined, // Typescript doesn't know about 'conditational' yet - signal: this.abortController.signal - })); - const response = await this.signinComplete(credential, signin.session); - return response; + return { error: { ...res, from: 'server' } }; } - catch (caughtError) { - const errorMessage = getErrorMessage(caughtError); - const error = { - from: 'client', - errorCode: 'unknown', - title: errorMessage - }; - console.error(caughtError); - console.error(error); - return { error }; + /** + * Sign in a user + * + * @param {SigninMethod} Object containing either UserID or Alias + * @returns + */ + async signin(signinMethod) { + try { + this.assertBrowserSupported(); + this.handleAbort(); + // if signinMethod is undefined, set it to an empty object + // this will cause a login using discoverable credentials + if (!signinMethod) { + signinMethod = { discoverable: true }; + } + const signin = await this.signinBegin(signinMethod); + if (signin.error) { + return signin; + } + const credential = (await navigator.credentials.get({ + publicKey: signin.data, + mediation: 'autofill' in signinMethod + ? 'conditional' + : undefined, // Typescript doesn't know about 'conditational' yet + signal: this.abortController.signal + })); + const response = await this.signinComplete(credential, signin.session); + return response; + } + catch (caughtError) { + const errorMessage = getErrorMessage(caughtError); + const error = { + from: 'client', + errorCode: 'unknown', + title: errorMessage + }; + console.error(caughtError); + console.error(error); + return { error }; + } } - } - /** - * Performs a step-up authentication process. This is essentially an overload for the sign-in workflow. It allows for - * a user authentication to be given a purpose or context for the sign-in, enabling a "step-up" authentication flow. - * - * @param {StepupRequest} stepup - The step-up request object. This includes the sign-in method and the purpose of the authentication - * - * @returns {token} - The result of the step-up sign-in process. - */ - async stepup(stepup, purpose) { - try { - this.assertBrowserSupported(); - this.handleAbort(); - if (!stepup.signinMethod) { - throw new Error('You need to provide the signinMethod'); + /** + * Performs a step-up authentication process. This is essentially an overload for the sign-in workflow. It allows for + * a user authentication to be given a purpose or context for the sign-in, enabling a "step-up" authentication flow. + * + * @param {StepupRequest} stepup - The step-up request object. This includes the sign-in method and the purpose of the authentication + * + * @returns {token} - The result of the step-up sign-in process. + */ + async stepup(stepup) { + try { + this.assertBrowserSupported(); + this.handleAbort(); + if (!stepup.signinMethod) { + throw new Error('You need to provide the signinMethod'); + } + if (!stepup.purpose) { + stepup.purpose = 'step-up'; + } + const signin = await this.signinBegin(stepup.signinMethod, stepup.purpose); + if (signin.error) { + return signin; + } + const credential = (await navigator.credentials.get({ + publicKey: signin.data, + mediation: 'autofill' in stepup.signinMethod + ? 'conditional' + : undefined, // Typescript doesn't know about 'conditional' yet + signal: this.abortController.signal + })); + return await this.signinComplete(credential, signin.session); } - if (!stepup.purpose) { - stepup.purpose = 'step-up'; + catch (caughtError) { + const errorMessage = getErrorMessage(caughtError); + const error = { + from: 'client', + errorCode: 'unknown', + title: errorMessage + }; + console.error(caughtError); + console.error(error); + return { error }; } - const signin = await this.signinBegin(stepup.signinMethod, stepup.purpose); - if (signin.error) { - return signin; + } + async signinBegin(signinMethod, purpose) { + var _a; + const response = await fetch(`${this.config.apiUrl}/signin/begin`, { + method: 'POST', + headers: this.createHeaders(), + body: JSON.stringify({ + userId: 'userId' in signinMethod ? signinMethod.userId : undefined, + alias: 'alias' in signinMethod ? signinMethod.alias : undefined, + RPID: this.config.rpid, + Origin: this.config.origin, + purpose: purpose + }) + }); + const res = await response.json(); + if (response.ok) { + return { + ...res, + data: { + ...res.data, + challenge: base64UrlToArrayBuffer(res.data.challenge), + allowCredentials: (_a = res.data.allowCredentials) === null || _a === void 0 ? void 0 : _a.map((cred) => { + return { ...cred, id: base64UrlToArrayBuffer(cred.id) }; + }) + } + }; } - const credential = (await navigator.credentials.get({ - publicKey: signin.data, - mediation: 'autofill' in stepup.signinMethod - ? 'conditional' - : undefined, // Typescript doesn't know about 'conditional' yet - signal: this.abortController.signal - })); - return await this.signinComplete(credential, signin.session); + return { error: { ...res, from: 'server' } }; } - catch (caughtError) { - const errorMessage = getErrorMessage(caughtError); - const error = { - from: 'client', - errorCode: 'unknown', - title: errorMessage - }; - console.error(caughtError); - console.error(error); - return { error }; + async signinComplete(credential, session) { + const assertionResponse = credential.response; + const response = await fetch(`${this.config.apiUrl}/signin/complete`, { + method: 'POST', + headers: this.createHeaders(), + body: JSON.stringify({ + session: session, + response: { + id: credential.id, + rawId: arrayBufferToBase64Url(new Uint8Array(credential.rawId)), + type: credential.type, + clientExtensionResults: credential.getClientExtensionResults(), + response: { + authenticatorData: arrayBufferToBase64Url(assertionResponse.authenticatorData), + clientDataJson: arrayBufferToBase64Url(assertionResponse.clientDataJSON), + signature: arrayBufferToBase64Url(assertionResponse.signature) + } + }, + RPID: this.config.rpid, + Origin: this.config.origin + }) + }); + const res = await response.json(); + if (response.ok) { + return res; + } + return { error: { ...res, from: 'server' } }; } - } - async signinBegin(signinMethod, purpose) { - var _a; - const response = await fetch(`${this.config.apiUrl}/signin/begin`, { - method: 'POST', - headers: this.createHeaders(), - body: JSON.stringify({ - userId: 'userId' in signinMethod ? signinMethod.userId : undefined, - alias: 'alias' in signinMethod ? signinMethod.alias : undefined, - RPID: this.config.rpid, - Origin: this.config.origin, - purpose: purpose - }) - }); - const res = await response.json(); - if (response.ok) { + handleAbort() { + this.abort(); + this.abortController = new AbortController(); + } + assertBrowserSupported() { + if (!isBrowserSupported()) { + throw new Error('WebAuthn and PublicKeyCredentials are not supported on this browser/device'); + } + } + createHeaders() { return { - ...res, - data: { - ...res.data, - challenge: base64UrlToArrayBuffer(res.data.challenge), - allowCredentials: (_a = res.data.allowCredentials) === null || _a === void 0 ? void 0 : _a.map((cred) => { - return { ...cred, id: base64UrlToArrayBuffer(cred.id) }; - }) - } + 'ApiKey': this.config.apiKey, + 'Content-Type': 'application/json', + 'Client-Version': 'js-1.1.0' }; } - return { error: { ...res, from: 'server' } }; } - async signinComplete(credential, session) { - const assertionResponse = credential.response; - const response = await fetch(`${this.config.apiUrl}/signin/complete`, { - method: 'POST', - headers: this.createHeaders(), - body: JSON.stringify({ - session: session, - response: { - id: credential.id, - rawId: arrayBufferToBase64Url(new Uint8Array(credential.rawId)), - type: credential.type, - clientExtensionResults: credential.getClientExtensionResults(), - response: { - authenticatorData: arrayBufferToBase64Url(assertionResponse.authenticatorData), - clientDataJson: arrayBufferToBase64Url(assertionResponse.clientDataJSON), - signature: arrayBufferToBase64Url(assertionResponse.signature) - } - }, - RPID: this.config.rpid, - Origin: this.config.origin - }) - }); - const res = await response.json(); - if (response.ok) { - return res; - } - return { error: { ...res, from: 'server' } }; + async function isPlatformSupported() { + if (!isBrowserSupported()) + return false; + return PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); } - handleAbort() { - this.abort(); - this.abortController = new AbortController(); + function isBrowserSupported() { + return (window.PublicKeyCredential !== undefined && typeof window.PublicKeyCredential === 'function'); } - assertBrowserSupported() { - if (!isBrowserSupported()) { - throw new Error('WebAuthn and PublicKeyCredentials are not supported on this browser/device'); - } + async function isAutofillSupported() { + const PublicKeyCredential = window.PublicKeyCredential; // Typescript lacks support for this + if (!PublicKeyCredential.isConditionalMediationAvailable) + return false; + return PublicKeyCredential.isConditionalMediationAvailable(); } - createHeaders() { - return { - 'ApiKey': this.config.apiKey, - 'Content-Type': 'application/json', - 'Client-Version': 'js-1.1.0' - }; + function base64ToBase64Url(base64) { + return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=*$/g, ''); } -} -async function isPlatformSupported() { - if (!isBrowserSupported()) - return false; - return PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); -} -function isBrowserSupported() { - return (window.PublicKeyCredential !== undefined && typeof window.PublicKeyCredential === 'function'); -} -async function isAutofillSupported() { - const PublicKeyCredential = window.PublicKeyCredential; // Typescript lacks support for this - if (!PublicKeyCredential.isConditionalMediationAvailable) - return false; - return PublicKeyCredential.isConditionalMediationAvailable(); -} -function base64ToBase64Url(base64) { - return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=*$/g, ''); -} -function base64UrlToBase64(base64Url) { - return base64Url.replace(/-/g, '+').replace(/_/g, '/'); -} -function base64UrlToArrayBuffer(base64UrlString) { - // improvement: Remove BufferSource-type and add proper types upstream - if (typeof base64UrlString !== 'string') { - const msg = 'Cannot convert from Base64Url to ArrayBuffer: Input was not of type string'; - console.error(msg, base64UrlString); - throw new TypeError(msg); + function base64UrlToBase64(base64Url) { + return base64Url.replace(/-/g, '+').replace(/_/g, '/'); } - const base64Unpadded = base64UrlToBase64(base64UrlString); - const paddingNeeded = (4 - (base64Unpadded.length % 4)) % 4; - const base64Padded = base64Unpadded.padEnd(base64Unpadded.length + paddingNeeded, '='); - const binary = window.atob(base64Padded); - const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i++) { - bytes[i] = binary.charCodeAt(i); + function base64UrlToArrayBuffer(base64UrlString) { + // improvement: Remove BufferSource-type and add proper types upstream + if (typeof base64UrlString !== 'string') { + const msg = 'Cannot convert from Base64Url to ArrayBuffer: Input was not of type string'; + console.error(msg, base64UrlString); + throw new TypeError(msg); + } + const base64Unpadded = base64UrlToBase64(base64UrlString); + const paddingNeeded = (4 - (base64Unpadded.length % 4)) % 4; + const base64Padded = base64Unpadded.padEnd(base64Unpadded.length + paddingNeeded, '='); + const binary = window.atob(base64Padded); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; } - return bytes; -} -function arrayBufferToBase64Url(buffer) { - const uint8Array = (() => { - if (Array.isArray(buffer)) - return Uint8Array.from(buffer); - if (buffer instanceof ArrayBuffer) - return new Uint8Array(buffer); - if (buffer instanceof Uint8Array) - return buffer; - const msg = 'Cannot convert from ArrayBuffer to Base64Url. Input was not of type ArrayBuffer, Uint8Array or Array'; - console.error(msg, buffer); - throw new Error(msg); - })(); - let string = ''; - for (let i = 0; i < uint8Array.byteLength; i++) { - string += String.fromCharCode(uint8Array[i]); + function arrayBufferToBase64Url(buffer) { + const uint8Array = (() => { + if (Array.isArray(buffer)) + return Uint8Array.from(buffer); + if (buffer instanceof ArrayBuffer) + return new Uint8Array(buffer); + if (buffer instanceof Uint8Array) + return buffer; + const msg = 'Cannot convert from ArrayBuffer to Base64Url. Input was not of type ArrayBuffer, Uint8Array or Array'; + console.error(msg, buffer); + throw new Error(msg); + })(); + let string = ''; + for (let i = 0; i < uint8Array.byteLength; i++) { + string += String.fromCharCode(uint8Array[i]); + } + const base64String = window.btoa(string); + return base64ToBase64Url(base64String); } - const base64String = window.btoa(string); - return base64ToBase64Url(base64String); -} -function isErrorWithMessage(error) { - return (typeof error === 'object' && - error !== null && - 'message' in error && - typeof error.message === 'string'); -} -function toErrorWithMessage(maybeError) { - if (isErrorWithMessage(maybeError)) - return maybeError; - try { - return new Error(JSON.stringify(maybeError)); + function isErrorWithMessage(error) { + return (typeof error === 'object' && + error !== null && + 'message' in error && + typeof error.message === 'string'); } - catch (_a) { - // fallback in case there's an error stringifying the maybeError - // like with circular references for example. - return new Error(String(maybeError)); + function toErrorWithMessage(maybeError) { + if (isErrorWithMessage(maybeError)) + return maybeError; + try { + return new Error(JSON.stringify(maybeError)); + } + catch (_a) { + // fallback in case there's an error stringifying the maybeError + // like with circular references for example. + return new Error(String(maybeError)); + } } -} -function getErrorMessage(error) { - return toErrorWithMessage(error).message; -} + function getErrorMessage(error) { + return toErrorWithMessage(error).message; + } + + exports.Client = Client; + exports.isAutofillSupported = isAutofillSupported; + exports.isBrowserSupported = isBrowserSupported; + exports.isPlatformSupported = isPlatformSupported; -exports.Client = Client; -exports.isAutofillSupported = isAutofillSupported; -exports.isBrowserSupported = isBrowserSupported; -exports.isPlatformSupported = isPlatformSupported; +})));