@things-factory/auth-base 10.1.278 → 10.1.280
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/package.json +24 -7
- package/CHANGELOG.md +0 -55
- package/dist-client/actions/auth.js.map +0 -1
- package/dist-client/apply-domain-theme.js.map +0 -1
- package/dist-client/auth.js.map +0 -1
- package/dist-client/bootstrap.js.map +0 -1
- package/dist-client/directive/privileged.js.map +0 -1
- package/dist-client/domain-required.js.map +0 -1
- package/dist-client/index.js.map +0 -1
- package/dist-client/profiled.js.map +0 -1
- package/dist-client/reducers/auth.js.map +0 -1
- package/dist-client/tsconfig.tsbuildinfo +0 -1
- package/dist-client/verify-webauthn.js.map +0 -1
- package/dist-server/ai-entry/access-assistant.js.map +0 -1
- package/dist-server/ai-entry/access-entry-targets.js.map +0 -1
- package/dist-server/ai-entry/entry-target-shape.js.map +0 -1
- package/dist-server/constants/error-code.js.map +0 -1
- package/dist-server/constants/error-message.js.map +0 -1
- package/dist-server/constants/max-age.js.map +0 -1
- package/dist-server/controllers/access-request.js.map +0 -1
- package/dist-server/controllers/account-mail.js.map +0 -1
- package/dist-server/controllers/auth.js.map +0 -1
- package/dist-server/controllers/change-pwd.js.map +0 -1
- package/dist-server/controllers/confirm-identity.js.map +0 -1
- package/dist-server/controllers/delete-user.js.map +0 -1
- package/dist-server/controllers/passkey.js.map +0 -1
- package/dist-server/controllers/password-setup.js.map +0 -1
- package/dist-server/controllers/profile.js.map +0 -1
- package/dist-server/controllers/reset-password.js.map +0 -1
- package/dist-server/controllers/signin.js.map +0 -1
- package/dist-server/controllers/signup.js.map +0 -1
- package/dist-server/controllers/unlock-user.js.map +0 -1
- package/dist-server/controllers/utils/find-verification-token.js.map +0 -1
- package/dist-server/controllers/utils/make-verification-token.js.map +0 -1
- package/dist-server/controllers/utils/password-rule.js.map +0 -1
- package/dist-server/controllers/utils/save-verification-token.js.map +0 -1
- package/dist-server/controllers/verification.js.map +0 -1
- package/dist-server/errors/auth-error.js.map +0 -1
- package/dist-server/errors/index.js.map +0 -1
- package/dist-server/errors/user-domain-not-match-error.js.map +0 -1
- package/dist-server/index.js.map +0 -1
- package/dist-server/middlewares/audit-scope-middleware.js.map +0 -1
- package/dist-server/middlewares/authenticate-401-middleware.js.map +0 -1
- package/dist-server/middlewares/bypass-signin-middleware.js.map +0 -1
- package/dist-server/middlewares/domain-authenticate-middleware.js.map +0 -1
- package/dist-server/middlewares/graphql-authenticate-middleware.js.map +0 -1
- package/dist-server/middlewares/index.js.map +0 -1
- package/dist-server/middlewares/jwt-authenticate-middleware.js.map +0 -1
- package/dist-server/middlewares/signin-middleware.js.map +0 -1
- package/dist-server/middlewares/verify-recaptcha-middleware.js.map +0 -1
- package/dist-server/migrations/1548206416130-SeedUser.js.map +0 -1
- package/dist-server/migrations/1566805283882-SeedPrivilege.js.map +0 -1
- package/dist-server/migrations/1745000000000-MigrateDomainOwnersToTable.js.map +0 -1
- package/dist-server/migrations/index.js.map +0 -1
- package/dist-server/role-tasks.js.map +0 -1
- package/dist-server/router/auth-checkin-router.js.map +0 -1
- package/dist-server/router/auth-private-process-router.js.map +0 -1
- package/dist-server/router/auth-public-process-router.js.map +0 -1
- package/dist-server/router/auth-signin-router.js.map +0 -1
- package/dist-server/router/auth-signup-router.js.map +0 -1
- package/dist-server/router/index.js.map +0 -1
- package/dist-server/router/oauth2/index.js.map +0 -1
- package/dist-server/router/oauth2/oauth2-authorize-router.js.map +0 -1
- package/dist-server/router/oauth2/oauth2-router.js.map +0 -1
- package/dist-server/router/oauth2/oauth2-server.js.map +0 -1
- package/dist-server/router/oauth2/passport-oauth2-client-password.js.map +0 -1
- package/dist-server/router/oauth2/passport-refresh-token.js.map +0 -1
- package/dist-server/router/path-base-domain-router.js.map +0 -1
- package/dist-server/router/site-root-router.js.map +0 -1
- package/dist-server/router/webauthn-router.js.map +0 -1
- package/dist-server/routes.js.map +0 -1
- package/dist-server/service/access-request/access-request-mutation.js.map +0 -1
- package/dist-server/service/access-request/access-request-query.js.map +0 -1
- package/dist-server/service/access-request/access-request-types.js.map +0 -1
- package/dist-server/service/access-request/access-request.js.map +0 -1
- package/dist-server/service/access-request/index.js.map +0 -1
- package/dist-server/service/app-binding/app-binding-mutation.js.map +0 -1
- package/dist-server/service/app-binding/app-binding-query.js.map +0 -1
- package/dist-server/service/app-binding/app-binding-types.js.map +0 -1
- package/dist-server/service/app-binding/app-binding.js.map +0 -1
- package/dist-server/service/app-binding/index.js.map +0 -1
- package/dist-server/service/appliance/appliance-mutation.js.map +0 -1
- package/dist-server/service/appliance/appliance-query.js.map +0 -1
- package/dist-server/service/appliance/appliance-types.js.map +0 -1
- package/dist-server/service/appliance/appliance.js.map +0 -1
- package/dist-server/service/appliance/index.js.map +0 -1
- package/dist-server/service/application/application-mutation.js.map +0 -1
- package/dist-server/service/application/application-query.js.map +0 -1
- package/dist-server/service/application/application-types.js.map +0 -1
- package/dist-server/service/application/application.js.map +0 -1
- package/dist-server/service/application/index.js.map +0 -1
- package/dist-server/service/audit/audit-action.js.map +0 -1
- package/dist-server/service/audit/audit-kinds.js.map +0 -1
- package/dist-server/service/audit/audit-query.js.map +0 -1
- package/dist-server/service/audit/audit-recorder.js.map +0 -1
- package/dist-server/service/audit/audit-request.js.map +0 -1
- package/dist-server/service/audit/audit-retention.js.map +0 -1
- package/dist-server/service/audit/audit-subscriber.js.map +0 -1
- package/dist-server/service/audit/audit-target.js.map +0 -1
- package/dist-server/service/audit/audit-types.js.map +0 -1
- package/dist-server/service/audit/audit-work.js.map +0 -1
- package/dist-server/service/audit/index.js.map +0 -1
- package/dist-server/service/auth-provider/auth-provider-mutation.js.map +0 -1
- package/dist-server/service/auth-provider/auth-provider-parameter-spec.js.map +0 -1
- package/dist-server/service/auth-provider/auth-provider-query.js.map +0 -1
- package/dist-server/service/auth-provider/auth-provider-type.js.map +0 -1
- package/dist-server/service/auth-provider/auth-provider.js.map +0 -1
- package/dist-server/service/auth-provider/index.js.map +0 -1
- package/dist-server/service/domain-generator/domain-generator-mutation.js.map +0 -1
- package/dist-server/service/domain-generator/domain-generator-types.js.map +0 -1
- package/dist-server/service/domain-generator/index.js.map +0 -1
- package/dist-server/service/domain-link/domain-link-mutation.js.map +0 -1
- package/dist-server/service/domain-link/domain-link-query.js.map +0 -1
- package/dist-server/service/domain-link/domain-link-types.js.map +0 -1
- package/dist-server/service/domain-link/domain-link.js.map +0 -1
- package/dist-server/service/domain-link/index.js.map +0 -1
- package/dist-server/service/domain-owner/domain-owner-mutation.js.map +0 -1
- package/dist-server/service/domain-owner/domain-owner-query.js.map +0 -1
- package/dist-server/service/domain-owner/domain-owner.js.map +0 -1
- package/dist-server/service/domain-owner/index.js.map +0 -1
- package/dist-server/service/granted-role/granted-role-mutation.js.map +0 -1
- package/dist-server/service/granted-role/granted-role-query.js.map +0 -1
- package/dist-server/service/granted-role/granted-role.js.map +0 -1
- package/dist-server/service/granted-role/index.js.map +0 -1
- package/dist-server/service/identity-confirmation/identity-confirmation.js.map +0 -1
- package/dist-server/service/index.js.map +0 -1
- package/dist-server/service/open-fields.js.map +0 -1
- package/dist-server/service/partner/index.js.map +0 -1
- package/dist-server/service/partner/partner-mutation.js.map +0 -1
- package/dist-server/service/partner/partner-query.js.map +0 -1
- package/dist-server/service/partner/partner-types.js.map +0 -1
- package/dist-server/service/partner/partner.js.map +0 -1
- package/dist-server/service/password-history/index.js.map +0 -1
- package/dist-server/service/password-history/password-history.js.map +0 -1
- package/dist-server/service/privilege/index.js.map +0 -1
- package/dist-server/service/privilege/open-field-rule.js.map +0 -1
- package/dist-server/service/privilege/privilege-axis.js.map +0 -1
- package/dist-server/service/privilege/privilege-deprecation.js.map +0 -1
- package/dist-server/service/privilege/privilege-directive.js.map +0 -1
- package/dist-server/service/privilege/privilege-mutation.js.map +0 -1
- package/dist-server/service/privilege/privilege-query.js.map +0 -1
- package/dist-server/service/privilege/privilege-rejection.js.map +0 -1
- package/dist-server/service/privilege/privilege-sync.js.map +0 -1
- package/dist-server/service/privilege/privilege-types.js.map +0 -1
- package/dist-server/service/privilege/privilege.js.map +0 -1
- package/dist-server/service/role/index.js.map +0 -1
- package/dist-server/service/role/role-mutation.js.map +0 -1
- package/dist-server/service/role/role-privilege-query.js.map +0 -1
- package/dist-server/service/role/role-query.js.map +0 -1
- package/dist-server/service/role/role-types.js.map +0 -1
- package/dist-server/service/role/role.js.map +0 -1
- package/dist-server/service/role-template/index.js.map +0 -1
- package/dist-server/service/role-template/role-task.js.map +0 -1
- package/dist-server/service/role-template/role-template-gap-resolver.js.map +0 -1
- package/dist-server/service/role-template/role-template-mutation.js.map +0 -1
- package/dist-server/service/role-template/role-template-query.js.map +0 -1
- package/dist-server/service/role-template/role-template-types.js.map +0 -1
- package/dist-server/service/role-template/role-template.js.map +0 -1
- package/dist-server/service/user/domain-query.js.map +0 -1
- package/dist-server/service/user/index.js.map +0 -1
- package/dist-server/service/user/user-mutation.js.map +0 -1
- package/dist-server/service/user/user-query.js.map +0 -1
- package/dist-server/service/user/user-types.js.map +0 -1
- package/dist-server/service/user/user.js.map +0 -1
- package/dist-server/service/users-auth-providers/index.js.map +0 -1
- package/dist-server/service/users-auth-providers/users-auth-providers.js.map +0 -1
- package/dist-server/service/verification-token/index.js.map +0 -1
- package/dist-server/service/verification-token/verification-token.js.map +0 -1
- package/dist-server/service/web-auth-credential/index.js.map +0 -1
- package/dist-server/service/web-auth-credential/web-auth-credential-mutation.js.map +0 -1
- package/dist-server/service/web-auth-credential/web-auth-credential-query.js.map +0 -1
- package/dist-server/service/web-auth-credential/web-auth-credential.js.map +0 -1
- package/dist-server/tsconfig.tsbuildinfo +0 -1
- package/dist-server/types.js.map +0 -1
- package/dist-server/utils/accepts.js.map +0 -1
- package/dist-server/utils/access-request-review.js.map +0 -1
- package/dist-server/utils/access-token-cookie.js.map +0 -1
- package/dist-server/utils/account-lock-rule.js.map +0 -1
- package/dist-server/utils/assignable-roles.js.map +0 -1
- package/dist-server/utils/build-auth-url.js.map +0 -1
- package/dist-server/utils/can-user-checkin-domain.js.map +0 -1
- package/dist-server/utils/check-permission.js.map +0 -1
- package/dist-server/utils/check-user-belongs-domain.js.map +0 -1
- package/dist-server/utils/check-user-has-role.js.map +0 -1
- package/dist-server/utils/checkin-domain-facts.js.map +0 -1
- package/dist-server/utils/checkin-domain-rule.js.map +0 -1
- package/dist-server/utils/credential-at-rest.js.map +0 -1
- package/dist-server/utils/credential-features.js.map +0 -1
- package/dist-server/utils/credential-serial-rule.js.map +0 -1
- package/dist-server/utils/domain-ownership.js.map +0 -1
- package/dist-server/utils/encrypt-state.js.map +0 -1
- package/dist-server/utils/get-aes-256-key.js.map +0 -1
- package/dist-server/utils/get-domain-users.js.map +0 -1
- package/dist-server/utils/get-secret.js.map +0 -1
- package/dist-server/utils/get-user-domains.js.map +0 -1
- package/dist-server/utils/lock-duration.js.map +0 -1
- package/dist-server/utils/password-reset-rule.js.map +0 -1
- package/dist-server/utils/profile-user.js.map +0 -1
- package/dist-server/utils/refuse-role-name.js.map +0 -1
- package/dist-server/utils/role-name-standing.js.map +0 -1
- package/dist-server/utils/same-origin-redirect.js.map +0 -1
- package/dist-server/utils/verification-token-rule.js.map +0 -1
- package/spec/unit/checkin-domain.spec.ts +0 -307
- package/spec/unit/checkin-privilege.spec.ts +0 -205
- package/spec/unit/domain-owner-entity.spec.ts +0 -179
- package/spec/unit/domain-owner-granted.spec.ts +0 -215
- package/spec/unit/domain-owner-migration.spec.ts +0 -236
- package/spec/unit/domain-owner-mutation.spec.ts +0 -337
- package/spec/unit/domain-ownership.spec.ts +0 -188
- package/tests/access-assistant-db.test.ts +0 -155
- package/tests/access-entry-targets-db.test.ts +0 -176
- package/tests/access-request-db.test.ts +0 -394
- package/tests/access-request-pg.test.ts +0 -83
- package/tests/access-request-review.test.ts +0 -193
- package/tests/account-lock-db.test.ts +0 -216
- package/tests/account-lock-rule.test.ts +0 -141
- package/tests/account-mail.test.ts +0 -69
- package/tests/activate-page-case.test.ts +0 -53
- package/tests/add-user-gates.test.ts +0 -34
- package/tests/app-binding-db.test.ts +0 -210
- package/tests/app-binding-delete-db.test.ts +0 -188
- package/tests/appliance-credential-state-db.test.ts +0 -207
- package/tests/appliance-delete-db.test.ts +0 -119
- package/tests/appliance-revoke-db.test.ts +0 -294
- package/tests/appliance-schema.test.ts +0 -129
- package/tests/application-delete-db.test.ts +0 -190
- package/tests/application-token-payload-db.test.ts +0 -185
- package/tests/assignable-roles-db.test.ts +0 -219
- package/tests/audit-db.test.ts +0 -410
- package/tests/audit-work.test.ts +0 -58
- package/tests/auth-base-role-tasks.test.ts +0 -58
- package/tests/checkin-domain-rule.test.ts +0 -128
- package/tests/checkin-privilege-seam-db.test.ts +0 -158
- package/tests/compiled.ts +0 -41
- package/tests/confirm-identity-db.test.ts +0 -85
- package/tests/credential-at-rest-db.test.ts +0 -246
- package/tests/credential-features-off-db.test.ts +0 -189
- package/tests/credential-serial-rule.test.ts +0 -84
- package/tests/declare-privileges.test.ts +0 -29
- package/tests/domain-assignees-db.test.ts +0 -95
- package/tests/domain-inheritance-sentinel.test.ts +0 -93
- package/tests/domain-required.test.ts +0 -127
- package/tests/has-privilege-db.test.ts +0 -198
- package/tests/identity-confirmation.test.ts +0 -146
- package/tests/list-inheritance-db.test.ts +0 -127
- package/tests/lock-recovery.test.ts +0 -206
- package/tests/mail-request-answers-db.test.ts +0 -142
- package/tests/open-doors.test.ts +0 -121
- package/tests/open-fields-enforcement.test.ts +0 -215
- package/tests/open-fields-framework.test.ts +0 -212
- package/tests/open-redirect-db.test.ts +0 -143
- package/tests/passkey-authenticator.ts +0 -104
- package/tests/passkey-db.test.ts +0 -295
- package/tests/passkey-signin-route-db.test.ts +0 -139
- package/tests/password-hashing.test.ts +0 -131
- package/tests/password-persistence.test.ts +0 -147
- package/tests/password-reset-rule-db.test.ts +0 -159
- package/tests/password-setup-db.test.ts +0 -215
- package/tests/permission-gate.test.ts +0 -228
- package/tests/picture-reducer.test.ts +0 -25
- package/tests/privilege-axis.test.ts +0 -157
- package/tests/privilege-deprecation.test.ts +0 -65
- package/tests/privilege-directive.test.ts +0 -332
- package/tests/privilege-rejection.test.ts +0 -104
- package/tests/privilege-sync-db.test.ts +0 -58
- package/tests/profile-user.test.ts +0 -24
- package/tests/refusal-cause.test.ts +0 -80
- package/tests/refusals-have-korean.test.ts +0 -24
- package/tests/request-holds.test.ts +0 -53
- package/tests/role-inheritance-db.test.ts +0 -286
- package/tests/role-mutation-db.test.ts +0 -293
- package/tests/role-name-lookup-sentinel.test.ts +0 -79
- package/tests/role-name-standing.test.ts +0 -83
- package/tests/role-privileges-db.test.ts +0 -252
- package/tests/role-task.test.ts +0 -168
- package/tests/role-template-gap-db.test.ts +0 -268
- package/tests/role-template-seed-db.test.ts +0 -180
- package/tests/role-template.test.ts +0 -162
- package/tests/role-users-gate.test.ts +0 -22
- package/tests/signin-policy.test.ts +0 -200
- package/tests/signup-duplicate-db.test.ts +0 -68
- package/tests/token-issuance.test.ts +0 -244
- package/tests/user-roles-field-db.test.ts +0 -134
- package/tests/verification-token-db.test.ts +0 -285
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
|
|
2
|
-
import { loadCompiled } from './compiled'
|
|
3
|
-
import { sameOriginRedirectPath } from '../server/utils/same-origin-redirect'
|
|
4
|
-
|
|
5
|
-
const { Privilege } = loadCompiled('service/privilege/privilege')
|
|
6
|
-
const { Role } = loadCompiled('service/role/role')
|
|
7
|
-
const { User, UserStatus } = loadCompiled('service/user/user')
|
|
8
|
-
const { authCheckinRouter } = loadCompiled('router/auth-checkin-router')
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* `/auth/checkin?redirect_to=` must not send the browser to another host.
|
|
12
|
-
*
|
|
13
|
-
* Every sign-in ends there: the 401 redirect, the sign-in form and the WebAuthn sign-in all hand
|
|
14
|
-
* their `redirect_to` to the check-in page, and the check-in page is where `context.redirect` runs.
|
|
15
|
-
* So a link like `https://our-host/auth/checkin?redirect_to=https://elsewhere` used to show our
|
|
16
|
-
* login page and then land on `elsewhere` — the open redirect this file closes.
|
|
17
|
-
*
|
|
18
|
-
* The first half drives the real route with the value an attacker would write, because a test of
|
|
19
|
-
* the helper alone would stay green if the route stopped calling it. The second half pins the
|
|
20
|
-
* helper's reading of the shapes a browser treats as "another host".
|
|
21
|
-
*
|
|
22
|
-
* ⚠ Fixed-domain mode (`config.subdomain`) is read once when shell loads, so this suite runs the
|
|
23
|
-
* path-domain branch only. The helper runs before either branch, so both see the same value.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
const OUR_HOST = 'ex.test'
|
|
27
|
-
|
|
28
|
-
let acme: any
|
|
29
|
-
let person: any
|
|
30
|
-
|
|
31
|
-
beforeAll(async () => {
|
|
32
|
-
await openAuthDatabase()
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
afterAll(async () => {
|
|
36
|
-
await closeAuthDatabase()
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
beforeEach(async () => {
|
|
40
|
-
await resetAuthDatabase()
|
|
41
|
-
|
|
42
|
-
acme = await getRepository(Domain).save({ name: 'acme', subdomain: 'acme' })
|
|
43
|
-
const seeing = await getRepository(Privilege).save({ name: 'query', category: 'board' })
|
|
44
|
-
const role = await getRepository(Role).save({ name: 'viewer', domain: { id: acme.id }, privileges: [seeing] })
|
|
45
|
-
|
|
46
|
-
const saved = await getRepository(User).save({
|
|
47
|
-
username: 'kim@acme.z',
|
|
48
|
-
email: 'kim@acme.z',
|
|
49
|
-
name: 'kim',
|
|
50
|
-
status: UserStatus.ACTIVATED,
|
|
51
|
-
domains: [{ id: acme.id }],
|
|
52
|
-
roles: [role]
|
|
53
|
-
})
|
|
54
|
-
person = await getRepository(User).findOne({ where: { id: saved.id }, relations: ['domains', 'roles'] })
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
/** Runs `GET /auth/checkin` the way a browser following a link would, and returns where it sent it. */
|
|
58
|
-
async function checkinRedirectsTo(redirectTo: string): Promise<string> {
|
|
59
|
-
const layer = authCheckinRouter.stack.find((layer: any) => layer.path === '/auth/checkin/:subdomain?')
|
|
60
|
-
const redirect = jest.fn()
|
|
61
|
-
const render = jest.fn()
|
|
62
|
-
|
|
63
|
-
const context: any = {
|
|
64
|
-
method: 'GET',
|
|
65
|
-
host: OUR_HOST,
|
|
66
|
-
href: `http://${OUR_HOST}/auth/checkin`,
|
|
67
|
-
header: {},
|
|
68
|
-
headers: {},
|
|
69
|
-
request: { header: { accept: 'text/html' } },
|
|
70
|
-
req: { headers: {}, connection: { remoteAddress: '127.0.0.1' } },
|
|
71
|
-
t: (key: string) => key,
|
|
72
|
-
state: { user: person },
|
|
73
|
-
params: {},
|
|
74
|
-
query: { redirect_to: redirectTo },
|
|
75
|
-
redirect,
|
|
76
|
-
render
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
await layer.stack[layer.stack.length - 1](context, async () => {})
|
|
80
|
-
|
|
81
|
-
expect(render).not.toHaveBeenCalled()
|
|
82
|
-
expect(redirect).toHaveBeenCalledTimes(1)
|
|
83
|
-
return redirect.mock.calls[0][0]
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
describe('GET /auth/checkin with a single domain', () => {
|
|
87
|
-
it('still goes where it was asked, on this host', async () => {
|
|
88
|
-
const target = new URL(await checkinRedirectsTo('/domain/acme/board-list?x=1'))
|
|
89
|
-
|
|
90
|
-
expect(target.host).toBe(OUR_HOST)
|
|
91
|
-
expect(target.pathname + target.search).toBe('/domain/acme/board-list?x=1')
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
it.each([
|
|
95
|
-
'https://evil.example/steal',
|
|
96
|
-
'//evil.example/steal',
|
|
97
|
-
'/\\evil.example/steal',
|
|
98
|
-
'https:evil.example',
|
|
99
|
-
'\t//evil.example'
|
|
100
|
-
])('does not leave this host for %j', async redirectTo => {
|
|
101
|
-
const target = new URL(await checkinRedirectsTo(redirectTo))
|
|
102
|
-
|
|
103
|
-
expect(target.host).toBe(OUR_HOST)
|
|
104
|
-
})
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
describe('sameOriginRedirectPath', () => {
|
|
108
|
-
const context = { host: OUR_HOST, headers: {} }
|
|
109
|
-
|
|
110
|
-
it.each([
|
|
111
|
-
['/domain/acme/x?y=1#z', '/domain/acme/x?y=1#z'],
|
|
112
|
-
[`http://${OUR_HOST}/domain/acme/x`, '/domain/acme/x'],
|
|
113
|
-
['board-list', '/board-list']
|
|
114
|
-
])('keeps %j on this site as %j', (value, path) => {
|
|
115
|
-
expect(sameOriginRedirectPath(value, context)).toBe(path)
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
it('keeps an absolute URL on the host the proxy says we are', () => {
|
|
119
|
-
expect(sameOriginRedirectPath('https://public.example/x', { host: 'internal:3000', headers: { 'x-forwarded-host': 'public.example' } })).toBe('/x')
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it.each([
|
|
123
|
-
'https://evil.example/x',
|
|
124
|
-
'//evil.example/x',
|
|
125
|
-
'/\\evil.example/x',
|
|
126
|
-
'https:evil.example/x',
|
|
127
|
-
'\t//evil.example/x',
|
|
128
|
-
'/\t/evil.example/x',
|
|
129
|
-
'javascript:alert(1)',
|
|
130
|
-
`ftp://${OUR_HOST}/x`
|
|
131
|
-
])('sends %j home', value => {
|
|
132
|
-
expect(sameOriginRedirectPath(value, context)).toBe('/')
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
it('does not return a path a browser reads as a host', () => {
|
|
136
|
-
/* `/.//evil.example` resolves on our origin to the path `//evil.example`. */
|
|
137
|
-
expect(sameOriginRedirectPath('/.//evil.example', context)).toBe('/evil.example')
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
it.each([undefined, '', ' ', ['/a', '/b']])('sends %j home', value => {
|
|
141
|
-
expect(sameOriginRedirectPath(value, context)).toBe('/')
|
|
142
|
-
})
|
|
143
|
-
})
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A passkey authenticator in software, for the tests.
|
|
3
|
-
*
|
|
4
|
-
* It answers registration and sign-in the way a browser hands them to the server - the same JSON
|
|
5
|
-
* `@simplewebauthn/browser` produces - with an ES256 key held here. `userVerified` sets the UV flag:
|
|
6
|
-
* the authenticator saying it checked a fingerprint, face or PIN. Nothing here verifies anything;
|
|
7
|
-
* that is the server's part, and the tests watch the server.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { createHash, generateKeyPairSync, randomBytes, sign as signData, type KeyObject } from 'crypto'
|
|
11
|
-
|
|
12
|
-
const { isoCBOR } = require('@simplewebauthn/server/helpers')
|
|
13
|
-
|
|
14
|
-
const FLAG_UP = 0x01
|
|
15
|
-
const FLAG_UV = 0x04
|
|
16
|
-
const FLAG_AT = 0x40
|
|
17
|
-
|
|
18
|
-
const b64url = (bytes: Uint8Array | Buffer) => Buffer.from(bytes).toString('base64url')
|
|
19
|
-
const sha256 = (data: Buffer | string) => createHash('sha256').update(data).digest()
|
|
20
|
-
|
|
21
|
-
export type Site = { rpID: string; origin: string }
|
|
22
|
-
|
|
23
|
-
export class SoftwareAuthenticator {
|
|
24
|
-
readonly credentialId = randomBytes(16)
|
|
25
|
-
private readonly privateKey: KeyObject
|
|
26
|
-
private readonly publicJwk: { x: string; y: string }
|
|
27
|
-
private counter = 0
|
|
28
|
-
|
|
29
|
-
constructor() {
|
|
30
|
-
const { privateKey, publicKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' })
|
|
31
|
-
this.privateKey = privateKey
|
|
32
|
-
this.publicJwk = publicKey.export({ format: 'jwk' }) as any
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
get id(): string {
|
|
36
|
-
return b64url(this.credentialId)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
private authenticatorData(site: Site, flags: number, attested?: Buffer): Buffer {
|
|
40
|
-
const count = Buffer.alloc(4)
|
|
41
|
-
count.writeUInt32BE(this.counter)
|
|
42
|
-
return Buffer.concat([sha256(site.rpID), Buffer.from([flags]), count, ...(attested ? [attested] : [])])
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
private clientData(type: string, challenge: string, site: Site): Buffer {
|
|
46
|
-
return Buffer.from(JSON.stringify({ type, challenge, origin: site.origin, crossOrigin: false }))
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** The browser's answer to `navigator.credentials.create`. */
|
|
50
|
-
register(challenge: string, site: Site, { userVerified = true } = {}) {
|
|
51
|
-
const cose = new Map<number, number | Uint8Array>([
|
|
52
|
-
[1, 2],
|
|
53
|
-
[3, -7],
|
|
54
|
-
[-1, 1],
|
|
55
|
-
[-2, new Uint8Array(Buffer.from(this.publicJwk.x, 'base64url'))],
|
|
56
|
-
[-3, new Uint8Array(Buffer.from(this.publicJwk.y, 'base64url'))]
|
|
57
|
-
])
|
|
58
|
-
const idLength = Buffer.alloc(2)
|
|
59
|
-
idLength.writeUInt16BE(this.credentialId.length)
|
|
60
|
-
const attested = Buffer.concat([Buffer.alloc(16), idLength, this.credentialId, Buffer.from(isoCBOR.encode(cose))])
|
|
61
|
-
|
|
62
|
-
const authData = this.authenticatorData(site, FLAG_UP | FLAG_AT | (userVerified ? FLAG_UV : 0), attested)
|
|
63
|
-
const attestationObject = isoCBOR.encode(
|
|
64
|
-
new Map<string, unknown>([
|
|
65
|
-
['fmt', 'none'],
|
|
66
|
-
['attStmt', new Map()],
|
|
67
|
-
['authData', new Uint8Array(authData)]
|
|
68
|
-
])
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
id: this.id,
|
|
73
|
-
rawId: this.id,
|
|
74
|
-
type: 'public-key',
|
|
75
|
-
clientExtensionResults: {},
|
|
76
|
-
response: {
|
|
77
|
-
clientDataJSON: b64url(this.clientData('webauthn.create', challenge, site)),
|
|
78
|
-
attestationObject: b64url(attestationObject),
|
|
79
|
-
transports: ['internal', 'hybrid']
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** The browser's answer to `navigator.credentials.get`. */
|
|
85
|
-
assert(challenge: string, site: Site, { userVerified = true } = {}) {
|
|
86
|
-
this.counter++
|
|
87
|
-
const authData = this.authenticatorData(site, FLAG_UP | (userVerified ? FLAG_UV : 0))
|
|
88
|
-
const clientDataJSON = this.clientData('webauthn.get', challenge, site)
|
|
89
|
-
const signature = signData('sha256', Buffer.concat([authData, sha256(clientDataJSON)]), this.privateKey)
|
|
90
|
-
|
|
91
|
-
return {
|
|
92
|
-
id: this.id,
|
|
93
|
-
rawId: this.id,
|
|
94
|
-
type: 'public-key',
|
|
95
|
-
clientExtensionResults: {},
|
|
96
|
-
response: {
|
|
97
|
-
authenticatorData: b64url(authData),
|
|
98
|
-
clientDataJSON: b64url(clientDataJSON),
|
|
99
|
-
signature: b64url(signature),
|
|
100
|
-
userHandle: null
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
package/tests/passkey-db.test.ts
DELETED
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Passkeys (ADR-0156) - registration, sign-in, confirmation and the caller's own list, through the
|
|
3
|
-
* real controllers and resolvers on the sqlite harness. The authenticator is software
|
|
4
|
-
* (`passkey-authenticator.ts`); what is watched is what the server accepts and what it writes.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase, authDataSource } from './db'
|
|
8
|
-
import { loadCompiled } from './compiled'
|
|
9
|
-
import { SoftwareAuthenticator } from './passkey-authenticator'
|
|
10
|
-
|
|
11
|
-
const { User } = loadCompiled('service/user/user')
|
|
12
|
-
const { WebAuthCredential } = loadCompiled('service/web-auth-credential/web-auth-credential')
|
|
13
|
-
const { WebAuthCredentialQuery } = loadCompiled('service/web-auth-credential/web-auth-credential-query')
|
|
14
|
-
const { WebAuthCredentialMutation } = loadCompiled('service/web-auth-credential/web-auth-credential-mutation')
|
|
15
|
-
const { AuditAction } = loadCompiled('service/audit/audit-action')
|
|
16
|
-
const { flushAudit } = loadCompiled('service/audit/audit-recorder')
|
|
17
|
-
const passkey = loadCompiled('controllers/passkey')
|
|
18
|
-
|
|
19
|
-
const SITE = { rpID: 'localhost', origin: 'http://localhost:3000' }
|
|
20
|
-
const PASSWORD = 'Passw0rd!'
|
|
21
|
-
|
|
22
|
-
let domain: any
|
|
23
|
-
let kim: any
|
|
24
|
-
let lee: any
|
|
25
|
-
|
|
26
|
-
async function seedUser(username: string, overrides: any = {}) {
|
|
27
|
-
const salt = User.generateSalt()
|
|
28
|
-
return await getRepository(User).save({
|
|
29
|
-
username,
|
|
30
|
-
email: `${username}@example.com`,
|
|
31
|
-
name: username,
|
|
32
|
-
salt,
|
|
33
|
-
password: User.encode(PASSWORD, salt),
|
|
34
|
-
status: 'activated',
|
|
35
|
-
domains: [domain],
|
|
36
|
-
...overrides
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** A challenge the way the router makes one: from the options, into the session, taken back out. */
|
|
41
|
-
async function signinChallenge(): Promise<string> {
|
|
42
|
-
const context: any = { session: {} }
|
|
43
|
-
passkey.rememberChallenge(context, (await passkey.signinOptions(SITE)).challenge)
|
|
44
|
-
return passkey.takeChallenge(context)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function registered(user: any, name = 'macOS · Chrome') {
|
|
48
|
-
const authenticator = new SoftwareAuthenticator()
|
|
49
|
-
const { challenge } = await passkey.registrationOptions(user, 'Operato', SITE)
|
|
50
|
-
await passkey.registerPasskey(user, authenticator.register(challenge, SITE), name, challenge, SITE)
|
|
51
|
-
return authenticator
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function signinWith(authenticator: SoftwareAuthenticator, options: { userVerified?: boolean } = {}) {
|
|
55
|
-
const challenge = await signinChallenge()
|
|
56
|
-
return await passkey.signinWithPasskey(authenticator.assert(challenge, SITE, options), challenge, SITE)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const credentialOf = (authenticator: SoftwareAuthenticator) =>
|
|
60
|
-
getRepository(WebAuthCredential).findOne({ where: { credentialId: authenticator.id }, withDeleted: true })
|
|
61
|
-
|
|
62
|
-
async function signinAudits() {
|
|
63
|
-
await flushAudit()
|
|
64
|
-
const rows = await getRepository(AuditAction).find({ where: { kind: 'auth.signin' }, relations: ['actor'], order: { createdAt: 'ASC' } })
|
|
65
|
-
return rows.map(row => [row.outcome, row.code, row.actor?.id ?? null, row.detail?.method])
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function context(user: any) {
|
|
69
|
-
return { state: { user, tx: authDataSource().manager } } as any
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
beforeAll(async () => {
|
|
73
|
-
await openAuthDatabase()
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
afterAll(async () => {
|
|
77
|
-
await closeAuthDatabase()
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
beforeEach(async () => {
|
|
81
|
-
await resetAuthDatabase()
|
|
82
|
-
domain = await getRepository(Domain).save({ name: 'factory', subdomain: 'factory' })
|
|
83
|
-
kim = await seedUser('kim')
|
|
84
|
-
lee = await seedUser('lee')
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
describe('what the browser is asked (decision 2)', () => {
|
|
88
|
-
it('registration needs a discoverable passkey and a verified person, and leaves the authenticator to the browser', async () => {
|
|
89
|
-
const options = await passkey.registrationOptions(kim, 'Operato', SITE)
|
|
90
|
-
|
|
91
|
-
expect(options.authenticatorSelection).toEqual({ residentKey: 'required', requireResidentKey: true, userVerification: 'required' })
|
|
92
|
-
expect(options.authenticatorSelection.authenticatorAttachment).toBeUndefined()
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('sign-in names no passkey and needs a verified person', async () => {
|
|
96
|
-
const options = await passkey.signinOptions(SITE)
|
|
97
|
-
|
|
98
|
-
expect(options.userVerification).toBe('required')
|
|
99
|
-
expect(options.allowCredentials ?? []).toEqual([])
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
it('registration lists what this person already has, so the browser can say so', async () => {
|
|
103
|
-
const authenticator = await registered(kim)
|
|
104
|
-
const options = await passkey.registrationOptions(kim, 'Operato', SITE)
|
|
105
|
-
|
|
106
|
-
expect(options.excludeCredentials.map(c => c.id)).toEqual([authenticator.id])
|
|
107
|
-
})
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
describe('a challenge answers once', () => {
|
|
111
|
-
it('is gone from the session after it is taken', () => {
|
|
112
|
-
const context: any = { session: {} }
|
|
113
|
-
passkey.rememberChallenge(context, 'abc')
|
|
114
|
-
|
|
115
|
-
expect(passkey.takeChallenge(context)).toBe('abc')
|
|
116
|
-
expect(() => passkey.takeChallenge(context)).toThrow('authn verification failed')
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
it('an answer to another challenge is refused', async () => {
|
|
120
|
-
const authenticator = await registered(kim)
|
|
121
|
-
const challenge = await signinChallenge()
|
|
122
|
-
const answer = authenticator.assert(challenge, SITE)
|
|
123
|
-
|
|
124
|
-
await expect(passkey.signinWithPasskey(answer, await signinChallenge(), SITE)).rejects.toThrow('authn verification failed')
|
|
125
|
-
})
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
describe('registration', () => {
|
|
129
|
-
it('keeps the passkey with its name and how it can be reached', async () => {
|
|
130
|
-
const authenticator = await registered(kim, ' macOS · Chrome ')
|
|
131
|
-
const credential = await credentialOf(authenticator)
|
|
132
|
-
|
|
133
|
-
expect(credential.userId).toBe(kim.id)
|
|
134
|
-
expect(credential.name).toBe('macOS · Chrome')
|
|
135
|
-
expect(credential.transports).toEqual(['internal', 'hybrid'])
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
it('refuses a passkey made without verifying the person', async () => {
|
|
139
|
-
const authenticator = new SoftwareAuthenticator()
|
|
140
|
-
const { challenge } = await passkey.registrationOptions(kim, 'Operato', SITE)
|
|
141
|
-
|
|
142
|
-
await expect(
|
|
143
|
-
passkey.registerPasskey(kim, authenticator.register(challenge, SITE, { userVerified: false }), 'x', challenge, SITE)
|
|
144
|
-
).rejects.toThrow('authn verification failed')
|
|
145
|
-
expect(await credentialOf(authenticator)).toBeNull()
|
|
146
|
-
})
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
describe('signing in (decisions 2 and 4)', () => {
|
|
150
|
-
it('signs the owner in, moves the counter on and notes when it was used', async () => {
|
|
151
|
-
const authenticator = await registered(kim)
|
|
152
|
-
|
|
153
|
-
const answer = await signinWith(authenticator)
|
|
154
|
-
const credential = await credentialOf(authenticator)
|
|
155
|
-
|
|
156
|
-
expect(answer.user.id).toBe(kim.id)
|
|
157
|
-
expect(answer.token).toEqual(expect.any(String))
|
|
158
|
-
expect(credential.counter).toBe(1)
|
|
159
|
-
expect(credential.lastUsedAt).toBeInstanceOf(Date)
|
|
160
|
-
expect(await signinAudits()).toEqual([['ok', null, kim.id, 'passkey']])
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
it('refuses an answer without a verified person, and the audit names the owner', async () => {
|
|
164
|
-
const authenticator = await registered(kim)
|
|
165
|
-
|
|
166
|
-
await expect(signinWith(authenticator, { userVerified: false })).rejects.toThrow('authn verification failed')
|
|
167
|
-
expect((await credentialOf(authenticator)).lastUsedAt).toBeNull()
|
|
168
|
-
expect(await signinAudits()).toEqual([['error', 'authn verification failed', kim.id, 'passkey']])
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
it('refuses a passkey nobody registered, and the audit names nobody', async () => {
|
|
172
|
-
await expect(signinWith(new SoftwareAuthenticator())).rejects.toThrow('authn verification failed')
|
|
173
|
-
expect(await signinAudits()).toEqual([['error', 'authn verification failed', null, 'passkey']])
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
it('refuses a removed passkey', async () => {
|
|
177
|
-
const authenticator = await registered(kim)
|
|
178
|
-
await getRepository(WebAuthCredential).softDelete({ credentialId: authenticator.id })
|
|
179
|
-
|
|
180
|
-
await expect(signinWith(authenticator)).rejects.toThrow('authn verification failed')
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
it('a locked account stays locked - the passkey neither lifts the lock nor counts as a failure', async () => {
|
|
184
|
-
const authenticator = await registered(kim)
|
|
185
|
-
await getRepository(User).update({ id: kim.id }, { status: 'locked', lockedAt: new Date(), failCount: 5 })
|
|
186
|
-
|
|
187
|
-
await expect(signinWith(authenticator)).rejects.toThrow('user-locked')
|
|
188
|
-
|
|
189
|
-
const after = await getRepository(User).findOneBy({ id: kim.id })
|
|
190
|
-
expect([after.status, after.failCount]).toEqual(['locked', 5])
|
|
191
|
-
expect((await credentialOf(authenticator)).lastUsedAt).toBeNull()
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
it('a lock that has run out lifts, the same as for a password', async () => {
|
|
195
|
-
const authenticator = await registered(kim)
|
|
196
|
-
await getRepository(User).update({ id: kim.id }, { status: 'locked', lockedAt: new Date(Date.now() - 24 * 60 * 60 * 1000), failCount: 5 })
|
|
197
|
-
|
|
198
|
-
await signinWith(authenticator)
|
|
199
|
-
|
|
200
|
-
const after = await getRepository(User).findOneBy({ id: kim.id })
|
|
201
|
-
expect([after.status, after.failCount]).toEqual(['activated', 0])
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
it('a deleted or inactive account is refused', async () => {
|
|
205
|
-
const authenticator = await registered(kim)
|
|
206
|
-
|
|
207
|
-
await getRepository(User).update({ id: kim.id }, { status: 'deleted' })
|
|
208
|
-
await expect(signinWith(authenticator)).rejects.toThrow('user-deleted')
|
|
209
|
-
|
|
210
|
-
await getRepository(User).update({ id: kim.id }, { status: 'inactive' })
|
|
211
|
-
await expect(signinWith(authenticator)).rejects.toThrow('user not activated')
|
|
212
|
-
})
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
describe('confirming it is the signed-in person', () => {
|
|
216
|
-
async function reverify(user: any, authenticator: SoftwareAuthenticator) {
|
|
217
|
-
const options = await passkey.reverifyOptions(user, SITE)
|
|
218
|
-
return await passkey.reverifyWithPasskey(user, authenticator.assert(options.challenge, SITE), options.challenge, SITE)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
it("answers with the person's own passkey", async () => {
|
|
222
|
-
const authenticator = await registered(kim)
|
|
223
|
-
|
|
224
|
-
await expect(reverify(kim, authenticator)).resolves.toBeUndefined()
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
it("refuses somebody else's passkey", async () => {
|
|
228
|
-
await registered(kim)
|
|
229
|
-
const lees = await registered(lee)
|
|
230
|
-
|
|
231
|
-
await expect(reverify(kim, lees)).rejects.toThrow('authn verification failed')
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
it('has nothing to ask of a person without a passkey', async () => {
|
|
235
|
-
expect(await passkey.reverifyOptions(kim, SITE)).toBeNull()
|
|
236
|
-
})
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
describe("the caller's own passkeys (decision 6)", () => {
|
|
240
|
-
const query = new WebAuthCredentialQuery()
|
|
241
|
-
const mutation = new WebAuthCredentialMutation()
|
|
242
|
-
|
|
243
|
-
it('lists only the caller’s own', async () => {
|
|
244
|
-
const mine = await registered(kim)
|
|
245
|
-
await registered(lee)
|
|
246
|
-
|
|
247
|
-
const list = await query.myWebAuthCredentials(context(kim))
|
|
248
|
-
|
|
249
|
-
expect(list.map(c => c.credentialId)).toEqual([mine.id])
|
|
250
|
-
})
|
|
251
|
-
|
|
252
|
-
it('renames the caller’s own, and answers another person’s id as not found', async () => {
|
|
253
|
-
const credential = await credentialOf(await registered(kim))
|
|
254
|
-
|
|
255
|
-
const renamed = await mutation.updateMyWebAuthCredential(credential.id, ' Phone ', context(kim))
|
|
256
|
-
expect(renamed.name).toBe('Phone')
|
|
257
|
-
|
|
258
|
-
await expect(mutation.updateMyWebAuthCredential(credential.id, 'Mine now', context(lee))).rejects.toThrow('There is no passkey of yours')
|
|
259
|
-
expect((await credentialOf({ id: credential.credentialId } as any)).name).toBe('Phone')
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
it('★ a name over the bound is refused with the bound, not cut', async () => {
|
|
263
|
-
const credential = await credentialOf(await registered(kim))
|
|
264
|
-
await mutation.updateMyWebAuthCredential(credential.id, 'Phone', context(kim))
|
|
265
|
-
|
|
266
|
-
await expect(mutation.updateMyWebAuthCredential(credential.id, '가'.repeat(65), context(kim))).rejects.toMatchObject({ code: 'passkey-name-too-long', params: { max: 64, length: 65 } })
|
|
267
|
-
expect((await credentialOf({ id: credential.credentialId } as any)).name).toBe('Phone')
|
|
268
|
-
expect((await mutation.updateMyWebAuthCredential(credential.id, '가'.repeat(64), context(kim))).name).toBe('가'.repeat(64))
|
|
269
|
-
})
|
|
270
|
-
|
|
271
|
-
it('removes the caller’s own - it signs nobody in afterwards and the removal is audited', async () => {
|
|
272
|
-
const authenticator = await registered(kim)
|
|
273
|
-
const credential = await credentialOf(authenticator)
|
|
274
|
-
|
|
275
|
-
await expect(mutation.deleteMyWebAuthCredential(credential.id, context(lee))).rejects.toThrow('There is no passkey of yours')
|
|
276
|
-
expect((await credentialOf(authenticator)).deletedAt).toBeNull()
|
|
277
|
-
|
|
278
|
-
expect(await mutation.deleteMyWebAuthCredential(credential.id, context(kim))).toBe(true)
|
|
279
|
-
expect((await credentialOf(authenticator)).deletedAt).toBeInstanceOf(Date)
|
|
280
|
-
expect(await query.myWebAuthCredentials(context(kim))).toEqual([])
|
|
281
|
-
await expect(signinWith(authenticator)).rejects.toThrow('authn verification failed')
|
|
282
|
-
|
|
283
|
-
await flushAudit()
|
|
284
|
-
const removals = await getRepository(AuditAction).find({ where: { kind: 'webauthn.remove' }, relations: ['actor'] })
|
|
285
|
-
expect(removals.map(row => row.actor?.id)).toEqual([kim.id])
|
|
286
|
-
})
|
|
287
|
-
|
|
288
|
-
it('the same authenticator can be registered again after its passkey was removed', async () => {
|
|
289
|
-
const authenticator = await registered(kim)
|
|
290
|
-
await getRepository(WebAuthCredential).softDelete({ credentialId: authenticator.id })
|
|
291
|
-
|
|
292
|
-
const { challenge } = await passkey.registrationOptions(kim, 'Operato', SITE)
|
|
293
|
-
await expect(passkey.registerPasskey(kim, authenticator.register(challenge, SITE), 'again', challenge, SITE)).resolves.toBeTruthy()
|
|
294
|
-
})
|
|
295
|
-
})
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The passkey sign-in route, through `authenticate401Middleware`, the way the sign-in screen reaches it.
|
|
3
|
-
*
|
|
4
|
-
* The screen used to fetch it. A fetch says `Accept: *\/*`, the middleware takes that for a page request,
|
|
5
|
-
* so a locked account got the whole `auth-activate` page back as the body - and the screen put that body
|
|
6
|
-
* in a toast. Now the screen posts a form, as the password does, and every refusal is answered with the
|
|
7
|
-
* page its code leads to: the activate page for a locked or unconfirmed account, the sign-in page with
|
|
8
|
-
* its one line for the rest.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
|
|
12
|
-
import { loadCompiled } from './compiled'
|
|
13
|
-
import { SoftwareAuthenticator } from './passkey-authenticator'
|
|
14
|
-
|
|
15
|
-
const { User } = loadCompiled('service/user/user')
|
|
16
|
-
const { authenticate401Middleware } = loadCompiled('middlewares/authenticate-401-middleware')
|
|
17
|
-
const { webAuthnGlobalPublicRouter } = loadCompiled('router/webauthn-router')
|
|
18
|
-
const CODES = loadCompiled('constants/error-code')
|
|
19
|
-
const passkey = loadCompiled('controllers/passkey')
|
|
20
|
-
|
|
21
|
-
const SITE = { rpID: 'localhost', origin: 'http://localhost:3000' }
|
|
22
|
-
const NAVIGATION = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
|
|
23
|
-
|
|
24
|
-
let domain: any
|
|
25
|
-
let kim: any
|
|
26
|
-
|
|
27
|
-
const signinRoute = webAuthnGlobalPublicRouter.stack.find(
|
|
28
|
-
(layer: any) => layer.path === '/auth/signin-webauthn' && layer.methods.includes('POST')
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
/** What the browser gets back for the form the screen posts. */
|
|
32
|
-
async function post(body: (challenge: string) => any) {
|
|
33
|
-
const { challenge } = await passkey.signinOptions(SITE)
|
|
34
|
-
const answer: any = { status: 200, cookies: {} }
|
|
35
|
-
|
|
36
|
-
const context: any = {
|
|
37
|
-
t: (key: string) => key,
|
|
38
|
-
method: 'POST',
|
|
39
|
-
path: '/auth/signin-webauthn',
|
|
40
|
-
originalUrl: '/auth/signin-webauthn',
|
|
41
|
-
header: { accept: NAVIGATION },
|
|
42
|
-
hostname: SITE.rpID,
|
|
43
|
-
origin: SITE.origin,
|
|
44
|
-
secure: false,
|
|
45
|
-
query: {},
|
|
46
|
-
session: { challenge },
|
|
47
|
-
state: { domain },
|
|
48
|
-
request: { body: body(challenge) },
|
|
49
|
-
cookies: { set: (key: string, value: string) => (answer.cookies[key] = value) },
|
|
50
|
-
render: async (view: string, options: any) => (answer.page = { view, ...options }),
|
|
51
|
-
redirect: (url: string) => (answer.redirect = url)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
await authenticate401Middleware(context, () => signinRoute.stack[0](context, async () => {}))
|
|
55
|
-
|
|
56
|
-
return { ...answer, status: context.status, body: context.body }
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function registered(user: any) {
|
|
60
|
-
const authenticator = new SoftwareAuthenticator()
|
|
61
|
-
const { challenge } = await passkey.registrationOptions(user, 'Operato', SITE)
|
|
62
|
-
await passkey.registerPasskey(user, authenticator.register(challenge, SITE), 'macOS · Chrome', challenge, SITE)
|
|
63
|
-
return authenticator
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const formWith = (authenticator: SoftwareAuthenticator) => (challenge: string) => ({
|
|
67
|
-
response: JSON.stringify(authenticator.assert(challenge, SITE)),
|
|
68
|
-
redirectTo: '/office'
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
beforeAll(async () => {
|
|
72
|
-
await openAuthDatabase()
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
afterAll(async () => {
|
|
76
|
-
await closeAuthDatabase()
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
beforeEach(async () => {
|
|
80
|
-
await resetAuthDatabase()
|
|
81
|
-
domain = await getRepository(Domain).save({ name: 'factory', subdomain: 'factory' })
|
|
82
|
-
|
|
83
|
-
const salt = User.generateSalt()
|
|
84
|
-
kim = await getRepository(User).save({
|
|
85
|
-
username: 'kim',
|
|
86
|
-
email: 'kim@example.com',
|
|
87
|
-
name: 'kim',
|
|
88
|
-
salt,
|
|
89
|
-
password: User.encode('Passw0rd!', salt),
|
|
90
|
-
status: 'activated',
|
|
91
|
-
domains: [domain]
|
|
92
|
-
})
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('signs in and goes on to check-in, with the token in the cookie', async () => {
|
|
96
|
-
const answer = await post(formWith(await registered(kim)))
|
|
97
|
-
|
|
98
|
-
expect(answer.redirect).toBe('/auth/checkin/factory?redirect_to=%2Foffice')
|
|
99
|
-
expect(Object.values(answer.cookies)).toHaveLength(1)
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
describe.each([
|
|
103
|
-
['locked', CODES.USER_LOCKED],
|
|
104
|
-
['inactive', CODES.USER_NOT_ACTIVATED]
|
|
105
|
-
])('a %s account', (status, errorCode) => {
|
|
106
|
-
it('lands on the activate page with its code and its line, the way a password does', async () => {
|
|
107
|
-
const authenticator = await registered(kim)
|
|
108
|
-
await getRepository(User).update({ id: kim.id }, { status, lockedAt: new Date() })
|
|
109
|
-
|
|
110
|
-
const answer = await post(formWith(authenticator))
|
|
111
|
-
|
|
112
|
-
expect(answer.status).toBe(401)
|
|
113
|
-
expect(answer.page.pageElement).toBe('auth-activate')
|
|
114
|
-
expect(answer.page.data).toMatchObject({ errorCode, message: `error.${errorCode}`, username: 'kim', redirectTo: '/office' })
|
|
115
|
-
expect(answer.cookies).toEqual({})
|
|
116
|
-
})
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
describe.each([
|
|
120
|
-
['deleted', CODES.USER_DELETED],
|
|
121
|
-
['password_reset_required', CODES.PASSWORD_SETUP_PENDING]
|
|
122
|
-
])('a %s account', (status, errorCode) => {
|
|
123
|
-
it('stays on the sign-in page with its one line', async () => {
|
|
124
|
-
const authenticator = await registered(kim)
|
|
125
|
-
await getRepository(User).update({ id: kim.id }, { status })
|
|
126
|
-
|
|
127
|
-
const answer = await post(formWith(authenticator))
|
|
128
|
-
|
|
129
|
-
expect(answer.page.pageElement).toBe('auth-signin')
|
|
130
|
-
expect(answer.page.data.message).toBe(`error.${errorCode}`)
|
|
131
|
-
})
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
it('an answer that is not JSON is a failed verification, said in one line on the sign-in page', async () => {
|
|
135
|
-
const answer = await post(() => ({ response: '<not json>', redirectTo: '/office' }))
|
|
136
|
-
|
|
137
|
-
expect(answer.page.pageElement).toBe('auth-signin')
|
|
138
|
-
expect(answer.page.data.message).toBe(`error.${CODES.AUTHN_VERIFICATION_FAILED}`)
|
|
139
|
-
})
|