@better-forge/auth 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +4 -2
- package/dist/apiTokenRoutes.d.ts +8 -0
- package/dist/apiTokenRoutes.d.ts.map +1 -0
- package/dist/apiTokenRoutes.js +67 -0
- package/dist/apiTokenRoutes.js.map +1 -0
- package/dist/apiTokens.d.ts +25 -0
- package/dist/apiTokens.d.ts.map +1 -0
- package/dist/apiTokens.js +50 -0
- package/dist/apiTokens.js.map +1 -0
- package/dist/authRoutes.d.ts +37 -0
- package/dist/authRoutes.d.ts.map +1 -0
- package/dist/authRoutes.js +403 -0
- package/dist/authRoutes.js.map +1 -0
- package/dist/context.d.ts +98 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +2 -0
- package/dist/context.js.map +1 -0
- package/dist/createAuth.d.ts +146 -0
- package/dist/createAuth.d.ts.map +1 -0
- package/dist/createAuth.js +52 -0
- package/dist/createAuth.js.map +1 -0
- package/dist/identityConfirmation.d.ts +13 -0
- package/dist/identityConfirmation.d.ts.map +1 -0
- package/dist/identityConfirmation.js +37 -0
- package/dist/identityConfirmation.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware.d.ts +41 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +100 -0
- package/dist/middleware.js.map +1 -0
- package/dist/migrations.d.ts +4 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +77 -0
- package/dist/migrations.js.map +1 -0
- package/dist/module.d.ts +8 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +5 -0
- package/dist/module.js.map +1 -0
- package/dist/oidc.d.ts +85 -0
- package/dist/oidc.d.ts.map +1 -0
- package/dist/oidc.js +213 -0
- package/dist/oidc.js.map +1 -0
- package/dist/oidcConfig.d.ts +39 -0
- package/dist/oidcConfig.d.ts.map +1 -0
- package/dist/oidcConfig.js +72 -0
- package/dist/oidcConfig.js.map +1 -0
- package/dist/oidcSettings.d.ts +15 -0
- package/dist/oidcSettings.d.ts.map +1 -0
- package/dist/oidcSettings.js +51 -0
- package/dist/oidcSettings.js.map +1 -0
- package/dist/oidcSettingsRoutes.d.ts +19 -0
- package/dist/oidcSettingsRoutes.d.ts.map +1 -0
- package/dist/oidcSettingsRoutes.js +133 -0
- package/dist/oidcSettingsRoutes.js.map +1 -0
- package/dist/roles.d.ts +25 -0
- package/dist/roles.d.ts.map +1 -0
- package/dist/roles.js +44 -0
- package/dist/roles.js.map +1 -0
- package/dist/routeScope.d.ts +34 -0
- package/dist/routeScope.d.ts.map +1 -0
- package/dist/routeScope.js +32 -0
- package/dist/routeScope.js.map +1 -0
- package/dist/sessions.d.ts +46 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +159 -0
- package/dist/sessions.js.map +1 -0
- package/dist/setupRoutes.d.ts +37 -0
- package/dist/setupRoutes.d.ts.map +1 -0
- package/dist/setupRoutes.js +70 -0
- package/dist/setupRoutes.js.map +1 -0
- package/dist/signInAudit.d.ts +19 -0
- package/dist/signInAudit.d.ts.map +1 -0
- package/dist/signInAudit.js +16 -0
- package/dist/signInAudit.js.map +1 -0
- package/dist/tables.d.ts +716 -0
- package/dist/tables.d.ts.map +1 -0
- package/dist/tables.js +56 -0
- package/dist/tables.js.map +1 -0
- package/dist/tokenCatalogue.d.ts +21 -0
- package/dist/tokenCatalogue.d.ts.map +1 -0
- package/dist/tokenCatalogue.js +27 -0
- package/dist/tokenCatalogue.js.map +1 -0
- package/dist/totp.d.ts +12 -0
- package/dist/totp.d.ts.map +1 -0
- package/dist/totp.js +93 -0
- package/dist/totp.js.map +1 -0
- package/dist/twoFactor.d.ts +10 -0
- package/dist/twoFactor.d.ts.map +1 -0
- package/dist/twoFactor.js +43 -0
- package/dist/twoFactor.js.map +1 -0
- package/dist/userRoutes.d.ts +13 -0
- package/dist/userRoutes.d.ts.map +1 -0
- package/dist/userRoutes.js +135 -0
- package/dist/userRoutes.js.map +1 -0
- package/package.json +66 -4
- package/src/apiTokenRoutes.ts +82 -0
- package/src/apiTokens.ts +62 -0
- package/src/authRoutes.ts +452 -0
- package/src/context.ts +85 -0
- package/src/createAuth.ts +58 -0
- package/src/identityConfirmation.ts +50 -0
- package/src/index.ts +17 -0
- package/src/middleware.ts +123 -0
- package/src/migrations.ts +77 -0
- package/src/module.ts +6 -0
- package/src/oidc.ts +291 -0
- package/src/oidcConfig.ts +96 -0
- package/src/oidcSettings.ts +60 -0
- package/src/oidcSettingsRoutes.ts +165 -0
- package/src/roles.ts +60 -0
- package/src/routeScope.ts +50 -0
- package/src/sessions.ts +209 -0
- package/src/setupRoutes.ts +100 -0
- package/src/signInAudit.ts +43 -0
- package/src/tables.ts +59 -0
- package/src/tokenCatalogue.ts +42 -0
- package/src/totp.ts +96 -0
- package/src/twoFactor.ts +49 -0
- package/src/userRoutes.ts +144 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import bcrypt from 'bcryptjs';
|
|
2
|
+
import { eq } from 'drizzle-orm';
|
|
3
|
+
import { users } from './tables.js';
|
|
4
|
+
/**
|
|
5
|
+
* The first-run routes: whether the application still needs setup, and the one request that creates the administrator
|
|
6
|
+
* and finishes it. Setup is refused once it is done, and only one attempt runs at a time.
|
|
7
|
+
*/
|
|
8
|
+
export function createSetupRoutes(options) {
|
|
9
|
+
const { db } = options;
|
|
10
|
+
let inProgress = false;
|
|
11
|
+
return async function setupRoutes(app, overrides = {}) {
|
|
12
|
+
app.get('/status', async () => ({ needsSetup: !options.isSetupComplete() }));
|
|
13
|
+
app.post('/complete', {
|
|
14
|
+
config: { rateLimit: options.rateLimit },
|
|
15
|
+
}, async (req, reply) => {
|
|
16
|
+
if (options.isSetupComplete()) {
|
|
17
|
+
return reply.code(409).send({ error: 'Setup already completed' });
|
|
18
|
+
}
|
|
19
|
+
const { email, password } = req.body ?? {};
|
|
20
|
+
if (typeof email !== 'string' || typeof password !== 'string' || !email || !password) {
|
|
21
|
+
return reply.code(400).send({ error: 'Email and password are required' });
|
|
22
|
+
}
|
|
23
|
+
if (email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
24
|
+
return reply.code(400).send({ error: 'A valid email address is required' });
|
|
25
|
+
}
|
|
26
|
+
if (password.length < 8 || password.length > 128) {
|
|
27
|
+
return reply.code(400).send({ error: 'Password must be between 8 and 128 characters' });
|
|
28
|
+
}
|
|
29
|
+
if (inProgress) {
|
|
30
|
+
return reply.code(409).send({ error: 'Setup is already in progress' });
|
|
31
|
+
}
|
|
32
|
+
inProgress = true;
|
|
33
|
+
try {
|
|
34
|
+
// Initialize DB and run migrations now that we have a confirmed DB type
|
|
35
|
+
options.prepareDatabase();
|
|
36
|
+
const hash = await bcrypt.hash(password, 10);
|
|
37
|
+
let setupMarkerWritten = false;
|
|
38
|
+
const user = await options.withTransaction(async () => {
|
|
39
|
+
// The marker can be missing while the data is not (a lost volume, a restore). Accounts that already
|
|
40
|
+
// exist mean setup was done: never create or overwrite an administrator over them.
|
|
41
|
+
if (!options.allowExistingUsers?.() && (await db.select({ id: users.id }).from(users).limit(1)).length > 0)
|
|
42
|
+
return null;
|
|
43
|
+
await db.insert(users).values({ email, passwordHash: hash, role: 'admin', createdAt: Date.now() });
|
|
44
|
+
await options.onSetup?.();
|
|
45
|
+
const admin = (await db.select().from(users).where(eq(users.email, email)))[0];
|
|
46
|
+
// Last step: if writing the marker fails the admin insert rolls back with it.
|
|
47
|
+
options.writeSetupComplete();
|
|
48
|
+
setupMarkerWritten = true;
|
|
49
|
+
return admin;
|
|
50
|
+
}).catch((error) => {
|
|
51
|
+
// COMMIT itself failed after the marker was written — undo it so setup can be retried.
|
|
52
|
+
if (setupMarkerWritten)
|
|
53
|
+
options.removeSetupMarker();
|
|
54
|
+
throw error;
|
|
55
|
+
});
|
|
56
|
+
if (!user)
|
|
57
|
+
return reply.code(409).send({ error: 'Setup already completed' });
|
|
58
|
+
const startServices = overrides.startBackgroundServices ?? options.startBackgroundServices;
|
|
59
|
+
startServices?.();
|
|
60
|
+
const identity = await options.createSession(app, reply, user);
|
|
61
|
+
const { sessionId: _sessionId, ...safeIdentity } = identity;
|
|
62
|
+
return safeIdentity;
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
inProgress = false;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=setupRoutes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setupRoutes.js","sourceRoot":"","sources":["../src/setupRoutes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AuthOptions } from './context.js';
|
|
2
|
+
/** How the user tried to sign in, as recorded in `diff.method` of 'sign_in' audit entries. */
|
|
3
|
+
export type SignInMethod = 'password' | 'sso';
|
|
4
|
+
/** Why a password, or the authentication-code step of any sign-in, was refused. SSO refusals use the OIDC codes. */
|
|
5
|
+
export type CredentialDenialCode = 'password_login_disabled' | 'no_matching_account' | 'wrong_password' | 'two_factor_expired' | 'invalid_two_factor_code';
|
|
6
|
+
export interface SignInDenial {
|
|
7
|
+
code: string;
|
|
8
|
+
reason: string;
|
|
9
|
+
email?: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
export declare function createSignInAudit(ctx: AuthOptions): {
|
|
12
|
+
auditSignIn: (user: {
|
|
13
|
+
id: number;
|
|
14
|
+
email: string;
|
|
15
|
+
}, method: SignInMethod, twoFactor: boolean) => Promise<void>;
|
|
16
|
+
auditSignInDenial: (method: SignInMethod, denial: SignInDenial, extra?: Record<string, unknown>) => Promise<void>;
|
|
17
|
+
};
|
|
18
|
+
export type SignInAudit = ReturnType<typeof createSignInAudit>;
|
|
19
|
+
//# sourceMappingURL=signInAudit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signInAudit.d.ts","sourceRoot":"","sources":["../src/signInAudit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE/C,8FAA8F;AAC9F,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,KAAK,CAAA;AAE7C,oHAAoH;AACpH,MAAM,MAAM,oBAAoB,GAC5B,yBAAyB,GACzB,qBAAqB,GACrB,gBAAgB,GAChB,oBAAoB,GACpB,yBAAyB,CAAA;AAE7B,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC3B;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,WAAW;wBAIf;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,UAAU,YAAY,aAAa,OAAO,KAAG,OAAO,CAAC,IAAI,CAAC;gCAQ/E,YAAY,UAAU,YAAY,UAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAQ,OAAO,CAAC,IAAI,CAAC;EASjI;AAED,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAA"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function createSignInAudit(ctx) {
|
|
2
|
+
const { audit } = ctx;
|
|
3
|
+
/** Records a successful sign-in; `twoFactor` says whether an authentication code was part of it. */
|
|
4
|
+
async function auditSignIn(user, method, twoFactor) {
|
|
5
|
+
await audit.write({ userId: user.id, userEmail: user.email }, 'allow', 'sign_in', user.id, user.email, { method, twoFactor });
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Records a refused sign-in. Nobody is signed in, so the actor is user 0 with the email that was entered or that
|
|
9
|
+
* the provider asserted, when there was one. The user only ever sees a generic message; this holds the reason.
|
|
10
|
+
*/
|
|
11
|
+
async function auditSignInDenial(method, denial, extra = {}) {
|
|
12
|
+
await audit.write({ userId: 0, userEmail: denial.email ?? `unknown (${method === 'sso' ? 'SSO' : 'password'})` }, 'deny', 'sign_in', null, denial.email ?? (method === 'sso' ? 'SSO sign-in' : 'Password sign-in'), { method, code: denial.code, reason: denial.reason, ...extra });
|
|
13
|
+
}
|
|
14
|
+
return { auditSignIn, auditSignInDenial };
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=signInAudit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signInAudit.js","sourceRoot":"","sources":["../src/signInAudit.ts"],"names":[],"mappings":"AAmBA,MAAM,UAAU,iBAAiB,CAAC,GAAgB;IAChD,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,CAAA;IAErB,oGAAoG;IACpG,KAAK,UAAU,WAAW,CAAC,IAAmC,EAAE,MAAoB,EAAE,SAAkB;QACtG,MAAM,KAAK,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,KAAK,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAA;IAC/H,CAAC;IAED;;;OAGG;IACH,KAAK,UAAU,iBAAiB,CAAC,MAAoB,EAAE,MAAoB,EAAE,QAAiC,EAAE;QAC9G,MAAM,KAAK,CAAC,KAAK,CACf,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,KAAK,IAAI,YAAY,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,GAAG,EAAE,EAC9F,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,CAAC,KAAK,IAAI,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,kBAAkB,CAAC,EAChG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,KAAK,EAAE,CAC/D,CAAA;IACH,CAAC;IAED,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,CAAA;AAC3C,CAAC"}
|