@nacre.work/api 0.17.5 → 0.19.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/errors.d.ts +19 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +28 -0
- package/dist/errors.js.map +1 -1
- package/dist/login.d.ts +166 -1
- package/dist/login.d.ts.map +1 -1
- package/dist/login.js +210 -3
- package/dist/login.js.map +1 -1
- package/dist/main.js +71 -1
- package/dist/main.js.map +1 -1
- package/dist/recovery.d.ts +91 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +199 -0
- package/dist/recovery.js.map +1 -0
- package/dist/second-factor.d.ts +202 -0
- package/dist/second-factor.d.ts.map +1 -0
- package/dist/second-factor.js +479 -0
- package/dist/second-factor.js.map +1 -0
- package/dist/server.d.ts +25 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +747 -38
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { Pool } from 'pg';
|
|
2
|
+
export interface EnrolledFactor {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly kind: FactorKind;
|
|
5
|
+
readonly label: string;
|
|
6
|
+
readonly createdAt: Date;
|
|
7
|
+
readonly lastUsedAt: Date | null;
|
|
8
|
+
}
|
|
9
|
+
export interface BegunEnrolment {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
/** Shown once, so a person can type it into an app that will not scan. */
|
|
12
|
+
readonly secret: string;
|
|
13
|
+
readonly otpauthUrl: string;
|
|
14
|
+
}
|
|
15
|
+
export type FactorKind = 'totp' | 'webauthn';
|
|
16
|
+
/**
|
|
17
|
+
* Where a WebAuthn ceremony happens, and it needs no new configuration.
|
|
18
|
+
*
|
|
19
|
+
* The relying party id is `NACRE_CANONICAL_URL`'s hostname and the origin list
|
|
20
|
+
* is that URL's origin plus whatever `NACRE_API_ALLOWED_ORIGINS` already
|
|
21
|
+
* admits — the console is served from the API's own origin unless a deployment
|
|
22
|
+
* has said otherwise, and if it has said otherwise it has said so there. A
|
|
23
|
+
* `NACRE_WEBAUTHN_*` variable would be a second answer to a question the
|
|
24
|
+
* deployment has already answered twice.
|
|
25
|
+
*/
|
|
26
|
+
export interface RelyingParty {
|
|
27
|
+
/** A registrable domain: `nacre.example.com`, never a URL and never a port. */
|
|
28
|
+
readonly id: string;
|
|
29
|
+
/** What the authenticator shows. */
|
|
30
|
+
readonly name: string;
|
|
31
|
+
/** Compared for equality. A suffix match would admit `evil-nacre.work`. */
|
|
32
|
+
readonly origins: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
export interface WebAuthnRegistrationOptions {
|
|
35
|
+
readonly challenge: string;
|
|
36
|
+
readonly rp: {
|
|
37
|
+
readonly id: string;
|
|
38
|
+
readonly name: string;
|
|
39
|
+
};
|
|
40
|
+
readonly user: {
|
|
41
|
+
readonly id: string;
|
|
42
|
+
readonly name: string;
|
|
43
|
+
readonly displayName: string;
|
|
44
|
+
};
|
|
45
|
+
readonly algorithms: readonly number[];
|
|
46
|
+
readonly excludeCredentials: readonly string[];
|
|
47
|
+
readonly timeoutMs: number;
|
|
48
|
+
}
|
|
49
|
+
export interface WebAuthnAssertionOptions {
|
|
50
|
+
readonly challenge: string;
|
|
51
|
+
readonly rpId: string;
|
|
52
|
+
readonly allowCredentials: readonly string[];
|
|
53
|
+
readonly timeoutMs: number;
|
|
54
|
+
}
|
|
55
|
+
export interface SecondFactorDeps {
|
|
56
|
+
readonly pool: Pool;
|
|
57
|
+
/**
|
|
58
|
+
* The sealing key, or `undefined` where the deployment configured none.
|
|
59
|
+
*
|
|
60
|
+
* **TOTP needs it and WebAuthn does not.** A TOTP secret is shared, so it has
|
|
61
|
+
* to be kept and therefore sealed; a WebAuthn credential leaves a public key
|
|
62
|
+
* here and nothing else, so there is nothing a dump could hand over. That is
|
|
63
|
+
* why `available` became per kind rather than staying one boolean: an
|
|
64
|
+
* installation with no key can still offer the *stronger* of the two, and
|
|
65
|
+
* refusing it because the weaker one has no key would be an accident of how
|
|
66
|
+
* this class was first written.
|
|
67
|
+
*/
|
|
68
|
+
readonly key: Buffer | undefined;
|
|
69
|
+
/** What an authenticator shows above the account. */
|
|
70
|
+
readonly issuer: string;
|
|
71
|
+
readonly relyingParty: RelyingParty;
|
|
72
|
+
readonly role?: string;
|
|
73
|
+
readonly now?: () => Date;
|
|
74
|
+
}
|
|
75
|
+
export declare class SecondFactors {
|
|
76
|
+
private readonly deps;
|
|
77
|
+
constructor(deps: SecondFactorDeps);
|
|
78
|
+
/**
|
|
79
|
+
* Whether this installation can hold a second factor at all.
|
|
80
|
+
*
|
|
81
|
+
* True where *either* kind is offered, which since WebAuthn is every
|
|
82
|
+
* installation: it needs no sealing key, only the canonical URL every
|
|
83
|
+
* deployment already sets.
|
|
84
|
+
*/
|
|
85
|
+
get available(): boolean;
|
|
86
|
+
/** The kinds this installation offers, so a screen can draw what works. */
|
|
87
|
+
get kinds(): readonly FactorKind[];
|
|
88
|
+
private get scope();
|
|
89
|
+
private get clock();
|
|
90
|
+
/**
|
|
91
|
+
* Does this person have to produce one?
|
|
92
|
+
*
|
|
93
|
+
* Only confirmed factors count. A secret generated and never proved is a
|
|
94
|
+
* secret that did not reach an authenticator, and treating it as live is how
|
|
95
|
+
* somebody locks themselves out at the moment they turn 2FA on.
|
|
96
|
+
*/
|
|
97
|
+
required(orgId: string, userId: string): Promise<boolean>;
|
|
98
|
+
list(orgId: string, userId: string): Promise<readonly EnrolledFactor[]>;
|
|
99
|
+
/**
|
|
100
|
+
* Start enrolling one. The secret is stored sealed and unconfirmed.
|
|
101
|
+
*
|
|
102
|
+
* A second call with the same label replaces the unconfirmed row rather than
|
|
103
|
+
* refusing: somebody who closed the page before scanning has no way to name
|
|
104
|
+
* what they abandoned, and the alternative is a label nobody can reuse.
|
|
105
|
+
*/
|
|
106
|
+
begin(orgId: string, userId: string, label: string): Promise<BegunEnrolment | undefined>;
|
|
107
|
+
/**
|
|
108
|
+
* Prove the secret arrived, and hand back the way in without it.
|
|
109
|
+
*
|
|
110
|
+
* The recovery codes are minted here rather than on demand for two reasons:
|
|
111
|
+
* a person who has lost their phone cannot ask for them, and this is the one
|
|
112
|
+
* moment the product has their attention about it.
|
|
113
|
+
*
|
|
114
|
+
* Replacing any codes that already exist — enrolling a second factor is not
|
|
115
|
+
* a reason to invalidate them, so this only runs when there was none.
|
|
116
|
+
*/
|
|
117
|
+
confirm(orgId: string, userId: string, id: string, code: string): Promise<readonly string[] | undefined>;
|
|
118
|
+
/**
|
|
119
|
+
* A code at sign-in: a six-digit one from an authenticator, or a recovery code.
|
|
120
|
+
*
|
|
121
|
+
* Both are accepted here rather than on two endpoints, because from the
|
|
122
|
+
* outside they answer the same question and a separate route would tell an
|
|
123
|
+
* attacker which one a person is using.
|
|
124
|
+
*
|
|
125
|
+
* The brute-force bound is in Postgres and not in Redis: the rate limiter
|
|
126
|
+
* fails **open** by design — it is not an authorization control and a cache
|
|
127
|
+
* restart must not be an outage — and this one is. Six digits is a million,
|
|
128
|
+
* and a limiter that forgets is a limiter an attacker waits out.
|
|
129
|
+
*/
|
|
130
|
+
verify(orgId: string, userId: string, code: string): Promise<boolean>;
|
|
131
|
+
/**
|
|
132
|
+
* Remove one. The caller has already proved they are this person and holds a
|
|
133
|
+
* current code — see the handler; removing a factor is exactly what somebody
|
|
134
|
+
* with a stolen session would do first.
|
|
135
|
+
*/
|
|
136
|
+
remove(orgId: string, userId: string, id: string): Promise<boolean>;
|
|
137
|
+
/**
|
|
138
|
+
* The address, for a notice about this account's factors.
|
|
139
|
+
*
|
|
140
|
+
* Here rather than on a users port because this class already reads that
|
|
141
|
+
* column for the authenticator label, and a second reader of one column is a
|
|
142
|
+
* second thing to keep scoped correctly.
|
|
143
|
+
*/
|
|
144
|
+
emailOf(orgId: string, userId: string): Promise<string | undefined>;
|
|
145
|
+
/** Mint and store a challenge. Returned base64url, which is what the browser wants. */
|
|
146
|
+
private issueChallenge;
|
|
147
|
+
/**
|
|
148
|
+
* Spend one, and refuse a challenge that is not outstanding for this person
|
|
149
|
+
* and this ceremony.
|
|
150
|
+
*
|
|
151
|
+
* The purpose is in the WHERE clause rather than checked afterwards: a
|
|
152
|
+
* challenge issued for enrolment must not be spendable on a sign-in, or a
|
|
153
|
+
* session could mint the input to a ceremony it is not in.
|
|
154
|
+
*/
|
|
155
|
+
private spendChallenge;
|
|
156
|
+
/**
|
|
157
|
+
* Start enrolling an authenticator.
|
|
158
|
+
*
|
|
159
|
+
* `excludeCredentials` carries what this person has already registered, so an
|
|
160
|
+
* authenticator they are holding refuses to make a second credential rather
|
|
161
|
+
* than quietly making one — which is the difference between "you already have
|
|
162
|
+
* this key" and two rows nobody can tell apart.
|
|
163
|
+
*/
|
|
164
|
+
beginWebAuthnRegistration(orgId: string, userId: string): Promise<WebAuthnRegistrationOptions | undefined>;
|
|
165
|
+
/** Every credential id this person has, for `allowCredentials` and `excludeCredentials`. */
|
|
166
|
+
private credentialIds;
|
|
167
|
+
/**
|
|
168
|
+
* Finish enrolling one, and hand back recovery codes if this is the first
|
|
169
|
+
* factor of any kind.
|
|
170
|
+
*
|
|
171
|
+
* Confirmed in the same statement that inserts it, unlike TOTP: there is no
|
|
172
|
+
* second step to prove the credential reached anything, because producing the
|
|
173
|
+
* attestation *is* that proof. A TOTP secret can be generated and never
|
|
174
|
+
* scanned; a WebAuthn credential cannot be registered without the
|
|
175
|
+
* authenticator having made it.
|
|
176
|
+
*/
|
|
177
|
+
finishWebAuthnRegistration(orgId: string, userId: string, label: string, response: {
|
|
178
|
+
readonly attestationObject: Uint8Array;
|
|
179
|
+
readonly clientDataJSON: Uint8Array;
|
|
180
|
+
readonly challenge: string;
|
|
181
|
+
}): Promise<readonly string[] | undefined>;
|
|
182
|
+
/** The options a sign-in needs, or nothing where this person has no key enrolled. */
|
|
183
|
+
beginWebAuthnAssertion(orgId: string, userId: string): Promise<WebAuthnAssertionOptions | undefined>;
|
|
184
|
+
/**
|
|
185
|
+
* Verify an assertion at sign-in.
|
|
186
|
+
*
|
|
187
|
+
* The counter is written back in the same transaction that spent the
|
|
188
|
+
* challenge, because a counter that is checked and not stored is a counter
|
|
189
|
+
* that only ever compares against zero — which is the clone detection not
|
|
190
|
+
* existing rather than being lenient.
|
|
191
|
+
*/
|
|
192
|
+
verifyWebAuthnAssertion(orgId: string, userId: string, response: {
|
|
193
|
+
readonly credentialId: string;
|
|
194
|
+
readonly authenticatorData: Uint8Array;
|
|
195
|
+
readonly clientDataJSON: Uint8Array;
|
|
196
|
+
readonly signature: Uint8Array;
|
|
197
|
+
readonly challenge: string;
|
|
198
|
+
}): Promise<boolean>;
|
|
199
|
+
/** How many unspent recovery codes are left, for the screen that says so. */
|
|
200
|
+
recoveryCodesLeft(orgId: string, userId: string): Promise<number>;
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=second-factor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"second-factor.d.ts","sourceRoot":"","sources":["../src/second-factor.ts"],"names":[],"mappings":"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"}
|