@things-factory/auth-base 10.1.250 → 10.1.254
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -6,6 +6,7 @@ const koa_router_1 = tslib_1.__importDefault(require("koa-router"));
|
|
|
6
6
|
const env_1 = require("@things-factory/env");
|
|
7
7
|
const passkey_js_1 = require("../controllers/passkey.js");
|
|
8
8
|
const confirm_identity_js_1 = require("../controllers/confirm-identity.js");
|
|
9
|
+
const auth_error_js_1 = require("../errors/auth-error.js");
|
|
9
10
|
const signin_js_1 = require("../controllers/signin.js");
|
|
10
11
|
const identity_confirmation_js_1 = require("../service/identity-confirmation/identity-confirmation.js");
|
|
11
12
|
const access_token_cookie_js_1 = require("../utils/access-token-cookie.js");
|
|
@@ -86,15 +87,27 @@ exports.webAuthnGlobalPublicRouter.get('/auth/signin-webauthn/challenge', async
|
|
|
86
87
|
(0, passkey_js_1.rememberChallenge)(context, options.challenge);
|
|
87
88
|
context.body = options;
|
|
88
89
|
});
|
|
89
|
-
/*
|
|
90
|
+
/*
|
|
91
|
+
* The sign-in screen posts this as a form, the way it posts a password: `response` is the browser's
|
|
92
|
+
* answer as JSON text, `redirectTo` where to go after. So a refusal takes the password's way too -
|
|
93
|
+
* `authenticate401Middleware` answers the page with the one that follows from the code (a locked
|
|
94
|
+
* account to the activate page, which sends the unlock mail again), never a page inside a fetch body.
|
|
95
|
+
*/
|
|
90
96
|
exports.webAuthnGlobalPublicRouter.post('/auth/signin-webauthn', async (context) => {
|
|
91
97
|
const challenge = (0, passkey_js_1.takeChallenge)(context);
|
|
92
98
|
const { response, redirectTo } = context.request.body || {};
|
|
93
99
|
const { domain } = context.state;
|
|
94
|
-
const { token } = await (0, passkey_js_1.signinWithPasskey)(response, challenge, (0, passkey_js_1.siteOf)(context), context);
|
|
100
|
+
const { token } = await (0, passkey_js_1.signinWithPasskey)(passkeyAnswerOf(response), challenge, (0, passkey_js_1.siteOf)(context), context);
|
|
95
101
|
(0, access_token_cookie_js_1.setAccessTokenCookie)(context, token);
|
|
96
|
-
|
|
97
|
-
const redirectURL = `/auth/checkin${domain ? '/' + domain.subdomain : ''}?redirect_to=${encodeURIComponent(redirectTo || '/')}`;
|
|
98
|
-
context.body = { redirectURL, verified: true };
|
|
102
|
+
context.redirect(`/auth/checkin${domain ? '/' + domain.subdomain : ''}?redirect_to=${encodeURIComponent(redirectTo || '/')}`);
|
|
99
103
|
});
|
|
104
|
+
/* The browser's answer from the form field. Text that is not one is a failed verification, like any other bad answer. */
|
|
105
|
+
function passkeyAnswerOf(field) {
|
|
106
|
+
try {
|
|
107
|
+
return JSON.parse(String(field));
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
throw new auth_error_js_1.AuthError({ errorCode: auth_error_js_1.AuthError.ERROR_CODES.AUTHN_VERIFICATION_FAILED, detail: { reason: 'unreadable answer' } });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
100
113
|
//# sourceMappingURL=webauthn-router.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webauthn-router.js","sourceRoot":"","sources":["../../server/router/webauthn-router.ts"],"names":[],"mappings":"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","sourcesContent":["import Router from 'koa-router'\nimport { appPackage, config } from '@things-factory/env'\n\nimport {\n registerPasskey,\n registrationOptions,\n rememberChallenge,\n reverifyOptions,\n reverifyWithPasskey,\n signinOptions,\n signinWithPasskey,\n siteOf,\n takeChallenge\n} from '../controllers/passkey.js'\nimport { confirmIdentity } from '../controllers/confirm-identity.js'\nimport { verifyPassword } from '../controllers/signin.js'\nimport { confirmedUntil, forgetConfirmation } from '../service/identity-confirmation/identity-confirmation.js'\nimport { setAccessTokenCookie } from '../utils/access-token-cookie.js'\nimport { sameOriginRedirectPath } from '../utils/same-origin-redirect.js'\n\nexport const webAuthnGlobalPublicRouter = new Router()\nexport const webAuthnGlobalPrivateRouter = new Router()\n\nconst { name: rpName } = appPackage as any\n\nconst disableUserFavoredLanguage = config.get('i18n/disableUserFavoredLanguage', false)\nconst languages = config.get('i18n/languages', false)\n\n/*\n * A refusal throws `AuthError`; `authenticate401Middleware` answers it with 401 and the refusal in\n * the request's language as the body. The screens read that body as the message.\n */\n\n// Confirming it is the signed-in person - only their own passkeys can answer\nwebAuthnGlobalPrivateRouter.get('/auth/verify-webauthn/challenge', async context => {\n const options = await reverifyOptions(context.state.user, siteOf(context))\n\n if (!options) {\n context.status = 400\n context.body = { error: 'No biometric credentials registered for this user' }\n return\n }\n\n rememberChallenge(context, options.challenge)\n context.body = options\n})\n\n/* A passkey that answers is also the recent confirmation a door may ask for (ADR-0168). */\nwebAuthnGlobalPrivateRouter.post('/auth/verify-webauthn', async context => {\n const challenge = takeChallenge(context)\n await confirmIdentity('passkey', context, user => reverifyWithPasskey(user, context.request.body, challenge, siteOf(context)))\n\n context.body = { verified: true, message: 'Biometric authentication successful' }\n})\n\n/* The same confirmation by password - the body is `{ password }` (ADR-0168). */\nwebAuthnGlobalPrivateRouter.post('/auth/confirm-identity', async context => {\n const { password } = context.request.body || {}\n await confirmIdentity('password', context, user => verifyPassword(user, String(password ?? ''), context))\n\n context.body = { confirmed: true }\n})\n\n/* Until when the confirmation holds - a screen puts away what it shows at that moment. Null when there is none. */\nwebAuthnGlobalPrivateRouter.get('/auth/confirm-identity', async context => {\n context.body = { confirmedUntil: confirmedUntil(context, context.state.user?.id)?.toISOString() ?? null }\n})\n\n/* Letting the confirmation go before it ends by itself. */\nwebAuthnGlobalPrivateRouter.delete('/auth/confirm-identity', async context => {\n forgetConfirmation(context)\n context.body = { confirmed: false }\n})\n\n// Adding a passkey for the signed-in person\nwebAuthnGlobalPrivateRouter.get('/auth/register-webauthn/challenge', async context => {\n const options = await registrationOptions(context.state.user, rpName, siteOf(context))\n\n rememberChallenge(context, options.challenge)\n context.body = options\n})\n\n/* The body is `{ response, name }` - the browser's answer, and the label the screen proposes. */\nwebAuthnGlobalPrivateRouter.post('/auth/verify-registration', async context => {\n const challenge = takeChallenge(context)\n const { response, name } = context.request.body || {}\n\n await registerPasskey(context.state.user, response, name, challenge, siteOf(context))\n\n context.body = { verified: true }\n})\n\n/*\n * After a password sign-in on the screen, before check-in: an offer to add a passkey on this device\n * (ADR-0156 decision 5). The page moves on by itself where it has nothing to offer.\n */\nwebAuthnGlobalPrivateRouter.get('/auth/passkey-offer', async context => {\n await context.render('auth-page', {\n pageElement: 'auth-passkey-offer',\n elementScript: '/auth/passkey-offer.js',\n data: {\n username: context.state.user.username,\n redirectTo: sameOriginRedirectPath(context.query.redirect_to, context),\n disableUserFavoredLanguage,\n languages\n }\n })\n})\n\n// Signing in\nwebAuthnGlobalPublicRouter.get('/auth/signin-webauthn/challenge', async context => {\n const options = await signinOptions(siteOf(context))\n\n rememberChallenge(context, options.challenge)\n context.body = options\n})\n\n/* The body is `{ response, redirectTo }`. */\nwebAuthnGlobalPublicRouter.post('/auth/signin-webauthn', async context => {\n const challenge = takeChallenge(context)\n const { response, redirectTo } = context.request.body || {}\n const { domain } = context.state\n\n const { token } = await signinWithPasskey(response, challenge, siteOf(context), context)\n setAccessTokenCookie(context, token)\n\n /* A two-step exchange run by fetch in the browser cannot follow a 3xx, so the next address is data. */\n const redirectURL = `/auth/checkin${domain ? '/' + domain.subdomain : ''}?redirect_to=${encodeURIComponent(redirectTo || '/')}`\n\n context.body = { redirectURL, verified: true }\n})\n"]}
|
|
1
|
+
{"version":3,"file":"webauthn-router.js","sourceRoot":"","sources":["../../server/router/webauthn-router.ts"],"names":[],"mappings":";;;;AAAA,oEAA+B;AAC/B,6CAAwD;AAExD,0DAUkC;AAClC,4EAAoE;AACpE,2DAAmD;AACnD,wDAAyD;AACzD,wGAA8G;AAC9G,4EAAsE;AACtE,8EAAyE;AAE5D,QAAA,0BAA0B,GAAG,IAAI,oBAAM,EAAE,CAAA;AACzC,QAAA,2BAA2B,GAAG,IAAI,oBAAM,EAAE,CAAA;AAEvD,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,gBAAiB,CAAA;AAE1C,MAAM,0BAA0B,GAAG,YAAM,CAAC,GAAG,CAAC,iCAAiC,EAAE,KAAK,CAAC,CAAA;AACvF,MAAM,SAAS,GAAG,YAAM,CAAC,GAAG,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAA;AAErD;;;GAGG;AAEH,6EAA6E;AAC7E,mCAA2B,CAAC,GAAG,CAAC,iCAAiC,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;IACjF,MAAM,OAAO,GAAG,MAAM,IAAA,4BAAe,EAAC,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,IAAA,mBAAM,EAAC,OAAO,CAAC,CAAC,CAAA;IAE1E,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,CAAC,MAAM,GAAG,GAAG,CAAA;QACpB,OAAO,CAAC,IAAI,GAAG,EAAE,KAAK,EAAE,mDAAmD,EAAE,CAAA;QAC7E,OAAM;IACR,CAAC;IAED,IAAA,8BAAiB,EAAC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAA;IAC7C,OAAO,CAAC,IAAI,GAAG,OAAO,CAAA;AACxB,CAAC,CAAC,CAAA;AAEF,2FAA2F;AAC3F,mCAA2B,CAAC,IAAI,CAAC,uBAAuB,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;IACxE,MAAM,SAAS,GAAG,IAAA,0BAAa,EAAC,OAAO,CAAC,CAAA;IACxC,MAAM,IAAA,qCAAe,EAAC,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,IAAA,gCAAmB,EAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,EAAE,IAAA,mBAAM,EAAC,OAAO,CAAC,CAAC,CAAC,CAAA;IAE9H,OAAO,CAAC,IAAI,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,qCAAqC,EAAE,CAAA;AACnF,CAAC,CAAC,CAAA;AAEF,gFAAgF;AAChF,mCAA2B,CAAC,IAAI,CAAC,wBAAwB,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;IACzE,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,CAAA;IAC/C,MAAM,IAAA,qCAAe,EAAC,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,IAAA,0BAAc,EAAC,IAAI,EAAE,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,CAAA;IAEzG,OAAO,CAAC,IAAI,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,CAAA;AACpC,CAAC,CAAC,CAAA;AAEF,mHAAmH;AACnH,mCAA2B,CAAC,GAAG,CAAC,wBAAwB,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;IACxE,OAAO,CAAC,IAAI,GAAG,EAAE,cAAc,EAAE,IAAA,yCAAc,EAAC,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,WAAW,EAAE,IAAI,IAAI,EAAE,CAAA;AAC3G,CAAC,CAAC,CAAA;AAEF,2DAA2D;AAC3D,mCAA2B,CAAC,MAAM,CAAC,wBAAwB,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;IAC3E,IAAA,6CAAkB,EAAC,OAAO,CAAC,CAAA;IAC3B,OAAO,CAAC,IAAI,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,CAAA;AACrC,CAAC,CAAC,CAAA;AAEF,4CAA4C;AAC5C,mCAA2B,CAAC,GAAG,CAAC,mCAAmC,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;IACnF,MAAM,OAAO,GAAG,MAAM,IAAA,gCAAmB,EAAC,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAA,mBAAM,EAAC,OAAO,CAAC,CAAC,CAAA;IAEtF,IAAA,8BAAiB,EAAC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAA;IAC7C,OAAO,CAAC,IAAI,GAAG,OAAO,CAAA;AACxB,CAAC,CAAC,CAAA;AAEF,iGAAiG;AACjG,mCAA2B,CAAC,IAAI,CAAC,2BAA2B,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;IAC5E,MAAM,SAAS,GAAG,IAAA,0BAAa,EAAC,OAAO,CAAC,CAAA;IACxC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,CAAA;IAErD,MAAM,IAAA,4BAAe,EAAC,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,IAAA,mBAAM,EAAC,OAAO,CAAC,CAAC,CAAA;IAErF,OAAO,CAAC,IAAI,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;AACnC,CAAC,CAAC,CAAA;AAEF;;;GAGG;AACH,mCAA2B,CAAC,GAAG,CAAC,qBAAqB,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;IACrE,MAAM,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE;QAChC,WAAW,EAAE,oBAAoB;QACjC,aAAa,EAAE,wBAAwB;QACvC,IAAI,EAAE;YACJ,QAAQ,EAAE,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ;YACrC,UAAU,EAAE,IAAA,gDAAsB,EAAC,OAAO,CAAC,KAAK,CAAC,WAAW,EAAE,OAAO,CAAC;YACtE,0BAA0B;YAC1B,SAAS;SACV;KACF,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,aAAa;AACb,kCAA0B,CAAC,GAAG,CAAC,iCAAiC,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;IAChF,MAAM,OAAO,GAAG,MAAM,IAAA,0BAAa,EAAC,IAAA,mBAAM,EAAC,OAAO,CAAC,CAAC,CAAA;IAEpD,IAAA,8BAAiB,EAAC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAA;IAC7C,OAAO,CAAC,IAAI,GAAG,OAAO,CAAA;AACxB,CAAC,CAAC,CAAA;AAEF;;;;;GAKG;AACH,kCAA0B,CAAC,IAAI,CAAC,uBAAuB,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;IACvE,MAAM,SAAS,GAAG,IAAA,0BAAa,EAAC,OAAO,CAAC,CAAA;IACxC,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,CAAA;IAC3D,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAEhC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,IAAA,8BAAiB,EAAC,eAAe,CAAC,QAAQ,CAAC,EAAE,SAAS,EAAE,IAAA,mBAAM,EAAC,OAAO,CAAC,EAAE,OAAO,CAAC,CAAA;IACzG,IAAA,6CAAoB,EAAC,OAAO,EAAE,KAAK,CAAC,CAAA;IAEpC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,MAAM,CAAC,CAAC,CAAC,GAAG,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,gBAAgB,kBAAkB,CAAC,UAAU,IAAI,GAAG,CAAC,EAAE,CAAC,CAAA;AAC/H,CAAC,CAAC,CAAA;AAEF,yHAAyH;AACzH,SAAS,eAAe,CAAC,KAAc;IACrC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,yBAAS,CAAC,EAAE,SAAS,EAAE,yBAAS,CAAC,WAAW,CAAC,yBAAyB,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,mBAAmB,EAAE,EAAE,CAAC,CAAA;IAC9H,CAAC;AACH,CAAC","sourcesContent":["import Router from 'koa-router'\nimport { appPackage, config } from '@things-factory/env'\n\nimport {\n registerPasskey,\n registrationOptions,\n rememberChallenge,\n reverifyOptions,\n reverifyWithPasskey,\n signinOptions,\n signinWithPasskey,\n siteOf,\n takeChallenge\n} from '../controllers/passkey.js'\nimport { confirmIdentity } from '../controllers/confirm-identity.js'\nimport { AuthError } from '../errors/auth-error.js'\nimport { verifyPassword } from '../controllers/signin.js'\nimport { confirmedUntil, forgetConfirmation } from '../service/identity-confirmation/identity-confirmation.js'\nimport { setAccessTokenCookie } from '../utils/access-token-cookie.js'\nimport { sameOriginRedirectPath } from '../utils/same-origin-redirect.js'\n\nexport const webAuthnGlobalPublicRouter = new Router()\nexport const webAuthnGlobalPrivateRouter = new Router()\n\nconst { name: rpName } = appPackage as any\n\nconst disableUserFavoredLanguage = config.get('i18n/disableUserFavoredLanguage', false)\nconst languages = config.get('i18n/languages', false)\n\n/*\n * A refusal throws `AuthError`; `authenticate401Middleware` answers it with 401 and the refusal in\n * the request's language as the body. The screens read that body as the message.\n */\n\n// Confirming it is the signed-in person - only their own passkeys can answer\nwebAuthnGlobalPrivateRouter.get('/auth/verify-webauthn/challenge', async context => {\n const options = await reverifyOptions(context.state.user, siteOf(context))\n\n if (!options) {\n context.status = 400\n context.body = { error: 'No biometric credentials registered for this user' }\n return\n }\n\n rememberChallenge(context, options.challenge)\n context.body = options\n})\n\n/* A passkey that answers is also the recent confirmation a door may ask for (ADR-0168). */\nwebAuthnGlobalPrivateRouter.post('/auth/verify-webauthn', async context => {\n const challenge = takeChallenge(context)\n await confirmIdentity('passkey', context, user => reverifyWithPasskey(user, context.request.body, challenge, siteOf(context)))\n\n context.body = { verified: true, message: 'Biometric authentication successful' }\n})\n\n/* The same confirmation by password - the body is `{ password }` (ADR-0168). */\nwebAuthnGlobalPrivateRouter.post('/auth/confirm-identity', async context => {\n const { password } = context.request.body || {}\n await confirmIdentity('password', context, user => verifyPassword(user, String(password ?? ''), context))\n\n context.body = { confirmed: true }\n})\n\n/* Until when the confirmation holds - a screen puts away what it shows at that moment. Null when there is none. */\nwebAuthnGlobalPrivateRouter.get('/auth/confirm-identity', async context => {\n context.body = { confirmedUntil: confirmedUntil(context, context.state.user?.id)?.toISOString() ?? null }\n})\n\n/* Letting the confirmation go before it ends by itself. */\nwebAuthnGlobalPrivateRouter.delete('/auth/confirm-identity', async context => {\n forgetConfirmation(context)\n context.body = { confirmed: false }\n})\n\n// Adding a passkey for the signed-in person\nwebAuthnGlobalPrivateRouter.get('/auth/register-webauthn/challenge', async context => {\n const options = await registrationOptions(context.state.user, rpName, siteOf(context))\n\n rememberChallenge(context, options.challenge)\n context.body = options\n})\n\n/* The body is `{ response, name }` - the browser's answer, and the label the screen proposes. */\nwebAuthnGlobalPrivateRouter.post('/auth/verify-registration', async context => {\n const challenge = takeChallenge(context)\n const { response, name } = context.request.body || {}\n\n await registerPasskey(context.state.user, response, name, challenge, siteOf(context))\n\n context.body = { verified: true }\n})\n\n/*\n * After a password sign-in on the screen, before check-in: an offer to add a passkey on this device\n * (ADR-0156 decision 5). The page moves on by itself where it has nothing to offer.\n */\nwebAuthnGlobalPrivateRouter.get('/auth/passkey-offer', async context => {\n await context.render('auth-page', {\n pageElement: 'auth-passkey-offer',\n elementScript: '/auth/passkey-offer.js',\n data: {\n username: context.state.user.username,\n redirectTo: sameOriginRedirectPath(context.query.redirect_to, context),\n disableUserFavoredLanguage,\n languages\n }\n })\n})\n\n// Signing in\nwebAuthnGlobalPublicRouter.get('/auth/signin-webauthn/challenge', async context => {\n const options = await signinOptions(siteOf(context))\n\n rememberChallenge(context, options.challenge)\n context.body = options\n})\n\n/*\n * The sign-in screen posts this as a form, the way it posts a password: `response` is the browser's\n * answer as JSON text, `redirectTo` where to go after. So a refusal takes the password's way too -\n * `authenticate401Middleware` answers the page with the one that follows from the code (a locked\n * account to the activate page, which sends the unlock mail again), never a page inside a fetch body.\n */\nwebAuthnGlobalPublicRouter.post('/auth/signin-webauthn', async context => {\n const challenge = takeChallenge(context)\n const { response, redirectTo } = context.request.body || {}\n const { domain } = context.state\n\n const { token } = await signinWithPasskey(passkeyAnswerOf(response), challenge, siteOf(context), context)\n setAccessTokenCookie(context, token)\n\n context.redirect(`/auth/checkin${domain ? '/' + domain.subdomain : ''}?redirect_to=${encodeURIComponent(redirectTo || '/')}`)\n})\n\n/* The browser's answer from the form field. Text that is not one is a failed verification, like any other bad answer. */\nfunction passkeyAnswerOf(field: unknown) {\n try {\n return JSON.parse(String(field))\n } catch {\n throw new AuthError({ errorCode: AuthError.ERROR_CODES.AUTHN_VERIFICATION_FAILED, detail: { reason: 'unreadable answer' } })\n }\n}\n"]}
|