@byline/admin 6.7.2 → 6.9.0
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.
- package/dist/fields/blocks/blocks-field.js +33 -3
- package/dist/fields/blocks/blocks-field.module.js +1 -0
- package/dist/fields/blocks/blocks-field_module.css +19 -0
- package/dist/forms/form-page-chrome.d.ts +3 -2
- package/dist/forms/form-page-chrome.js +13 -1
- package/dist/forms/form-renderer.d.ts +2 -1
- package/dist/forms/form-renderer.js +23 -17
- package/dist/forms/unavailable-blocks.d.ts +15 -0
- package/dist/forms/unavailable-blocks.js +38 -0
- package/dist/modules/admin-account/components/change-password.js +1 -1
- package/dist/modules/admin-account/components/container.js +18 -3
- package/dist/modules/admin-account/components/container.module.js +2 -0
- package/dist/modules/admin-account/components/container_module.css +7 -0
- package/dist/modules/admin-account/components/preferences.js +1 -1
- package/dist/modules/admin-account/components/update.js +1 -1
- package/dist/modules/admin-roles/components/create.js +1 -1
- package/dist/modules/admin-roles/components/permissions.js +1 -1
- package/dist/modules/admin-roles/components/update.js +1 -1
- package/dist/modules/admin-users/abilities.d.ts +1 -0
- package/dist/modules/admin-users/abilities.js +8 -1
- package/dist/modules/admin-users/components/create.js +1 -1
- package/dist/modules/admin-users/components/roles.js +1 -1
- package/dist/modules/admin-users/components/set-password.js +1 -1
- package/dist/modules/admin-users/components/update.js +1 -1
- package/dist/modules/auth/auth-maintenance-repository.d.ts +31 -0
- package/dist/modules/auth/auth-maintenance-repository.js +1 -0
- package/dist/modules/auth/auth-maintenance.d.ts +11 -0
- package/dist/modules/auth/auth-maintenance.js +39 -0
- package/dist/modules/auth/auth-maintenance.test.node.d.ts +1 -0
- package/dist/modules/auth/components/passkey-browser.d.ts +14 -0
- package/dist/modules/auth/components/passkey-browser.js +118 -0
- package/dist/modules/auth/components/passkey-browser.test.node.d.ts +1 -0
- package/dist/modules/auth/components/passkey-manager.d.ts +26 -0
- package/dist/modules/auth/components/passkey-manager.js +406 -0
- package/dist/modules/auth/components/passkey-manager.module.js +22 -0
- package/dist/modules/auth/components/passkey-manager_module.css +104 -0
- package/dist/modules/auth/components/sign-in-form.js +62 -3
- package/dist/modules/auth/components/sign-in-form.module.js +5 -1
- package/dist/modules/auth/components/sign-in-form_module.css +40 -0
- package/dist/modules/auth/index.d.ts +5 -0
- package/dist/modules/auth/index.js +3 -0
- package/dist/modules/auth/jwt-session-provider.d.ts +12 -4
- package/dist/modules/auth/jwt-session-provider.js +135 -94
- package/dist/modules/auth/native-passkey-provider.d.ts +72 -0
- package/dist/modules/auth/native-passkey-provider.js +618 -0
- package/dist/modules/auth/passkey-rate-limiter.d.ts +14 -0
- package/dist/modules/auth/passkey-rate-limiter.js +60 -0
- package/dist/modules/auth/passkey-rate-limiter.test.node.d.ts +8 -0
- package/dist/modules/auth/passkey-storage-validation.d.ts +14 -0
- package/dist/modules/auth/passkey-storage-validation.js +59 -0
- package/dist/modules/auth/passkeys-repository.d.ts +96 -0
- package/dist/modules/auth/passkeys-repository.js +1 -0
- package/dist/modules/auth/refresh-tokens-repository.d.ts +1 -1
- package/dist/modules/auth/sign-in-rate-limiter.d.ts +2 -0
- package/dist/modules/auth/sign-in-rate-limiter.js +1 -1
- package/dist/services/admin-services-context.d.ts +1 -1
- package/dist/services/admin-services-types.d.ts +60 -0
- package/dist/store.d.ts +4 -0
- package/package.json +16 -9
- package/src/fields/blocks/blocks-field.module.css +23 -0
- package/src/fields/blocks/blocks-field.tsx +26 -2
- package/src/forms/form-page-chrome.tsx +19 -1
- package/src/forms/form-renderer-capabilities.test.tsx +6 -6
- package/src/forms/form-renderer.tsx +24 -15
- package/src/forms/unavailable-blocks.test.tsx +152 -0
- package/src/forms/unavailable-blocks.tsx +58 -0
- package/src/modules/admin-account/components/change-password.tsx +1 -1
- package/src/modules/admin-account/components/container.module.css +9 -0
- package/src/modules/admin-account/components/container.tsx +12 -3
- package/src/modules/admin-account/components/preferences.tsx +1 -1
- package/src/modules/admin-account/components/update.tsx +1 -1
- package/src/modules/admin-roles/components/create.tsx +1 -1
- package/src/modules/admin-roles/components/permissions.tsx +1 -1
- package/src/modules/admin-roles/components/update.tsx +1 -1
- package/src/modules/admin-users/abilities.ts +7 -0
- package/src/modules/admin-users/components/create.tsx +1 -1
- package/src/modules/admin-users/components/roles.tsx +1 -1
- package/src/modules/admin-users/components/set-password.tsx +1 -1
- package/src/modules/admin-users/components/update.tsx +1 -1
- package/src/modules/auth/auth-maintenance-repository.ts +26 -0
- package/src/modules/auth/auth-maintenance.test.node.ts +110 -0
- package/src/modules/auth/auth-maintenance.ts +62 -0
- package/src/modules/auth/components/passkey-browser.test.node.ts +188 -0
- package/src/modules/auth/components/passkey-browser.ts +139 -0
- package/src/modules/auth/components/passkey-manager.module.css +100 -0
- package/src/modules/auth/components/passkey-manager.test.tsx +189 -0
- package/src/modules/auth/components/passkey-manager.tsx +386 -0
- package/src/modules/auth/components/sign-in-form.module.css +37 -0
- package/src/modules/auth/components/sign-in-form.test.tsx +92 -0
- package/src/modules/auth/components/sign-in-form.tsx +65 -4
- package/src/modules/auth/index.ts +15 -0
- package/src/modules/auth/jwt-session-provider.ts +116 -44
- package/src/modules/auth/native-passkey-provider.ts +710 -0
- package/src/modules/auth/passkey-rate-limiter.test.node.ts +83 -0
- package/src/modules/auth/passkey-rate-limiter.ts +76 -0
- package/src/modules/auth/passkey-storage-validation.ts +108 -0
- package/src/modules/auth/passkeys-repository.ts +97 -0
- package/src/modules/auth/refresh-tokens-repository.ts +1 -1
- package/src/modules/auth/sign-in-rate-limiter.ts +1 -1
- package/src/services/admin-services-context.tsx +2 -0
- package/src/services/admin-services-types.ts +60 -0
- package/src/store.ts +4 -0
|
@@ -1,20 +1,52 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState } from "react";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
4
|
import { getAdminConfig } from "@byline/core";
|
|
5
5
|
import { useTranslation } from "@byline/i18n/react";
|
|
6
|
-
import { Alert, Button, Card, Input, InputPassword, LoaderEllipsis } from "@byline/ui/react";
|
|
6
|
+
import { Alert, Button, Card, Input, InputPassword, LoaderEllipsis, PasskeyIcon } from "@byline/ui/react";
|
|
7
7
|
import clsx from "clsx";
|
|
8
8
|
import { useBylineAdminServices } from "../../../services/admin-services-context.js";
|
|
9
9
|
import { normalizeRootRelativeRedirect, resolveSignInFormRedirect } from "../safe-redirect.js";
|
|
10
|
+
import { passkeyErrorKey, signInWithPasskey } from "./passkey-browser.js";
|
|
10
11
|
import sign_in_form_module from "./sign-in-form.module.js";
|
|
11
12
|
function SignInForm({ redirectTo, homeUrl }) {
|
|
12
|
-
const { adminSignIn } = useBylineAdminServices();
|
|
13
|
+
const { adminSignIn, passkeys } = useBylineAdminServices();
|
|
13
14
|
const { t } = useTranslation('byline-admin');
|
|
14
15
|
const [email, setEmail] = useState('');
|
|
15
16
|
const [password, setPassword] = useState('');
|
|
16
17
|
const [pending, setPending] = useState(false);
|
|
17
18
|
const [error, setError] = useState(null);
|
|
19
|
+
const mounted = useRef(false);
|
|
20
|
+
const [passkeysEnabled, setPasskeysEnabled] = useState(false);
|
|
21
|
+
useEffect(()=>{
|
|
22
|
+
let active = true;
|
|
23
|
+
mounted.current = true;
|
|
24
|
+
passkeys?.capability().then(({ enabled })=>{
|
|
25
|
+
if (active) setPasskeysEnabled(enabled);
|
|
26
|
+
}).catch(()=>{});
|
|
27
|
+
return ()=>{
|
|
28
|
+
active = false;
|
|
29
|
+
mounted.current = false;
|
|
30
|
+
};
|
|
31
|
+
}, [
|
|
32
|
+
passkeys
|
|
33
|
+
]);
|
|
34
|
+
async function handlePasskey() {
|
|
35
|
+
if (!passkeys || pending) return;
|
|
36
|
+
setPending(true);
|
|
37
|
+
setError(null);
|
|
38
|
+
try {
|
|
39
|
+
await signInWithPasskey(passkeys, ()=>mounted.current);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (mounted.current) {
|
|
42
|
+
setError(t(passkeyErrorKey(error)));
|
|
43
|
+
setPending(false);
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const destination = resolveSignInFormRedirect(redirectTo, getAdminConfig().routes.admin);
|
|
48
|
+
window.location.assign(normalizeRootRelativeRedirect(destination) ?? '/');
|
|
49
|
+
}
|
|
18
50
|
async function handleSubmit(event) {
|
|
19
51
|
event.preventDefault();
|
|
20
52
|
if (pending) return;
|
|
@@ -115,6 +147,33 @@ function SignInForm({ redirectTo, homeUrl }) {
|
|
|
115
147
|
})
|
|
116
148
|
]
|
|
117
149
|
})
|
|
150
|
+
}),
|
|
151
|
+
passkeysEnabled && /*#__PURE__*/ jsxs(Card.Footer, {
|
|
152
|
+
className: clsx('byline-sign-in-alternate', sign_in_form_module.alternate),
|
|
153
|
+
children: [
|
|
154
|
+
/*#__PURE__*/ jsx("div", {
|
|
155
|
+
className: clsx('byline-sign-in-divider', sign_in_form_module.divider),
|
|
156
|
+
children: /*#__PURE__*/ jsx("span", {
|
|
157
|
+
children: t('auth.signIn.alternate')
|
|
158
|
+
})
|
|
159
|
+
}),
|
|
160
|
+
/*#__PURE__*/ jsxs(Button, {
|
|
161
|
+
type: "button",
|
|
162
|
+
variant: "outlined",
|
|
163
|
+
disabled: pending,
|
|
164
|
+
onClick: handlePasskey,
|
|
165
|
+
className: clsx('byline-passkey-sign-in-button', sign_in_form_module["passkey-button"]),
|
|
166
|
+
children: [
|
|
167
|
+
/*#__PURE__*/ jsx(PasskeyIcon, {
|
|
168
|
+
width: "20px",
|
|
169
|
+
height: "20px"
|
|
170
|
+
}),
|
|
171
|
+
/*#__PURE__*/ jsx("span", {
|
|
172
|
+
children: t('passkeys.signIn')
|
|
173
|
+
})
|
|
174
|
+
]
|
|
175
|
+
})
|
|
176
|
+
]
|
|
118
177
|
})
|
|
119
178
|
]
|
|
120
179
|
});
|
|
@@ -7,6 +7,10 @@ const sign_in_form_module = {
|
|
|
7
7
|
actions: "actions-bVKfoJ",
|
|
8
8
|
"home-link": "home-link-YfGVyd",
|
|
9
9
|
homeLink: "home-link-YfGVyd",
|
|
10
|
-
button: "button-paNjW_"
|
|
10
|
+
button: "button-paNjW_",
|
|
11
|
+
alternate: "alternate-bC6QfW",
|
|
12
|
+
divider: "divider-QyDRIH",
|
|
13
|
+
"passkey-button": "passkey-button-LpEiWc",
|
|
14
|
+
passkeyButton: "passkey-button-LpEiWc"
|
|
11
15
|
};
|
|
12
16
|
export default sign_in_form_module;
|
|
@@ -39,3 +39,43 @@
|
|
|
39
39
|
margin-left: auto;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
:is(.alternate-bC6QfW, .byline-sign-in-alternate) {
|
|
43
|
+
align-items: stretch;
|
|
44
|
+
gap: var(--spacing-16);
|
|
45
|
+
padding-bottom: var(--spacing-24);
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
:is(.divider-QyDRIH, .byline-sign-in-divider) {
|
|
50
|
+
align-items: center;
|
|
51
|
+
gap: var(--spacing-12);
|
|
52
|
+
color: var(--gray-500);
|
|
53
|
+
font-size: var(--font-size-xs);
|
|
54
|
+
text-transform: uppercase;
|
|
55
|
+
letter-spacing: .04em;
|
|
56
|
+
display: flex;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.divider-QyDRIH:before, .divider-QyDRIH:after {
|
|
60
|
+
content: "";
|
|
61
|
+
border-top: var(--border-width-thin) var(--border-style-solid) var(--border-color, #8885);
|
|
62
|
+
flex: 1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.byline-sign-in-divider:before {
|
|
66
|
+
content: "";
|
|
67
|
+
border-top: var(--border-width-thin) var(--border-style-solid) var(--border-color, #8885);
|
|
68
|
+
flex: 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.byline-sign-in-divider:after {
|
|
72
|
+
content: "";
|
|
73
|
+
border-top: var(--border-width-thin) var(--border-style-solid) var(--border-color, #8885);
|
|
74
|
+
flex: 1;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
:is(.passkey-button-LpEiWc, .byline-passkey-sign-in-button) {
|
|
78
|
+
gap: var(--spacing-8);
|
|
79
|
+
width: 100%;
|
|
80
|
+
}
|
|
81
|
+
|
|
@@ -19,9 +19,14 @@
|
|
|
19
19
|
* Clerk, institutional SSO) should be shipped as separate packages
|
|
20
20
|
* against `@byline/auth` rather than added here.
|
|
21
21
|
*/
|
|
22
|
+
export { createAuthMaintenanceTasks } from './auth-maintenance.js';
|
|
22
23
|
export { JwtSessionProvider, type JwtSessionProviderConfig, type NativeSessionEvent, } from './jwt-session-provider.js';
|
|
24
|
+
export { createPasskeyRequestLimiter } from './passkey-rate-limiter.js';
|
|
25
|
+
export { assertPasskeyAttempt, assertPasskeyBytes, assertPasskeyClaim, assertPasskeyTokenHash, } from './passkey-storage-validation.js';
|
|
23
26
|
export { hashPassword, verifyPassword } from './password.js';
|
|
24
27
|
export { resolveActor } from './resolve-actor.js';
|
|
25
28
|
export { createPasswordSignInLimiter, type SignInLimiterOptions, type SignInRateLimitPolicy, type SignInRateLimitStore, type SignInSecurityEvent, } from './sign-in-rate-limiter.js';
|
|
29
|
+
export type { AuthMaintenanceRepository } from './auth-maintenance-repository.js';
|
|
26
30
|
export type { LoginSessionRow, LoginSessionsRepository } from './login-sessions-repository.js';
|
|
31
|
+
export type { PasskeyAttemptRow, PasskeyClaimInput, PasskeyCredentialRow, PasskeysRepository, } from './passkeys-repository.js';
|
|
27
32
|
export type { IssueRefreshTokenInput, RefreshTokenRow, RefreshTokensRepository, } from './refresh-tokens-repository.js';
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
export { createAuthMaintenanceTasks } from "./auth-maintenance.js";
|
|
1
2
|
export { JwtSessionProvider } from "./jwt-session-provider.js";
|
|
3
|
+
export { createPasskeyRequestLimiter } from "./passkey-rate-limiter.js";
|
|
4
|
+
export { assertPasskeyAttempt, assertPasskeyBytes, assertPasskeyClaim, assertPasskeyTokenHash } from "./passkey-storage-validation.js";
|
|
2
5
|
export { hashPassword, verifyPassword } from "./password.js";
|
|
3
6
|
export { resolveActor } from "./resolve-actor.js";
|
|
4
7
|
export { createPasswordSignInLimiter } from "./sign-in-rate-limiter.js";
|
|
@@ -5,13 +5,20 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
-
import { type AdminAuth, type RefreshSessionArgs, type RevokeSessionArgs, type SessionProvider, type SessionProviderCapabilities, type SessionTokens, type SignInResult, type SignInWithPasswordArgs } from '@byline/auth';
|
|
8
|
+
import { type AdminAuth, type PasskeyProvider, type RefreshSessionArgs, type RevokeSessionArgs, type SessionProvider, type SessionProviderCapabilities, type SessionTokens, type SignInResult, type SignInWithPasswordArgs } from '@byline/auth';
|
|
9
|
+
import type { PasskeyConfig } from '@byline/core';
|
|
9
10
|
import type { AdminStore } from '../../store.js';
|
|
10
|
-
export
|
|
11
|
+
export type NativeSessionEvent = {
|
|
11
12
|
type: 'refresh_attempted' | 'refresh_completed' | 'refresh_contested';
|
|
12
|
-
}
|
|
13
|
+
} | {
|
|
14
|
+
type: 'passkey_attempted' | 'passkey_completed' | 'passkey_failed' | 'passkey_admission_denied';
|
|
15
|
+
purpose: 'sign-in-options' | 'sign-in' | 'password-proof' | 'proof-options' | 'proof' | 'enrollment-options' | 'enrollment' | 'list' | 'rename' | 'remove';
|
|
16
|
+
reason?: 'invalid-proof' | 'stale-authority' | 'forbidden' | 'limit' | 'unavailable' | 'capacity' | 'network';
|
|
17
|
+
};
|
|
13
18
|
export interface JwtSessionProviderConfig {
|
|
14
|
-
/**
|
|
19
|
+
/** Optional explicit WebAuthn trust configuration; disabled when absent. */
|
|
20
|
+
passkeys?: PasskeyConfig;
|
|
21
|
+
/** Best-effort, token-free authentication telemetry. Errors never change authentication outcomes. */
|
|
15
22
|
onEvent?: (event: NativeSessionEvent) => void;
|
|
16
23
|
/**
|
|
17
24
|
* Adapter-backed admin repositories. Construct via the DB adapter's
|
|
@@ -40,6 +47,7 @@ export interface JwtSessionProviderConfig {
|
|
|
40
47
|
export declare class JwtSessionProvider implements SessionProvider {
|
|
41
48
|
#private;
|
|
42
49
|
readonly capabilities: SessionProviderCapabilities;
|
|
50
|
+
readonly passkeys?: PasskeyProvider;
|
|
43
51
|
constructor(config: JwtSessionProviderConfig);
|
|
44
52
|
signInWithPassword(args: SignInWithPasswordArgs): Promise<SignInResult>;
|
|
45
53
|
verifyAccessToken(token: string): Promise<{
|
|
@@ -3,6 +3,7 @@ import { ERR_ACCESS_EXPIRED, ERR_ACCOUNT_DISABLED, ERR_INVALID_CREDENTIALS, ERR_
|
|
|
3
3
|
import { passwordSignInSchema } from "@byline/core/validation";
|
|
4
4
|
import { SignJWT, compactVerify, jwtVerify } from "jose";
|
|
5
5
|
import { v7 } from "uuid";
|
|
6
|
+
import { NativePasskeyProvider } from "./native-passkey-provider.js";
|
|
6
7
|
import { verifyPassword } from "./password.js";
|
|
7
8
|
import { resolveActor, resolveActorFromUser } from "./resolve-actor.js";
|
|
8
9
|
function _check_private_redeclaration(obj, privateCollection) {
|
|
@@ -52,11 +53,11 @@ const CAPABILITIES = {
|
|
|
52
53
|
magicLink: false,
|
|
53
54
|
sso: false
|
|
54
55
|
};
|
|
55
|
-
var _onEvent = /*#__PURE__*/ new WeakMap(),
|
|
56
|
+
var _onEvent = /*#__PURE__*/ new WeakMap(), jwt_session_provider_store = /*#__PURE__*/ new WeakMap(), _signingKey = /*#__PURE__*/ new WeakMap(), _issuer = /*#__PURE__*/ new WeakMap(), _accessTtl = /*#__PURE__*/ new WeakMap(), _refreshTtl = /*#__PURE__*/ new WeakMap(), _now = /*#__PURE__*/ new WeakMap(), _verifyAccessTokenAtStore = /*#__PURE__*/ new WeakSet(), _emit = /*#__PURE__*/ new WeakSet(), _emitEvent = /*#__PURE__*/ new WeakSet(), _authenticatePasskeySession = /*#__PURE__*/ new WeakSet(), _completeSignIn = /*#__PURE__*/ new WeakSet(), _observedLogins = /*#__PURE__*/ new WeakSet(), _issueTokens = /*#__PURE__*/ new WeakSet(), _signAccessToken = /*#__PURE__*/ new WeakSet();
|
|
56
57
|
class JwtSessionProvider {
|
|
57
58
|
async signInWithPassword(args) {
|
|
58
59
|
const credentials = passwordSignInSchema.parse(args);
|
|
59
|
-
const users = _class_private_field_get(this,
|
|
60
|
+
const users = _class_private_field_get(this, jwt_session_provider_store).adminUsers;
|
|
60
61
|
const row = await users.getByEmailForSignIn(credentials.email);
|
|
61
62
|
if (!row) {
|
|
62
63
|
await verifyPassword(credentials.password, DUMMY_HASH_FOR_TIMING);
|
|
@@ -74,99 +75,27 @@ class JwtSessionProvider {
|
|
|
74
75
|
if (!row.is_enabled) throw ERR_ACCOUNT_DISABLED({
|
|
75
76
|
message: 'account disabled'
|
|
76
77
|
});
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
row.id,
|
|
80
|
-
...previous.map((login)=>login.admin_user_id)
|
|
81
|
-
], work) : _class_private_field_get(this, _store).withSessionLock(row.id, (store)=>work(store));
|
|
82
|
-
return withLocks(async (store)=>{
|
|
83
|
-
const current = await store.adminUsers.getByIdForSignIn(row.id);
|
|
84
|
-
if (!current?.is_enabled) throw ERR_ACCOUNT_DISABLED({
|
|
85
|
-
message: 'account disabled'
|
|
86
|
-
});
|
|
87
|
-
if (current.password_hash !== row.password_hash || current.session_version !== row.session_version) throw ERR_INVALID_CREDENTIALS({
|
|
78
|
+
return _class_private_method_get(this, _completeSignIn, completeSignIn).call(this, row, args, async (_store, current)=>{
|
|
79
|
+
if (current.password_hash !== row.password_hash) throw ERR_INVALID_CREDENTIALS({
|
|
88
80
|
message: 'credentials changed during sign-in'
|
|
89
81
|
});
|
|
90
|
-
for (const observed of previous){
|
|
91
|
-
const login = await store.loginSessions.findById(observed.id);
|
|
92
|
-
if (login && login.admin_user_id === observed.admin_user_id) await store.loginSessions.revoke(login.id, _class_private_field_get(this, _now).call(this));
|
|
93
|
-
}
|
|
94
|
-
await store.adminUsers.recordLoginSuccess(row.id, args.ip ?? null);
|
|
95
|
-
const actor = await resolveActorFromUser(store, current);
|
|
96
|
-
if (!actor) throw ERR_ACCOUNT_DISABLED({
|
|
97
|
-
message: 'account disabled'
|
|
98
|
-
});
|
|
99
|
-
const tokens = await _class_private_method_get(this, _issueTokens, issueTokens).call(this, store, {
|
|
100
|
-
adminUserId: row.id,
|
|
101
|
-
sessionVersion: current.session_version,
|
|
102
|
-
ip: args.ip ?? null,
|
|
103
|
-
userAgent: args.userAgent ?? null
|
|
104
|
-
});
|
|
105
|
-
return {
|
|
106
|
-
...tokens,
|
|
107
|
-
actor
|
|
108
|
-
};
|
|
109
82
|
});
|
|
110
83
|
}
|
|
111
84
|
async verifyAccessToken(token) {
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
const result = await jwtVerify(token, _class_private_field_get(this, _signingKey), {
|
|
115
|
-
issuer: _class_private_field_get(this, _issuer),
|
|
116
|
-
algorithms: [
|
|
117
|
-
'HS256'
|
|
118
|
-
],
|
|
119
|
-
currentDate: _class_private_field_get(this, _now).call(this),
|
|
120
|
-
requiredClaims: [
|
|
121
|
-
'sub',
|
|
122
|
-
'iat',
|
|
123
|
-
'exp',
|
|
124
|
-
'jti',
|
|
125
|
-
'sv',
|
|
126
|
-
'sid'
|
|
127
|
-
]
|
|
128
|
-
});
|
|
129
|
-
payload = result.payload;
|
|
130
|
-
} catch (err) {
|
|
131
|
-
if (err instanceof Error && 'code' in err && 'ERR_JWT_EXPIRED' === err.code) throw ERR_ACCESS_EXPIRED({
|
|
132
|
-
message: 'access token expired'
|
|
133
|
-
});
|
|
134
|
-
throw ERR_INVALID_TOKEN({
|
|
135
|
-
message: 'access token verification failed',
|
|
136
|
-
cause: err
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
if ('access' !== payload.typ || 'string' != typeof payload.sub || !validSid(payload.sid) || !Number.isSafeInteger(payload.sv) || payload.sv < 0) throw ERR_INVALID_TOKEN({
|
|
140
|
-
message: 'unexpected token type'
|
|
141
|
-
});
|
|
142
|
-
const current = await _class_private_field_get(this, _store).adminUsers.getByIdForSignIn(payload.sub);
|
|
143
|
-
if (!current?.is_enabled) throw ERR_ACCOUNT_DISABLED({
|
|
144
|
-
message: 'account disabled or deleted'
|
|
145
|
-
});
|
|
146
|
-
if (current.session_version !== payload.sv) throw ERR_REVOKED_TOKEN({
|
|
147
|
-
message: 'account session generation changed'
|
|
148
|
-
});
|
|
149
|
-
const login = await _class_private_field_get(this, _store).loginSessions.findById(payload.sid);
|
|
150
|
-
if (!login || login.admin_user_id !== payload.sub || login.session_version !== payload.sv || null != login.revoked_at || login.expires_at.getTime() <= _class_private_field_get(this, _now).call(this).getTime()) throw ERR_REVOKED_TOKEN({
|
|
151
|
-
message: 'login revoked or expired'
|
|
152
|
-
});
|
|
153
|
-
const actor = await resolveActorFromUser(_class_private_field_get(this, _store), current);
|
|
154
|
-
if (!actor) throw ERR_ACCOUNT_DISABLED({
|
|
155
|
-
message: 'account disabled or deleted'
|
|
156
|
-
});
|
|
85
|
+
const { actor, sessionId } = await _class_private_method_get(this, _verifyAccessTokenAtStore, verifyAccessTokenAtStore).call(this, token, _class_private_field_get(this, jwt_session_provider_store));
|
|
157
86
|
return {
|
|
158
87
|
actor,
|
|
159
|
-
sessionId
|
|
88
|
+
sessionId
|
|
160
89
|
};
|
|
161
90
|
}
|
|
162
91
|
async refreshSession(args) {
|
|
163
92
|
_class_private_method_get(this, _emit, emit).call(this, 'refresh_attempted');
|
|
164
93
|
const hash = hashToken(args.refreshToken);
|
|
165
|
-
const observed = await _class_private_field_get(this,
|
|
94
|
+
const observed = await _class_private_field_get(this, jwt_session_provider_store).refreshTokens.findByHash(hash);
|
|
166
95
|
if (!observed) throw ERR_INVALID_TOKEN({
|
|
167
96
|
message: 'refresh token not recognised'
|
|
168
97
|
});
|
|
169
|
-
const outcome = await _class_private_field_get(this,
|
|
98
|
+
const outcome = await _class_private_field_get(this, jwt_session_provider_store).withSessionLock(observed.admin_user_id, async (store, user)=>{
|
|
170
99
|
if (!user?.is_enabled) throw ERR_ACCOUNT_DISABLED({
|
|
171
100
|
message: 'account disabled or deleted'
|
|
172
101
|
});
|
|
@@ -242,21 +171,25 @@ class JwtSessionProvider {
|
|
|
242
171
|
if (args.expectedSessionId && logins.some((login)=>login.id !== args.expectedSessionId)) throw ERR_SESSION_CHANGED({
|
|
243
172
|
message: 'session changed before logout'
|
|
244
173
|
});
|
|
245
|
-
for (const login of logins)await _class_private_field_get(this,
|
|
174
|
+
for (const login of logins)await _class_private_field_get(this, jwt_session_provider_store).withSessionLock(login.admin_user_id, async (store)=>{
|
|
246
175
|
await store.loginSessions.revoke(login.id, _class_private_field_get(this, _now).call(this));
|
|
247
176
|
});
|
|
248
177
|
if (args.refreshToken) {
|
|
249
|
-
const row = await _class_private_field_get(this,
|
|
250
|
-
if (row) await _class_private_field_get(this,
|
|
178
|
+
const row = await _class_private_field_get(this, jwt_session_provider_store).refreshTokens.findByHash(hashToken(args.refreshToken));
|
|
179
|
+
if (row) await _class_private_field_get(this, jwt_session_provider_store).withSessionLock(row.admin_user_id, async (store)=>{
|
|
251
180
|
await store.refreshTokens.revokeChain(row.id, _class_private_field_get(this, _now).call(this));
|
|
252
181
|
});
|
|
253
182
|
}
|
|
254
183
|
}
|
|
255
184
|
async resolveActor(adminUserId) {
|
|
256
|
-
return resolveActor(_class_private_field_get(this,
|
|
185
|
+
return resolveActor(_class_private_field_get(this, jwt_session_provider_store), adminUserId);
|
|
257
186
|
}
|
|
258
187
|
constructor(config){
|
|
188
|
+
_class_private_method_init(this, _verifyAccessTokenAtStore);
|
|
259
189
|
_class_private_method_init(this, _emit);
|
|
190
|
+
_class_private_method_init(this, _emitEvent);
|
|
191
|
+
_class_private_method_init(this, _authenticatePasskeySession);
|
|
192
|
+
_class_private_method_init(this, _completeSignIn);
|
|
260
193
|
_class_private_method_init(this, _observedLogins);
|
|
261
194
|
_class_private_method_init(this, _issueTokens);
|
|
262
195
|
_class_private_method_init(this, _signAccessToken);
|
|
@@ -264,7 +197,7 @@ class JwtSessionProvider {
|
|
|
264
197
|
writable: true,
|
|
265
198
|
value: void 0
|
|
266
199
|
});
|
|
267
|
-
_class_private_field_init(this,
|
|
200
|
+
_class_private_field_init(this, jwt_session_provider_store, {
|
|
268
201
|
writable: true,
|
|
269
202
|
value: void 0
|
|
270
203
|
});
|
|
@@ -288,25 +221,133 @@ class JwtSessionProvider {
|
|
|
288
221
|
writable: true,
|
|
289
222
|
value: void 0
|
|
290
223
|
});
|
|
291
|
-
this.capabilities = CAPABILITIES;
|
|
292
224
|
_class_private_field_set(this, _onEvent, config.onEvent);
|
|
293
|
-
_class_private_field_set(this,
|
|
225
|
+
_class_private_field_set(this, jwt_session_provider_store, config.store);
|
|
294
226
|
_class_private_field_set(this, _signingKey, 'string' == typeof config.signingSecret ? new TextEncoder().encode(config.signingSecret) : config.signingSecret);
|
|
295
227
|
if (_class_private_field_get(this, _signingKey).byteLength < 32) throw new Error('JwtSessionProvider: signingSecret must carry at least 32 bytes of entropy (256 bits)');
|
|
296
228
|
_class_private_field_set(this, _issuer, config.issuer ?? DEFAULT_ISSUER);
|
|
297
229
|
_class_private_field_set(this, _accessTtl, config.accessTokenTtlSeconds ?? DEFAULT_ACCESS_TOKEN_TTL_SECONDS);
|
|
298
230
|
_class_private_field_set(this, _refreshTtl, config.refreshTokenTtlSeconds ?? DEFAULT_REFRESH_TOKEN_TTL_SECONDS);
|
|
299
231
|
_class_private_field_set(this, _now, config.now ?? (()=>new Date()));
|
|
232
|
+
this.capabilities = config.passkeys ? {
|
|
233
|
+
...CAPABILITIES,
|
|
234
|
+
passkeys: true
|
|
235
|
+
} : CAPABILITIES;
|
|
236
|
+
if (config.passkeys) this.passkeys = new NativePasskeyProvider(config.passkeys, {
|
|
237
|
+
store: _class_private_field_get(this, jwt_session_provider_store),
|
|
238
|
+
now: _class_private_field_get(this, _now),
|
|
239
|
+
authenticate: (session, store)=>_class_private_method_get(this, _authenticatePasskeySession, authenticatePasskeySession).call(this, session, store),
|
|
240
|
+
signIn: (snapshot, args, recheck)=>_class_private_method_get(this, _completeSignIn, completeSignIn).call(this, snapshot, args, recheck),
|
|
241
|
+
emit: (event)=>_class_private_method_get(this, _emitEvent, emitEvent).call(this, event)
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
async function verifyAccessTokenAtStore(token, store) {
|
|
246
|
+
let payload;
|
|
247
|
+
try {
|
|
248
|
+
const result = await jwtVerify(token, _class_private_field_get(this, _signingKey), {
|
|
249
|
+
issuer: _class_private_field_get(this, _issuer),
|
|
250
|
+
algorithms: [
|
|
251
|
+
'HS256'
|
|
252
|
+
],
|
|
253
|
+
currentDate: _class_private_field_get(this, _now).call(this),
|
|
254
|
+
requiredClaims: [
|
|
255
|
+
'sub',
|
|
256
|
+
'iat',
|
|
257
|
+
'exp',
|
|
258
|
+
'jti',
|
|
259
|
+
'sv',
|
|
260
|
+
'sid'
|
|
261
|
+
]
|
|
262
|
+
});
|
|
263
|
+
payload = result.payload;
|
|
264
|
+
} catch (err) {
|
|
265
|
+
if (err instanceof Error && 'code' in err && 'ERR_JWT_EXPIRED' === err.code) throw ERR_ACCESS_EXPIRED({
|
|
266
|
+
message: 'access token expired'
|
|
267
|
+
});
|
|
268
|
+
throw ERR_INVALID_TOKEN({
|
|
269
|
+
message: 'access token verification failed',
|
|
270
|
+
cause: err
|
|
271
|
+
});
|
|
300
272
|
}
|
|
273
|
+
if ('access' !== payload.typ || 'string' != typeof payload.sub || !validSid(payload.sid) || !Number.isSafeInteger(payload.sv) || payload.sv < 0) throw ERR_INVALID_TOKEN({
|
|
274
|
+
message: 'unexpected token type'
|
|
275
|
+
});
|
|
276
|
+
const current = await store.adminUsers.getByIdForSignIn(payload.sub);
|
|
277
|
+
if (!current?.is_enabled) throw ERR_ACCOUNT_DISABLED({
|
|
278
|
+
message: 'account disabled or deleted'
|
|
279
|
+
});
|
|
280
|
+
if (current.session_version !== payload.sv) throw ERR_REVOKED_TOKEN({
|
|
281
|
+
message: 'account session generation changed'
|
|
282
|
+
});
|
|
283
|
+
const login = await store.loginSessions.findById(payload.sid);
|
|
284
|
+
if (!login || login.admin_user_id !== payload.sub || login.session_version !== payload.sv || null != login.revoked_at || login.expires_at.getTime() <= _class_private_field_get(this, _now).call(this).getTime()) throw ERR_REVOKED_TOKEN({
|
|
285
|
+
message: 'login revoked or expired'
|
|
286
|
+
});
|
|
287
|
+
const actor = await resolveActorFromUser(store, current);
|
|
288
|
+
if (!actor) throw ERR_ACCOUNT_DISABLED({
|
|
289
|
+
message: 'account disabled or deleted'
|
|
290
|
+
});
|
|
291
|
+
return {
|
|
292
|
+
actor,
|
|
293
|
+
sessionId: payload.sid,
|
|
294
|
+
user: current
|
|
295
|
+
};
|
|
301
296
|
}
|
|
302
297
|
function emit(type) {
|
|
298
|
+
_class_private_method_get(this, _emitEvent, emitEvent).call(this, {
|
|
299
|
+
type
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
function emitEvent(event) {
|
|
303
303
|
var _this, _this1;
|
|
304
304
|
try {
|
|
305
|
-
null == (_this = _class_private_field_get(_this1 = this, _onEvent))
|
|
306
|
-
type
|
|
307
|
-
});
|
|
305
|
+
Promise.resolve(null == (_this = _class_private_field_get(_this1 = this, _onEvent)) ? void 0 : _this.call(_this1, event)).catch(()=>{});
|
|
308
306
|
} catch {}
|
|
309
307
|
}
|
|
308
|
+
async function authenticatePasskeySession(session, store = _class_private_field_get(this, jwt_session_provider_store)) {
|
|
309
|
+
const result = await _class_private_method_get(this, _verifyAccessTokenAtStore, verifyAccessTokenAtStore).call(this, session.accessToken, store);
|
|
310
|
+
if (result.sessionId !== session.expectedSessionId) throw ERR_SESSION_CHANGED({
|
|
311
|
+
message: 'session changed'
|
|
312
|
+
});
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
315
|
+
async function completeSignIn(snapshot, args, recheck) {
|
|
316
|
+
const previous = await _class_private_method_get(this, _observedLogins, observedLogins).call(this, args);
|
|
317
|
+
const withLocks = (work)=>previous.length ? _class_private_field_get(this, jwt_session_provider_store).withSessionLocks([
|
|
318
|
+
snapshot.id,
|
|
319
|
+
...previous.map((login)=>login.admin_user_id)
|
|
320
|
+
], work) : _class_private_field_get(this, jwt_session_provider_store).withSessionLock(snapshot.id, (store)=>work(store));
|
|
321
|
+
return withLocks(async (store)=>{
|
|
322
|
+
const current = await store.adminUsers.getByIdForSignIn(snapshot.id);
|
|
323
|
+
if (!current?.is_enabled) throw ERR_ACCOUNT_DISABLED({
|
|
324
|
+
message: 'account disabled'
|
|
325
|
+
});
|
|
326
|
+
if (current.session_version !== snapshot.session_version) throw ERR_INVALID_CREDENTIALS({
|
|
327
|
+
message: 'credentials changed during sign-in'
|
|
328
|
+
});
|
|
329
|
+
await recheck(store, current);
|
|
330
|
+
for (const observed of previous){
|
|
331
|
+
const login = await store.loginSessions.findById(observed.id);
|
|
332
|
+
if (login && login.admin_user_id === observed.admin_user_id) await store.loginSessions.revoke(login.id, _class_private_field_get(this, _now).call(this));
|
|
333
|
+
}
|
|
334
|
+
await store.adminUsers.recordLoginSuccess(snapshot.id, args.ip ?? null);
|
|
335
|
+
const actor = await resolveActorFromUser(store, current);
|
|
336
|
+
if (!actor) throw ERR_ACCOUNT_DISABLED({
|
|
337
|
+
message: 'account disabled'
|
|
338
|
+
});
|
|
339
|
+
const tokens = await _class_private_method_get(this, _issueTokens, issueTokens).call(this, store, {
|
|
340
|
+
adminUserId: snapshot.id,
|
|
341
|
+
sessionVersion: current.session_version,
|
|
342
|
+
ip: args.ip ?? null,
|
|
343
|
+
userAgent: args.userAgent ?? null
|
|
344
|
+
});
|
|
345
|
+
return {
|
|
346
|
+
...tokens,
|
|
347
|
+
actor
|
|
348
|
+
};
|
|
349
|
+
});
|
|
350
|
+
}
|
|
310
351
|
async function observedLogins(args) {
|
|
311
352
|
const ids = new Set();
|
|
312
353
|
if (args.previousAccessToken) {
|
|
@@ -320,20 +361,20 @@ async function observedLogins(args) {
|
|
|
320
361
|
claims = JSON.parse(new TextDecoder().decode(verified.payload));
|
|
321
362
|
} catch {}
|
|
322
363
|
if (claims?.iss === _class_private_field_get(this, _issuer) && 'access' === claims.typ && validSid(claims.sid) && 'string' == typeof claims.sub) {
|
|
323
|
-
const login = await _class_private_field_get(this,
|
|
364
|
+
const login = await _class_private_field_get(this, jwt_session_provider_store).loginSessions.findById(claims.sid);
|
|
324
365
|
if (login?.admin_user_id === claims.sub) ids.add(login.id);
|
|
325
366
|
}
|
|
326
367
|
}
|
|
327
368
|
if (args.previousRefreshToken) {
|
|
328
|
-
const token = await _class_private_field_get(this,
|
|
369
|
+
const token = await _class_private_field_get(this, jwt_session_provider_store).refreshTokens.findByHash(hashToken(args.previousRefreshToken));
|
|
329
370
|
if (token?.sid) {
|
|
330
|
-
const login = await _class_private_field_get(this,
|
|
371
|
+
const login = await _class_private_field_get(this, jwt_session_provider_store).loginSessions.findById(token.sid);
|
|
331
372
|
if (login?.admin_user_id === token.admin_user_id) ids.add(login.id);
|
|
332
373
|
}
|
|
333
374
|
}
|
|
334
375
|
const result = [];
|
|
335
376
|
for (const id of ids){
|
|
336
|
-
const login = await _class_private_field_get(this,
|
|
377
|
+
const login = await _class_private_field_get(this, jwt_session_provider_store).loginSessions.findById(id);
|
|
337
378
|
if (login) result.push(login);
|
|
338
379
|
}
|
|
339
380
|
return result;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
|
+
* Copyright (c) Infonomic Company Limited
|
|
6
|
+
*/
|
|
7
|
+
import { type AdminAuth, type PasskeyAdmissionReason, type PasskeyAuthenticationOptions, type PasskeyCeremony, type PasskeyCredentialSummary, type PasskeyManagementAction, type PasskeyManagementGrant, type PasskeyOperation, type PasskeyProvider, type PasskeyRegistrationOptions, type PasskeyRequestMetadata, type PasskeySession, type SignInResult, type SignInWithPasswordArgs } from '@byline/auth';
|
|
8
|
+
import { type PasskeyConfig } from '@byline/core';
|
|
9
|
+
import type { AdminStore } from '../../store.js';
|
|
10
|
+
import type { AdminUserWithPasswordRow } from '../admin-users/repository.js';
|
|
11
|
+
import type { NativeSessionEvent } from './jwt-session-provider.js';
|
|
12
|
+
type Authenticated = {
|
|
13
|
+
actor: AdminAuth;
|
|
14
|
+
user: AdminUserWithPasswordRow;
|
|
15
|
+
sessionId: string;
|
|
16
|
+
};
|
|
17
|
+
type SignInArgs = Pick<SignInWithPasswordArgs, 'previousAccessToken' | 'previousRefreshToken' | 'ip' | 'userAgent'>;
|
|
18
|
+
type PasskeyEvent = Extract<NativeSessionEvent, {
|
|
19
|
+
purpose: string;
|
|
20
|
+
}>;
|
|
21
|
+
interface NativeDependencies {
|
|
22
|
+
store: AdminStore;
|
|
23
|
+
now(): Date;
|
|
24
|
+
authenticate(session: PasskeySession, store?: AdminStore): Promise<Authenticated>;
|
|
25
|
+
/** Private constructor closure supplied only by JwtSessionProvider; never publicly exported. */
|
|
26
|
+
signIn(snapshot: AdminUserWithPasswordRow, args: SignInArgs, recheck: (store: AdminStore, user: AdminUserWithPasswordRow) => Promise<void>): Promise<SignInResult>;
|
|
27
|
+
emit(event: PasskeyEvent): void;
|
|
28
|
+
}
|
|
29
|
+
/** Internal native implementation. Public callers enter through SessionProvider.passkeys. */
|
|
30
|
+
export declare class NativePasskeyProvider implements PasskeyProvider {
|
|
31
|
+
#private;
|
|
32
|
+
private readonly deps;
|
|
33
|
+
constructor(trust: PasskeyConfig, deps: NativeDependencies);
|
|
34
|
+
recordAdmissionDenied(purpose: PasskeyOperation, reason: PasskeyAdmissionReason): void;
|
|
35
|
+
signInOptions(_metadata: PasskeyRequestMetadata): Promise<PasskeyCeremony<PasskeyAuthenticationOptions>>;
|
|
36
|
+
verifySignIn(args: SignInArgs & {
|
|
37
|
+
ceremonyId: string;
|
|
38
|
+
response: unknown;
|
|
39
|
+
}): Promise<SignInResult>;
|
|
40
|
+
passwordProof(args: PasskeySession & {
|
|
41
|
+
password: string;
|
|
42
|
+
action: PasskeyManagementAction;
|
|
43
|
+
}): Promise<PasskeyManagementGrant>;
|
|
44
|
+
proofOptions(args: PasskeySession & {
|
|
45
|
+
action: PasskeyManagementAction;
|
|
46
|
+
}): Promise<PasskeyCeremony<PasskeyAuthenticationOptions>>;
|
|
47
|
+
verifyProof(args: PasskeySession & {
|
|
48
|
+
ceremonyId: string;
|
|
49
|
+
response: unknown;
|
|
50
|
+
}): Promise<PasskeyManagementGrant>;
|
|
51
|
+
enrollmentOptions(args: PasskeySession & {
|
|
52
|
+
grantId: string;
|
|
53
|
+
}): Promise<PasskeyCeremony<PasskeyRegistrationOptions>>;
|
|
54
|
+
verifyEnrollment(args: PasskeySession & {
|
|
55
|
+
ceremonyId: string;
|
|
56
|
+
response: unknown;
|
|
57
|
+
name: string;
|
|
58
|
+
}): Promise<PasskeyCredentialSummary>;
|
|
59
|
+
listCredentials(args: PasskeySession & {
|
|
60
|
+
targetUserId?: string;
|
|
61
|
+
}): Promise<PasskeyCredentialSummary[]>;
|
|
62
|
+
renameCredential(args: PasskeySession & {
|
|
63
|
+
credentialRecordId: string;
|
|
64
|
+
name: string;
|
|
65
|
+
}): Promise<PasskeyCredentialSummary>;
|
|
66
|
+
removeCredential(args: PasskeySession & {
|
|
67
|
+
grantId: string;
|
|
68
|
+
}): Promise<{
|
|
69
|
+
signedOut: boolean;
|
|
70
|
+
}>;
|
|
71
|
+
}
|
|
72
|
+
export {};
|