You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

331 lines
9.0 KiB

import {
AUTH_CACHE_TAGS,
AUTH_COOKIE_NAMES,
AUTH_DEFAULTS,
AUTH_EVENT_NAMES,
AUTH_ROUTE_PATHS,
AUTH_TEST_ACTIONS,
AUTH_TEST_COOKIE_NAMES,
AUTH_TEST_FORM_FIELDS,
AUTH_TEST_IDS,
AUTH_TEST_TENANT_ID,
issueAuthCsrf,
verifyAuthCsrf,
type AuthSessionId
} from '$libs/auth';
import {
createDeterministicAuthCrypto,
createEngineAuth,
createMemoryAuthActors,
createMemoryAuthAdapter,
createMemoryAuthCache,
createMemoryAuthClock,
createMemoryAuthLogr,
createMemoryAuthMailer,
createMemoryAuthSessPort,
createTestPasswordHasher
} from '$svrs/auth';
export const prerender = false;
const AUTH_TEST_PASSWORD = 'correct horse battery staple';
const AUTH_TEST_CSRF_KEY = 'test-page-csrf-key';
const AUTH_TEST_SESSION_COOKIE_MAX_AGE = 60 * 60;
let harness = createHarness();
export async function load({ cookies }: { cookies: { get(name: string): string | undefined } }) {
const sessionId = cookies.get(AUTH_TEST_COOKIE_NAMES.SESSION) as AuthSessionId | undefined;
return {
auth: {
routes: AUTH_ROUTE_PATHS,
events: AUTH_EVENT_NAMES,
cacheTags: AUTH_CACHE_TAGS,
testIds: AUTH_TEST_IDS,
actions: AUTH_TEST_ACTIONS,
fields: AUTH_TEST_FORM_FIELDS,
defaults: {
identifier: 'ada@example.com',
password: AUTH_TEST_PASSWORD,
displayName: 'Ada Lovelace'
}
},
state: await snapshot(sessionId)
};
}
export const actions = {
async signUp({ request, cookies }: TestActionEvent) {
return runAuthAction(cookies, AUTH_TEST_ACTIONS.SIGN_UP, async () => {
const input = await readPasswordForm(request);
const result = await harness.engine.signUpPassword({
tenantId: AUTH_TEST_TENANT_ID,
identifier: input.identifier,
password: input.password,
profile: { displayName: input.displayName },
device: {
displayName: 'Test browser',
userAgentDisplay: 'SvelteKit action'
}
});
writeSessionCookie(cookies, result.session.sessionId);
return { current: result.current };
});
},
async signIn({ request, cookies }: TestActionEvent) {
return runAuthAction(cookies, AUTH_TEST_ACTIONS.SIGN_IN, async () => {
const input = await readPasswordForm(request);
const result = await harness.engine.signInPassword({
tenantId: AUTH_TEST_TENANT_ID,
identifier: input.identifier,
password: input.password,
device: {
displayName: 'Test browser',
userAgentDisplay: 'SvelteKit action'
}
});
writeSessionCookie(cookies, result.session.sessionId);
return { current: result.current };
});
},
async signOut({ cookies }: TestActionEvent) {
return runAuthAction(cookies, AUTH_TEST_ACTIONS.SIGN_OUT, async () => {
const sessionId = readSessionCookie(cookies);
const result = await harness.engine.signOut({
tenantId: AUTH_TEST_TENANT_ID,
sessionId
});
clearSessionCookie(cookies);
return { current: result.current, endedSessionIds: result.endedSessionIds };
});
},
async signOutGlobal({ cookies }: TestActionEvent) {
return runAuthAction(cookies, AUTH_TEST_ACTIONS.SIGN_OUT_GLOBAL, async () => {
const sessionId = readSessionCookie(cookies);
const result = await harness.engine.signOutGlobal({
tenantId: AUTH_TEST_TENANT_ID,
sessionId
});
clearSessionCookie(cookies);
return { current: result.current, endedSessionIds: result.endedSessionIds };
});
},
async csrfRoundtrip({ cookies }: TestActionEvent) {
return runAuthAction(cookies, AUTH_TEST_ACTIONS.CSRF_ROUNDTRIP, async () => {
const result = await harness.engine.issueCsrf({ tenantId: AUTH_TEST_TENANT_ID });
await harness.engine.verifyCsrf({
tenantId: AUTH_TEST_TENANT_ID,
token: result.token,
cookie: result.cookie.value
});
return {
csrf: {
ok: true,
expiresAt: result.expiresAt,
cookieName: AUTH_COOKIE_NAMES.CSRF
}
};
});
},
async csrfExpired({ cookies }: TestActionEvent) {
return runAuthAction(cookies, AUTH_TEST_ACTIONS.CSRF_EXPIRED, async () => {
const crypto = createDeterministicAuthCrypto('auth-test-page-expired-csrf');
const clock = createMemoryAuthClock(10_000);
const result = await issueAuthCsrf({
crypto,
clock,
tenantId: AUTH_TEST_TENANT_ID,
config: { signingKey: AUTH_TEST_CSRF_KEY }
});
clock.advance(AUTH_DEFAULTS.CSRF_TTL_MS + 1);
try {
await verifyAuthCsrf({
crypto,
clock,
tenantId: AUTH_TEST_TENANT_ID,
token: result.token,
cookie: result.cookie.value,
config: { signingKey: AUTH_TEST_CSRF_KEY }
});
return { csrf: { ok: false, reason: 'unexpected-valid-token' } };
} catch (error) {
return { csrf: { ok: true, rejected: errorName(error) } };
}
});
},
async reset({ cookies }: TestActionEvent) {
harness = createHarness();
clearSessionCookie(cookies);
return {
ok: true,
action: AUTH_TEST_ACTIONS.RESET,
message: 'Auth test harness reset.',
state: await snapshot()
};
}
};
interface TestCookies {
get(name: string): string | undefined;
set(name: string, value: string, options: TestCookieOptions): void;
delete(name: string, options: Pick<TestCookieOptions, 'path'>): void;
}
interface TestCookieOptions {
readonly path: string;
readonly httpOnly?: boolean;
readonly sameSite?: 'strict' | 'lax' | 'none';
readonly secure?: boolean;
readonly maxAge?: number;
}
interface TestActionEvent {
readonly request: Request;
readonly cookies: TestCookies;
}
interface PasswordFormInput {
readonly identifier: string;
readonly password: string;
readonly displayName: string;
}
function createHarness() {
const crypto = createDeterministicAuthCrypto('auth-test-page');
const clock = createMemoryAuthClock(Date.now());
const store = createMemoryAuthAdapter({ suppressProductionWarning: true });
const actors = createMemoryAuthActors(crypto);
const sess = createMemoryAuthSessPort({ crypto, clock });
const logger = createMemoryAuthLogr();
const cache = createMemoryAuthCache();
const mailer = createMemoryAuthMailer();
const engine = createEngineAuth({
security: { csrf: { signingKey: AUTH_TEST_CSRF_KEY } },
ports: {
store,
actors,
sess,
logger,
timer: clock,
crypto,
cache,
mailer,
passwordHasher: createTestPasswordHasher()
}
});
return { engine, store, actors, sess, logger, cache, mailer, clock };
}
async function snapshot(sessionId?: AuthSessionId) {
const current = await harness.engine.current({
tenantId: AUTH_TEST_TENANT_ID,
sessionId
});
const store = harness.store.snapshot();
return {
current,
counts: {
actors: harness.actors.snapshot().length,
credentials: store.credentials.length,
sessions: store.sessionBindings.length,
devices: store.devices.length,
flows: store.flows.length,
events: harness.logger.entries.length,
cacheInvalidations: harness.cache.invalidations.length,
mails: harness.mailer.messages.length
},
events: harness.logger.entries.slice(-10).map((entry) => ({
level: entry.level,
category: entry.category,
message: entry.message,
eventName: entry.eventName,
code: entry.code,
actorId: entry.actorRef?.actorId,
sessionId: entry.sessionId
})),
cacheInvalidations: harness.cache.invalidations.slice(-10),
devices: store.devices.slice(-10),
credentials: store.credentials.map((credential) => ({
id: credential.id,
kind: credential.kind,
identifierDisplay: credential.identifierDisplay,
actorId: credential.actorRef.actorId,
verifiedAt: credential.verifiedAt
}))
};
}
async function runAuthAction(
cookies: TestCookies,
action: string,
run: () => Promise<Record<string, unknown>>
) {
try {
const result = await run();
return {
ok: true,
action,
...result,
state: await snapshot(readSessionCookie(cookies))
};
} catch (error) {
return {
ok: false,
action,
error: errorToPayload(error),
state: await snapshot(readSessionCookie(cookies))
};
}
}
async function readPasswordForm(request: Request): Promise<PasswordFormInput> {
const form = await request.formData();
return {
identifier: String(form.get(AUTH_TEST_FORM_FIELDS.IDENTIFIER) ?? ''),
password: String(form.get(AUTH_TEST_FORM_FIELDS.PASSWORD) ?? ''),
displayName: String(form.get(AUTH_TEST_FORM_FIELDS.DISPLAY_NAME) ?? '')
};
}
function readSessionCookie(cookies: TestCookies): AuthSessionId | undefined {
return cookies.get(AUTH_TEST_COOKIE_NAMES.SESSION) as AuthSessionId | undefined;
}
function writeSessionCookie(cookies: TestCookies, sessionId: AuthSessionId | undefined): void {
if (!sessionId) return;
cookies.set(AUTH_TEST_COOKIE_NAMES.SESSION, sessionId, {
path: '/test/auth',
httpOnly: true,
sameSite: 'lax',
secure: false,
maxAge: AUTH_TEST_SESSION_COOKIE_MAX_AGE
});
}
function clearSessionCookie(cookies: TestCookies): void {
cookies.delete(AUTH_TEST_COOKIE_NAMES.SESSION, { path: '/test/auth' });
}
function errorToPayload(error: unknown) {
return {
name: errorName(error),
message:
error instanceof Error
? error.message
: typeof error === 'string'
? error
: 'Unknown auth error',
code: typeof error === 'object' && error && 'code' in error ? String(error.code) : undefined
};
}
function errorName(error: unknown): string {
return error instanceof Error ? error.name : typeof error;
}

Powered by TurnKey Linux.