or3-provider-basic-auth 0.0.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/README.md +99 -0
- package/dist/module.d.mts +5 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +42 -0
- package/dist/runtime/components/BasicAuthChangePasswordModal.client.d.vue.ts +12 -0
- package/dist/runtime/components/BasicAuthChangePasswordModal.client.vue +133 -0
- package/dist/runtime/components/BasicAuthChangePasswordModal.client.vue.d.ts +12 -0
- package/dist/runtime/components/BasicAuthRegisterModal.client.d.vue.ts +11 -0
- package/dist/runtime/components/BasicAuthRegisterModal.client.vue +164 -0
- package/dist/runtime/components/BasicAuthRegisterModal.client.vue.d.ts +11 -0
- package/dist/runtime/components/BasicAuthSignInModal.client.d.vue.ts +13 -0
- package/dist/runtime/components/BasicAuthSignInModal.client.vue +136 -0
- package/dist/runtime/components/BasicAuthSignInModal.client.vue.d.ts +13 -0
- package/dist/runtime/components/BasicAuthUserMenu.client.d.vue.ts +12 -0
- package/dist/runtime/components/BasicAuthUserMenu.client.vue +106 -0
- package/dist/runtime/components/BasicAuthUserMenu.client.vue.d.ts +12 -0
- package/dist/runtime/components/SidebarAuthButtonBasic.client.d.vue.ts +2 -0
- package/dist/runtime/components/SidebarAuthButtonBasic.client.vue +154 -0
- package/dist/runtime/components/SidebarAuthButtonBasic.client.vue.d.ts +2 -0
- package/dist/runtime/lib/constants.d.ts +7 -0
- package/dist/runtime/lib/constants.js +7 -0
- package/dist/runtime/plugins/auth-status.client.d.ts +2 -0
- package/dist/runtime/plugins/auth-status.client.js +38 -0
- package/dist/runtime/plugins/basic-auth-ui.client.d.ts +2 -0
- package/dist/runtime/plugins/basic-auth-ui.client.js +46 -0
- package/dist/runtime/server/admin/adapters/auth-basic-auth.d.ts +2 -0
- package/dist/runtime/server/admin/adapters/auth-basic-auth.js +34 -0
- package/dist/runtime/server/api/basic-auth/_helpers.d.ts +6 -0
- package/dist/runtime/server/api/basic-auth/_helpers.js +26 -0
- package/dist/runtime/server/api/basic-auth/change-password.post.d.ts +8 -0
- package/dist/runtime/server/api/basic-auth/change-password.post.js +49 -0
- package/dist/runtime/server/api/basic-auth/refresh.post.d.ts +8 -0
- package/dist/runtime/server/api/basic-auth/refresh.post.js +78 -0
- package/dist/runtime/server/api/basic-auth/register.post.d.ts +8 -0
- package/dist/runtime/server/api/basic-auth/register.post.js +112 -0
- package/dist/runtime/server/api/basic-auth/sign-in.post.d.ts +8 -0
- package/dist/runtime/server/api/basic-auth/sign-in.post.js +75 -0
- package/dist/runtime/server/api/basic-auth/sign-out.post.d.ts +8 -0
- package/dist/runtime/server/api/basic-auth/sign-out.post.js +37 -0
- package/dist/runtime/server/auth/basic-auth-provider.d.ts +2 -0
- package/dist/runtime/server/auth/basic-auth-provider.js +41 -0
- package/dist/runtime/server/auth/index.d.ts +1 -0
- package/dist/runtime/server/auth/index.js +1 -0
- package/dist/runtime/server/db/client.d.ts +3 -0
- package/dist/runtime/server/db/client.js +106 -0
- package/dist/runtime/server/db/schema.d.ts +21 -0
- package/dist/runtime/server/db/schema.js +0 -0
- package/dist/runtime/server/lib/config.d.ts +22 -0
- package/dist/runtime/server/lib/config.js +94 -0
- package/dist/runtime/server/lib/cookies.d.ts +4 -0
- package/dist/runtime/server/lib/cookies.js +42 -0
- package/dist/runtime/server/lib/errors.d.ts +4 -0
- package/dist/runtime/server/lib/errors.js +25 -0
- package/dist/runtime/server/lib/jwt.d.ts +37 -0
- package/dist/runtime/server/lib/jwt.js +77 -0
- package/dist/runtime/server/lib/password.d.ts +2 -0
- package/dist/runtime/server/lib/password.js +8 -0
- package/dist/runtime/server/lib/rate-limit.d.ts +6 -0
- package/dist/runtime/server/lib/rate-limit.js +163 -0
- package/dist/runtime/server/lib/request-identity.d.ts +16 -0
- package/dist/runtime/server/lib/request-identity.js +32 -0
- package/dist/runtime/server/lib/request-security.d.ts +2 -0
- package/dist/runtime/server/lib/request-security.js +37 -0
- package/dist/runtime/server/lib/session-store.d.ts +46 -0
- package/dist/runtime/server/lib/session-store.js +190 -0
- package/dist/runtime/server/plugins/register.d.ts +2 -0
- package/dist/runtime/server/plugins/register.js +63 -0
- package/dist/runtime/server/token-broker/basic-auth-token-broker.d.ts +6 -0
- package/dist/runtime/server/token-broker/basic-auth-token-broker.js +8 -0
- package/dist/runtime/server/token-broker/index.d.ts +1 -0
- package/dist/runtime/server/token-broker/index.js +1 -0
- package/dist/types.d.mts +7 -0
- package/package.json +70 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createError, defineEventHandler, setCookie } from "h3";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { clearAuthCookies, setAccessCookie, setRefreshCookie } from "../../lib/cookies.js";
|
|
5
|
+
import { getBasicAuthConfig } from "../../lib/config.js";
|
|
6
|
+
import {
|
|
7
|
+
createInvalidRequestError,
|
|
8
|
+
createInvalidCredentialsError
|
|
9
|
+
} from "../../lib/errors.js";
|
|
10
|
+
import { signAccessToken, signRefreshToken, hashRefreshToken } from "../../lib/jwt.js";
|
|
11
|
+
import { hashPassword } from "../../lib/password.js";
|
|
12
|
+
import { enforceBasicAuthRateLimit } from "../../lib/rate-limit.js";
|
|
13
|
+
import {
|
|
14
|
+
createAccount,
|
|
15
|
+
createAuthSession,
|
|
16
|
+
findAccountByEmail,
|
|
17
|
+
normalizeEmail,
|
|
18
|
+
getSessionMetadataFromEvent
|
|
19
|
+
} from "../../lib/session-store.js";
|
|
20
|
+
import { enforceMutationOriginPolicy } from "../../lib/request-security.js";
|
|
21
|
+
import { assertBasicAuthReady, noStore, parseBodyWithSchema } from "./_helpers.js";
|
|
22
|
+
const registerSchema = z.object({
|
|
23
|
+
email: z.string().email().max(320),
|
|
24
|
+
password: z.string().min(8).max(512),
|
|
25
|
+
confirmPassword: z.string().min(8).max(512),
|
|
26
|
+
displayName: z.string().trim().min(1).max(120).optional(),
|
|
27
|
+
inviteToken: z.string().trim().min(1).max(4096).optional()
|
|
28
|
+
}).refine((value) => value.password === value.confirmPassword, {
|
|
29
|
+
message: "Passwords must match"
|
|
30
|
+
});
|
|
31
|
+
function mapRegistrationErrorToHttp(reason) {
|
|
32
|
+
if (reason === "disabled") {
|
|
33
|
+
throw createError({
|
|
34
|
+
statusCode: 403,
|
|
35
|
+
statusMessage: "Registration is currently disabled. Please contact an administrator."
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
throw createError({
|
|
39
|
+
statusCode: 403,
|
|
40
|
+
statusMessage: "A valid invite is required to register."
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export async function handleRegister(event) {
|
|
44
|
+
assertBasicAuthReady(event);
|
|
45
|
+
noStore(event);
|
|
46
|
+
enforceMutationOriginPolicy(event);
|
|
47
|
+
enforceBasicAuthRateLimit(event, "basic-auth:register");
|
|
48
|
+
const body = await parseBodyWithSchema(event, registerSchema);
|
|
49
|
+
const email = normalizeEmail(body.email);
|
|
50
|
+
const existing = findAccountByEmail(email);
|
|
51
|
+
if (existing) {
|
|
52
|
+
throw createInvalidCredentialsError();
|
|
53
|
+
}
|
|
54
|
+
const runtimeConfig = useRuntimeConfig();
|
|
55
|
+
const mode = runtimeConfig.auth?.registrationMode ?? (runtimeConfig.auth?.autoProvision === false ? "disabled" : "open");
|
|
56
|
+
if (mode === "disabled") {
|
|
57
|
+
mapRegistrationErrorToHttp("disabled");
|
|
58
|
+
}
|
|
59
|
+
if (mode === "invite_only" && !body.inviteToken?.trim()) {
|
|
60
|
+
mapRegistrationErrorToHttp("invite_required");
|
|
61
|
+
}
|
|
62
|
+
const passwordHash = await hashPassword(body.password);
|
|
63
|
+
const account = createAccount({
|
|
64
|
+
email,
|
|
65
|
+
passwordHash,
|
|
66
|
+
displayName: body.displayName?.trim() || null
|
|
67
|
+
});
|
|
68
|
+
const config = getBasicAuthConfig();
|
|
69
|
+
if (config.refreshTtlSeconds <= 0 || config.accessTtlSeconds <= 0) {
|
|
70
|
+
throw createInvalidRequestError();
|
|
71
|
+
}
|
|
72
|
+
const sessionId = randomUUID();
|
|
73
|
+
const refreshToken = await signRefreshToken({
|
|
74
|
+
sub: account.id,
|
|
75
|
+
sid: sessionId,
|
|
76
|
+
ver: account.token_version
|
|
77
|
+
});
|
|
78
|
+
createAuthSession({
|
|
79
|
+
accountId: account.id,
|
|
80
|
+
sessionId,
|
|
81
|
+
refreshTokenHash: hashRefreshToken(refreshToken),
|
|
82
|
+
expiresAtMs: Date.now() + config.refreshTtlSeconds * 1e3,
|
|
83
|
+
metadata: getSessionMetadataFromEvent(event)
|
|
84
|
+
});
|
|
85
|
+
const accessToken = await signAccessToken({
|
|
86
|
+
sub: account.id,
|
|
87
|
+
sid: sessionId,
|
|
88
|
+
ver: account.token_version,
|
|
89
|
+
email: account.email,
|
|
90
|
+
display_name: account.display_name
|
|
91
|
+
});
|
|
92
|
+
setAccessCookie(event, accessToken, config.accessTtlSeconds);
|
|
93
|
+
setRefreshCookie(event, refreshToken, config.refreshTtlSeconds);
|
|
94
|
+
if (mode === "invite_only" && body.inviteToken?.trim()) {
|
|
95
|
+
setCookie(event, "or3_invite_token", body.inviteToken.trim(), {
|
|
96
|
+
httpOnly: true,
|
|
97
|
+
sameSite: "lax",
|
|
98
|
+
secure: process.env.NODE_ENV === "production",
|
|
99
|
+
path: "/",
|
|
100
|
+
maxAge: 10 * 60
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return { ok: true };
|
|
104
|
+
}
|
|
105
|
+
export default defineEventHandler(async (event) => {
|
|
106
|
+
try {
|
|
107
|
+
return await handleRegister(event);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
clearAuthCookies(event);
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type H3Event } from 'h3';
|
|
2
|
+
export declare function handleSignIn(event: H3Event): Promise<{
|
|
3
|
+
ok: boolean;
|
|
4
|
+
}>;
|
|
5
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
|
|
6
|
+
ok: boolean;
|
|
7
|
+
}>>;
|
|
8
|
+
export default _default;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { defineEventHandler } from "h3";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { clearAuthCookies, setAccessCookie, setRefreshCookie } from "../../lib/cookies.js";
|
|
5
|
+
import { getBasicAuthConfig } from "../../lib/config.js";
|
|
6
|
+
import {
|
|
7
|
+
createInvalidCredentialsError,
|
|
8
|
+
createInvalidRequestError
|
|
9
|
+
} from "../../lib/errors.js";
|
|
10
|
+
import { signAccessToken, signRefreshToken, hashRefreshToken } from "../../lib/jwt.js";
|
|
11
|
+
import { verifyPassword } from "../../lib/password.js";
|
|
12
|
+
import { enforceBasicAuthRateLimit } from "../../lib/rate-limit.js";
|
|
13
|
+
import {
|
|
14
|
+
createAuthSession,
|
|
15
|
+
findAccountByEmail,
|
|
16
|
+
normalizeEmail,
|
|
17
|
+
getSessionMetadataFromEvent
|
|
18
|
+
} from "../../lib/session-store.js";
|
|
19
|
+
import { enforceMutationOriginPolicy } from "../../lib/request-security.js";
|
|
20
|
+
import { assertBasicAuthReady, noStore, parseBodyWithSchema } from "./_helpers.js";
|
|
21
|
+
const signInSchema = z.object({
|
|
22
|
+
email: z.string().email().max(320),
|
|
23
|
+
// Sign-in must allow legacy short passwords; strength is enforced at set/change time.
|
|
24
|
+
password: z.string().min(1).max(512)
|
|
25
|
+
});
|
|
26
|
+
const DUMMY_PASSWORD_HASH = "$2a$12$1XCXpgmbzURDdc0AGJiAsemtT39PAw8WwV7fBOq6A5VQv6.qN6Va.";
|
|
27
|
+
export async function handleSignIn(event) {
|
|
28
|
+
assertBasicAuthReady(event);
|
|
29
|
+
noStore(event);
|
|
30
|
+
enforceMutationOriginPolicy(event);
|
|
31
|
+
enforceBasicAuthRateLimit(event, "basic-auth:sign-in");
|
|
32
|
+
const input = await parseBodyWithSchema(event, signInSchema);
|
|
33
|
+
const email = normalizeEmail(input.email);
|
|
34
|
+
const account = findAccountByEmail(email);
|
|
35
|
+
if (!account) {
|
|
36
|
+
await verifyPassword(input.password, DUMMY_PASSWORD_HASH);
|
|
37
|
+
clearAuthCookies(event);
|
|
38
|
+
throw createInvalidCredentialsError();
|
|
39
|
+
}
|
|
40
|
+
const isPasswordValid = await verifyPassword(input.password, account.password_hash);
|
|
41
|
+
if (!isPasswordValid) {
|
|
42
|
+
clearAuthCookies(event);
|
|
43
|
+
throw createInvalidCredentialsError();
|
|
44
|
+
}
|
|
45
|
+
const config = getBasicAuthConfig();
|
|
46
|
+
if (config.refreshTtlSeconds <= 0 || config.accessTtlSeconds <= 0) {
|
|
47
|
+
throw createInvalidRequestError();
|
|
48
|
+
}
|
|
49
|
+
const sessionId = randomUUID();
|
|
50
|
+
const refreshToken = await signRefreshToken({
|
|
51
|
+
sub: account.id,
|
|
52
|
+
sid: sessionId,
|
|
53
|
+
ver: account.token_version
|
|
54
|
+
});
|
|
55
|
+
createAuthSession({
|
|
56
|
+
accountId: account.id,
|
|
57
|
+
sessionId,
|
|
58
|
+
refreshTokenHash: hashRefreshToken(refreshToken),
|
|
59
|
+
expiresAtMs: Date.now() + config.refreshTtlSeconds * 1e3,
|
|
60
|
+
metadata: getSessionMetadataFromEvent(event)
|
|
61
|
+
});
|
|
62
|
+
const accessToken = await signAccessToken({
|
|
63
|
+
sub: account.id,
|
|
64
|
+
sid: sessionId,
|
|
65
|
+
ver: account.token_version,
|
|
66
|
+
email: account.email,
|
|
67
|
+
display_name: account.display_name
|
|
68
|
+
});
|
|
69
|
+
setAccessCookie(event, accessToken, config.accessTtlSeconds);
|
|
70
|
+
setRefreshCookie(event, refreshToken, config.refreshTtlSeconds);
|
|
71
|
+
return { ok: true };
|
|
72
|
+
}
|
|
73
|
+
export default defineEventHandler(async (event) => {
|
|
74
|
+
return await handleSignIn(event);
|
|
75
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type H3Event } from 'h3';
|
|
2
|
+
export declare function handleSignOut(event: H3Event): Promise<{
|
|
3
|
+
ok: boolean;
|
|
4
|
+
}>;
|
|
5
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
|
|
6
|
+
ok: boolean;
|
|
7
|
+
}>>;
|
|
8
|
+
export default _default;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { defineEventHandler } from "h3";
|
|
2
|
+
import { clearAuthCookies } from "../../lib/cookies.js";
|
|
3
|
+
import {
|
|
4
|
+
getAccessTokenFromEvent,
|
|
5
|
+
getRefreshTokenFromEvent,
|
|
6
|
+
verifyAccessToken,
|
|
7
|
+
verifyRefreshToken
|
|
8
|
+
} from "../../lib/jwt.js";
|
|
9
|
+
import { enforceBasicAuthRateLimit } from "../../lib/rate-limit.js";
|
|
10
|
+
import { revokeSessionById } from "../../lib/session-store.js";
|
|
11
|
+
import { enforceMutationOriginPolicy } from "../../lib/request-security.js";
|
|
12
|
+
import { assertBasicAuthReady, noStore } from "./_helpers.js";
|
|
13
|
+
export async function handleSignOut(event) {
|
|
14
|
+
assertBasicAuthReady(event);
|
|
15
|
+
noStore(event);
|
|
16
|
+
enforceMutationOriginPolicy(event);
|
|
17
|
+
enforceBasicAuthRateLimit(event, "basic-auth:sign-out");
|
|
18
|
+
const refreshToken = getRefreshTokenFromEvent(event);
|
|
19
|
+
if (refreshToken) {
|
|
20
|
+
const refreshClaims = await verifyRefreshToken(refreshToken);
|
|
21
|
+
if (refreshClaims) {
|
|
22
|
+
revokeSessionById(refreshClaims.sid);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const accessToken = getAccessTokenFromEvent(event);
|
|
26
|
+
if (accessToken) {
|
|
27
|
+
const accessClaims = await verifyAccessToken(accessToken);
|
|
28
|
+
if (accessClaims) {
|
|
29
|
+
revokeSessionById(accessClaims.sid);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
clearAuthCookies(event);
|
|
33
|
+
return { ok: true };
|
|
34
|
+
}
|
|
35
|
+
export default defineEventHandler(async (event) => {
|
|
36
|
+
return await handleSignOut(event);
|
|
37
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { BASIC_AUTH_PROVIDER_ID } from "../../lib/constants.js";
|
|
2
|
+
import { getAccessTokenFromEvent, verifyAccessToken } from "../lib/jwt.js";
|
|
3
|
+
import {
|
|
4
|
+
findAccountById,
|
|
5
|
+
findSessionById,
|
|
6
|
+
isSessionUsable
|
|
7
|
+
} from "../lib/session-store.js";
|
|
8
|
+
export const basicAuthProvider = {
|
|
9
|
+
name: BASIC_AUTH_PROVIDER_ID,
|
|
10
|
+
async getSession(event) {
|
|
11
|
+
const accessToken = getAccessTokenFromEvent(event);
|
|
12
|
+
if (!accessToken) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const payload = await verifyAccessToken(accessToken);
|
|
16
|
+
if (!payload) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const session = findSessionById(payload.sid);
|
|
20
|
+
if (!isSessionUsable(session)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const account = findAccountById(payload.sub);
|
|
24
|
+
if (!account) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
if (account.token_version !== payload.ver) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
provider: BASIC_AUTH_PROVIDER_ID,
|
|
32
|
+
user: {
|
|
33
|
+
id: payload.sub,
|
|
34
|
+
email: payload.email ?? account.email,
|
|
35
|
+
displayName: payload.display_name ?? account.display_name ?? void 0
|
|
36
|
+
},
|
|
37
|
+
expiresAt: new Date((payload.exp ?? 0) * 1e3),
|
|
38
|
+
claims: payload
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { basicAuthProvider } from './basic-auth-provider.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { basicAuthProvider } from "./basic-auth-provider.js";
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { dirname } from "node:path";
|
|
2
|
+
import { chmodSync, mkdirSync } from "node:fs";
|
|
3
|
+
import Database from "better-sqlite3";
|
|
4
|
+
import { getBasicAuthConfig } from "../lib/config.js";
|
|
5
|
+
let dbSingleton = null;
|
|
6
|
+
const DB_DIR_MODE = 448;
|
|
7
|
+
const DB_FILE_MODE = 384;
|
|
8
|
+
function hardenDbDirectoryPermissions(dbPath) {
|
|
9
|
+
if (dbPath === ":memory:" || process.platform === "win32") return;
|
|
10
|
+
const dir = dirname(dbPath);
|
|
11
|
+
mkdirSync(dir, { recursive: true, mode: DB_DIR_MODE });
|
|
12
|
+
chmodSync(dir, DB_DIR_MODE);
|
|
13
|
+
}
|
|
14
|
+
function hardenDbFilePermissions(dbPath) {
|
|
15
|
+
if (dbPath === ":memory:" || process.platform === "win32") return;
|
|
16
|
+
chmodSync(dbPath, DB_FILE_MODE);
|
|
17
|
+
}
|
|
18
|
+
function runMigrations(db) {
|
|
19
|
+
const versionRow = db.prepare("PRAGMA user_version").get();
|
|
20
|
+
const version = typeof versionRow?.user_version === "number" ? versionRow.user_version : 0;
|
|
21
|
+
if (version < 1) {
|
|
22
|
+
db.exec(`
|
|
23
|
+
CREATE TABLE IF NOT EXISTS basic_auth_accounts (
|
|
24
|
+
id TEXT PRIMARY KEY,
|
|
25
|
+
email TEXT NOT NULL UNIQUE,
|
|
26
|
+
password_hash TEXT NOT NULL,
|
|
27
|
+
display_name TEXT,
|
|
28
|
+
token_version INTEGER NOT NULL DEFAULT 0,
|
|
29
|
+
created_at INTEGER NOT NULL,
|
|
30
|
+
updated_at INTEGER NOT NULL
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
CREATE TABLE IF NOT EXISTS basic_auth_sessions (
|
|
34
|
+
id TEXT PRIMARY KEY,
|
|
35
|
+
account_id TEXT NOT NULL,
|
|
36
|
+
refresh_token_hash TEXT NOT NULL,
|
|
37
|
+
expires_at INTEGER NOT NULL,
|
|
38
|
+
revoked_at INTEGER,
|
|
39
|
+
created_at INTEGER NOT NULL,
|
|
40
|
+
rotated_from_session_id TEXT,
|
|
41
|
+
replaced_by_session_id TEXT,
|
|
42
|
+
ip_address TEXT,
|
|
43
|
+
user_agent TEXT,
|
|
44
|
+
FOREIGN KEY(account_id) REFERENCES basic_auth_accounts(id)
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
CREATE INDEX IF NOT EXISTS idx_basic_auth_sessions_account_id
|
|
48
|
+
ON basic_auth_sessions(account_id);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_basic_auth_sessions_expires_at
|
|
50
|
+
ON basic_auth_sessions(expires_at);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_basic_auth_sessions_rotated_from
|
|
52
|
+
ON basic_auth_sessions(rotated_from_session_id);
|
|
53
|
+
`);
|
|
54
|
+
db.pragma("user_version = 1");
|
|
55
|
+
}
|
|
56
|
+
if (version < 2) {
|
|
57
|
+
db.exec(`
|
|
58
|
+
CREATE TABLE IF NOT EXISTS basic_auth_rate_limits (
|
|
59
|
+
key TEXT PRIMARY KEY,
|
|
60
|
+
subject TEXT NOT NULL,
|
|
61
|
+
operation TEXT NOT NULL,
|
|
62
|
+
window_started_at INTEGER NOT NULL,
|
|
63
|
+
request_count INTEGER NOT NULL,
|
|
64
|
+
updated_at INTEGER NOT NULL
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_basic_auth_rate_limits_updated_at
|
|
68
|
+
ON basic_auth_rate_limits(updated_at);
|
|
69
|
+
`);
|
|
70
|
+
db.pragma("user_version = 2");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function getBasicAuthDb() {
|
|
74
|
+
if (dbSingleton) return dbSingleton;
|
|
75
|
+
const config = getBasicAuthConfig();
|
|
76
|
+
if (config.dbPath !== ":memory:") {
|
|
77
|
+
try {
|
|
78
|
+
hardenDbDirectoryPermissions(config.dbPath);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`[basic-auth] Failed to secure DB directory permissions for "${dirname(config.dbPath)}": ${error.message}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const db = new Database(config.dbPath);
|
|
86
|
+
db.pragma("journal_mode = WAL");
|
|
87
|
+
db.pragma("foreign_keys = ON");
|
|
88
|
+
if (config.dbPath !== ":memory:") {
|
|
89
|
+
try {
|
|
90
|
+
hardenDbFilePermissions(config.dbPath);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
db.close();
|
|
93
|
+
throw new Error(
|
|
94
|
+
`[basic-auth] Failed to secure DB file permissions for "${config.dbPath}": ${error.message}`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
runMigrations(db);
|
|
99
|
+
dbSingleton = db;
|
|
100
|
+
return dbSingleton;
|
|
101
|
+
}
|
|
102
|
+
export function resetBasicAuthDbForTests() {
|
|
103
|
+
if (!dbSingleton) return;
|
|
104
|
+
dbSingleton.close();
|
|
105
|
+
dbSingleton = null;
|
|
106
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface BasicAuthAccount {
|
|
2
|
+
id: string;
|
|
3
|
+
email: string;
|
|
4
|
+
password_hash: string;
|
|
5
|
+
display_name: string | null;
|
|
6
|
+
token_version: number;
|
|
7
|
+
created_at: number;
|
|
8
|
+
updated_at: number;
|
|
9
|
+
}
|
|
10
|
+
export interface BasicAuthSession {
|
|
11
|
+
id: string;
|
|
12
|
+
account_id: string;
|
|
13
|
+
refresh_token_hash: string;
|
|
14
|
+
expires_at: number;
|
|
15
|
+
revoked_at: number | null;
|
|
16
|
+
created_at: number;
|
|
17
|
+
rotated_from_session_id: string | null;
|
|
18
|
+
replaced_by_session_id: string | null;
|
|
19
|
+
ip_address: string | null;
|
|
20
|
+
user_agent: string | null;
|
|
21
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const BASIC_AUTH_INSECURE_DEV_ESCAPE_HATCH_ENV = "OR3_BASIC_AUTH_ALLOW_INSECURE_DEV";
|
|
2
|
+
export interface BasicAuthConfig {
|
|
3
|
+
providerId: string;
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
strict: boolean;
|
|
6
|
+
jwtSecret: string;
|
|
7
|
+
refreshSecret: string;
|
|
8
|
+
accessTtlSeconds: number;
|
|
9
|
+
refreshTtlSeconds: number;
|
|
10
|
+
dbPath: string;
|
|
11
|
+
bootstrapEmail?: string;
|
|
12
|
+
bootstrapPassword?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface BasicAuthConfigDiagnostics {
|
|
15
|
+
isValid: boolean;
|
|
16
|
+
errors: string[];
|
|
17
|
+
warnings: string[];
|
|
18
|
+
config: BasicAuthConfig;
|
|
19
|
+
}
|
|
20
|
+
export declare function isBasicAuthInsecureDevEscapeHatchEnabled(): boolean;
|
|
21
|
+
export declare function getBasicAuthConfig(runtimeConfig?: ReturnType<typeof useRuntimeConfig>): BasicAuthConfig;
|
|
22
|
+
export declare function validateBasicAuthConfig(runtimeConfig?: ReturnType<typeof useRuntimeConfig>): BasicAuthConfigDiagnostics;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
BASIC_AUTH_PROVIDER_ID,
|
|
4
|
+
DEFAULT_ACCESS_TTL_SECONDS,
|
|
5
|
+
DEFAULT_REFRESH_TTL_SECONDS
|
|
6
|
+
} from "../../lib/constants.js";
|
|
7
|
+
export const BASIC_AUTH_INSECURE_DEV_ESCAPE_HATCH_ENV = "OR3_BASIC_AUTH_ALLOW_INSECURE_DEV";
|
|
8
|
+
function parsePositiveInt(input, fallback) {
|
|
9
|
+
if (!input) return fallback;
|
|
10
|
+
const parsed = Number(input);
|
|
11
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
14
|
+
return Math.floor(parsed);
|
|
15
|
+
}
|
|
16
|
+
function isStrictMode(runtimeConfig) {
|
|
17
|
+
if (process.env.OR3_STRICT_CONFIG === "true") return true;
|
|
18
|
+
if (process.env.NODE_ENV === "production") return true;
|
|
19
|
+
return runtimeConfig.auth?.strict === true;
|
|
20
|
+
}
|
|
21
|
+
export function isBasicAuthInsecureDevEscapeHatchEnabled() {
|
|
22
|
+
return process.env.NODE_ENV !== "production" && process.env[BASIC_AUTH_INSECURE_DEV_ESCAPE_HATCH_ENV] === "true";
|
|
23
|
+
}
|
|
24
|
+
export function getBasicAuthConfig(runtimeConfig) {
|
|
25
|
+
const config = runtimeConfig ?? useRuntimeConfig();
|
|
26
|
+
const enabled = config.auth?.enabled === true;
|
|
27
|
+
const providerId = config.auth?.provider || BASIC_AUTH_PROVIDER_ID;
|
|
28
|
+
const cwd = process.cwd();
|
|
29
|
+
const configuredDbPath = process.env.OR3_BASIC_AUTH_DB_PATH;
|
|
30
|
+
const dbPath = configuredDbPath ? configuredDbPath === ":memory:" ? ":memory:" : resolve(cwd, configuredDbPath) : resolve(cwd, ".data/or3-basic-auth.sqlite");
|
|
31
|
+
const jwtSecret = process.env.OR3_BASIC_AUTH_JWT_SECRET ?? "";
|
|
32
|
+
const refreshSecret = process.env.OR3_BASIC_AUTH_REFRESH_SECRET ?? "";
|
|
33
|
+
return {
|
|
34
|
+
providerId,
|
|
35
|
+
enabled,
|
|
36
|
+
strict: isStrictMode(config),
|
|
37
|
+
jwtSecret,
|
|
38
|
+
refreshSecret,
|
|
39
|
+
accessTtlSeconds: parsePositiveInt(
|
|
40
|
+
process.env.OR3_BASIC_AUTH_ACCESS_TTL_SECONDS,
|
|
41
|
+
DEFAULT_ACCESS_TTL_SECONDS
|
|
42
|
+
),
|
|
43
|
+
refreshTtlSeconds: parsePositiveInt(
|
|
44
|
+
process.env.OR3_BASIC_AUTH_REFRESH_TTL_SECONDS,
|
|
45
|
+
DEFAULT_REFRESH_TTL_SECONDS
|
|
46
|
+
),
|
|
47
|
+
dbPath,
|
|
48
|
+
bootstrapEmail: process.env.OR3_BASIC_AUTH_BOOTSTRAP_EMAIL,
|
|
49
|
+
bootstrapPassword: process.env.OR3_BASIC_AUTH_BOOTSTRAP_PASSWORD
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function validateBasicAuthConfig(runtimeConfig) {
|
|
53
|
+
const config = getBasicAuthConfig(runtimeConfig);
|
|
54
|
+
const errors = [];
|
|
55
|
+
const warnings = [];
|
|
56
|
+
if (!config.enabled) {
|
|
57
|
+
warnings.push("auth.enabled=false; basic-auth provider registration skipped.");
|
|
58
|
+
}
|
|
59
|
+
if (config.providerId !== BASIC_AUTH_PROVIDER_ID) {
|
|
60
|
+
warnings.push(`auth.provider=${config.providerId}; basic-auth provider remains idle.`);
|
|
61
|
+
}
|
|
62
|
+
if (!config.jwtSecret) {
|
|
63
|
+
errors.push("Missing OR3_BASIC_AUTH_JWT_SECRET.");
|
|
64
|
+
}
|
|
65
|
+
if (!config.refreshSecret) {
|
|
66
|
+
errors.push("Missing OR3_BASIC_AUTH_REFRESH_SECRET.");
|
|
67
|
+
}
|
|
68
|
+
if (config.accessTtlSeconds > DEFAULT_ACCESS_TTL_SECONDS) {
|
|
69
|
+
warnings.push(
|
|
70
|
+
`OR3_BASIC_AUTH_ACCESS_TTL_SECONDS=${config.accessTtlSeconds} is above recommended ${DEFAULT_ACCESS_TTL_SECONDS}s.`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (config.refreshTtlSeconds <= config.accessTtlSeconds) {
|
|
74
|
+
warnings.push(
|
|
75
|
+
"Refresh TTL should be greater than access TTL for practical session refresh behavior."
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (config.bootstrapEmail && !config.bootstrapPassword || !config.bootstrapEmail && config.bootstrapPassword) {
|
|
79
|
+
warnings.push(
|
|
80
|
+
"Bootstrap account is partially configured. Set both OR3_BASIC_AUTH_BOOTSTRAP_EMAIL and OR3_BASIC_AUTH_BOOTSTRAP_PASSWORD."
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if ((process.env.OR3_BASIC_AUTH_RATE_LIMIT_BACKEND ?? "").trim().toLowerCase() === "memory") {
|
|
84
|
+
warnings.push(
|
|
85
|
+
"OR3_BASIC_AUTH_RATE_LIMIT_BACKEND=memory uses per-process rate limiting and is unsafe for clustered deployments."
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
isValid: errors.length === 0,
|
|
90
|
+
errors,
|
|
91
|
+
warnings,
|
|
92
|
+
config
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type H3Event } from 'h3';
|
|
2
|
+
export declare function setAccessCookie(event: H3Event, token: string, maxAgeSeconds: number): void;
|
|
3
|
+
export declare function setRefreshCookie(event: H3Event, token: string, maxAgeSeconds: number): void;
|
|
4
|
+
export declare function clearAuthCookies(event: H3Event): void;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { setCookie, deleteCookie } from "h3";
|
|
2
|
+
import {
|
|
3
|
+
ACCESS_COOKIE_NAME,
|
|
4
|
+
ACCESS_COOKIE_PATH,
|
|
5
|
+
REFRESH_COOKIE_NAME,
|
|
6
|
+
REFRESH_COOKIE_PATH
|
|
7
|
+
} from "../../lib/constants.js";
|
|
8
|
+
function isProduction() {
|
|
9
|
+
return process.env.NODE_ENV === "production";
|
|
10
|
+
}
|
|
11
|
+
export function setAccessCookie(event, token, maxAgeSeconds) {
|
|
12
|
+
setCookie(event, ACCESS_COOKIE_NAME, token, {
|
|
13
|
+
httpOnly: true,
|
|
14
|
+
sameSite: "lax",
|
|
15
|
+
secure: isProduction(),
|
|
16
|
+
path: ACCESS_COOKIE_PATH,
|
|
17
|
+
maxAge: maxAgeSeconds
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function setRefreshCookie(event, token, maxAgeSeconds) {
|
|
21
|
+
setCookie(event, REFRESH_COOKIE_NAME, token, {
|
|
22
|
+
httpOnly: true,
|
|
23
|
+
sameSite: "lax",
|
|
24
|
+
secure: isProduction(),
|
|
25
|
+
path: REFRESH_COOKIE_PATH,
|
|
26
|
+
maxAge: maxAgeSeconds
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function clearAuthCookies(event) {
|
|
30
|
+
deleteCookie(event, ACCESS_COOKIE_NAME, {
|
|
31
|
+
httpOnly: true,
|
|
32
|
+
sameSite: "lax",
|
|
33
|
+
secure: isProduction(),
|
|
34
|
+
path: ACCESS_COOKIE_PATH
|
|
35
|
+
});
|
|
36
|
+
deleteCookie(event, REFRESH_COOKIE_NAME, {
|
|
37
|
+
httpOnly: true,
|
|
38
|
+
sameSite: "lax",
|
|
39
|
+
secure: isProduction(),
|
|
40
|
+
path: REFRESH_COOKIE_PATH
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function createInvalidCredentialsError(): import("h3").H3Error<unknown>;
|
|
2
|
+
export declare function createSessionExpiredError(): import("h3").H3Error<unknown>;
|
|
3
|
+
export declare function createInvalidRequestError(): import("h3").H3Error<unknown>;
|
|
4
|
+
export declare function createAuthNotConfiguredError(): import("h3").H3Error<unknown>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createError } from "h3";
|
|
2
|
+
export function createInvalidCredentialsError() {
|
|
3
|
+
return createError({
|
|
4
|
+
statusCode: 401,
|
|
5
|
+
statusMessage: "Invalid credentials"
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
export function createSessionExpiredError() {
|
|
9
|
+
return createError({
|
|
10
|
+
statusCode: 401,
|
|
11
|
+
statusMessage: "Session expired"
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export function createInvalidRequestError() {
|
|
15
|
+
return createError({
|
|
16
|
+
statusCode: 400,
|
|
17
|
+
statusMessage: "Invalid request"
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function createAuthNotConfiguredError() {
|
|
21
|
+
return createError({
|
|
22
|
+
statusCode: 503,
|
|
23
|
+
statusMessage: "Authentication provider is not configured"
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { getCookie } from 'h3';
|
|
2
|
+
import { type JwtPayload } from 'jsonwebtoken';
|
|
3
|
+
interface AccessClaimsInput {
|
|
4
|
+
sub: string;
|
|
5
|
+
sid: string;
|
|
6
|
+
ver: number;
|
|
7
|
+
email?: string;
|
|
8
|
+
display_name?: string | null;
|
|
9
|
+
}
|
|
10
|
+
export interface BasicAccessTokenClaims extends JwtPayload {
|
|
11
|
+
sub: string;
|
|
12
|
+
sid: string;
|
|
13
|
+
ver: number;
|
|
14
|
+
typ: 'access';
|
|
15
|
+
email?: string;
|
|
16
|
+
display_name?: string | null;
|
|
17
|
+
}
|
|
18
|
+
interface RefreshClaimsInput {
|
|
19
|
+
sub: string;
|
|
20
|
+
sid: string;
|
|
21
|
+
ver: number;
|
|
22
|
+
}
|
|
23
|
+
export interface BasicRefreshTokenClaims extends JwtPayload {
|
|
24
|
+
sub: string;
|
|
25
|
+
sid: string;
|
|
26
|
+
ver: number;
|
|
27
|
+
jti: string;
|
|
28
|
+
typ: 'refresh';
|
|
29
|
+
}
|
|
30
|
+
export declare function signAccessToken(input: AccessClaimsInput): Promise<string>;
|
|
31
|
+
export declare function signRefreshToken(input: RefreshClaimsInput): Promise<string>;
|
|
32
|
+
export declare function verifyAccessToken(token: string): Promise<BasicAccessTokenClaims | null>;
|
|
33
|
+
export declare function verifyRefreshToken(token: string): Promise<BasicRefreshTokenClaims | null>;
|
|
34
|
+
export declare function getAccessTokenFromEvent(event: Parameters<typeof getCookie>[0]): string | null;
|
|
35
|
+
export declare function getRefreshTokenFromEvent(event: Parameters<typeof getCookie>[0]): string | null;
|
|
36
|
+
export declare function hashRefreshToken(token: string): string;
|
|
37
|
+
export {};
|