parent
413353e034
commit
2e1923fe6a
@ -0,0 +1,131 @@
|
||||
import {
|
||||
AUTH_CONTENT_TYPES,
|
||||
AUTH_COOKIE_NAMES,
|
||||
AUTH_ERROR_CODES,
|
||||
AUTH_HEADER_NAMES,
|
||||
AUTH_HTTP_STATUS,
|
||||
AUTH_SAFE_MESSAGES
|
||||
} from '$libs/auth/consts';
|
||||
import { parseAuthCookieHeader, serializeAuthSetCookie } from '$libs/auth/cookies';
|
||||
import { toAuthSafeError } from '$libs/auth/errors';
|
||||
import type {
|
||||
AuthCsrfIssueInput,
|
||||
AuthCurrentInput,
|
||||
AuthEmailVerificationCompleteInput,
|
||||
AuthEmailVerificationRequestInput,
|
||||
AuthPasswordResetCompleteInput,
|
||||
AuthPasswordResetRequestInput,
|
||||
AuthServerRequestLike,
|
||||
AuthSetCookie,
|
||||
AuthSignInPasswordInput,
|
||||
AuthSignUpPasswordInput,
|
||||
AuthTenantId
|
||||
} from '$libs/auth/types';
|
||||
import type { AuthErrorCode } from '$libs/auth/types';
|
||||
|
||||
export interface AuthRouteRequest {
|
||||
readonly request: Request;
|
||||
readonly tenantId: AuthTenantId;
|
||||
}
|
||||
|
||||
export interface AuthHandlerEngine {
|
||||
current(input: AuthCurrentInput): Promise<unknown>;
|
||||
issueCsrf(input: AuthCsrfIssueInput): Promise<{
|
||||
readonly token: string;
|
||||
readonly cookie: AuthSetCookie;
|
||||
readonly expiresAt: number;
|
||||
}>;
|
||||
verifyCsrf(input: {
|
||||
readonly tenantId: AuthTenantId;
|
||||
readonly token?: string | null;
|
||||
readonly cookie?: string | null;
|
||||
}): Promise<unknown>;
|
||||
signUpPassword(input: AuthSignUpPasswordInput): Promise<unknown>;
|
||||
signInPassword(input: AuthSignInPasswordInput): Promise<unknown>;
|
||||
signOut(input: {
|
||||
readonly tenantId: AuthTenantId;
|
||||
readonly request: AuthServerRequestLike;
|
||||
}): Promise<unknown>;
|
||||
signOutGlobal(input: {
|
||||
readonly tenantId: AuthTenantId;
|
||||
readonly request: AuthServerRequestLike;
|
||||
}): Promise<unknown>;
|
||||
requestEmailVerification(input: AuthEmailVerificationRequestInput): Promise<unknown>;
|
||||
completeEmailVerification(input: AuthEmailVerificationCompleteInput): Promise<unknown>;
|
||||
requestPasswordReset(input: AuthPasswordResetRequestInput): Promise<unknown>;
|
||||
completePasswordReset(input: AuthPasswordResetCompleteInput): Promise<unknown>;
|
||||
}
|
||||
|
||||
export async function verifyAuthRequestCsrf(
|
||||
engine: AuthHandlerEngine,
|
||||
input: AuthRouteRequest
|
||||
): Promise<void> {
|
||||
const cookieHeader = input.request.headers.get(AUTH_HEADER_NAMES.COOKIE);
|
||||
const cookies = parseAuthCookieHeader(cookieHeader);
|
||||
await engine.verifyCsrf({
|
||||
tenantId: input.tenantId,
|
||||
token: input.request.headers.get(AUTH_HEADER_NAMES.CSRF),
|
||||
cookie: cookies[AUTH_COOKIE_NAMES.CSRF]
|
||||
});
|
||||
}
|
||||
|
||||
export async function protectAuthHandler(run: () => Promise<Response>): Promise<Response> {
|
||||
try {
|
||||
return await run();
|
||||
} catch (error) {
|
||||
const safe = toAuthSafeError(error);
|
||||
return authJson({ error: safe }, { status: statusForAuthError(safe.code) });
|
||||
}
|
||||
}
|
||||
|
||||
export function authRouteNotFound(): Response {
|
||||
return authJson(
|
||||
{
|
||||
error: {
|
||||
code: AUTH_ERROR_CODES.ROUTE_NOT_FOUND,
|
||||
messageKey: AUTH_SAFE_MESSAGES.ROUTE_NOT_FOUND
|
||||
}
|
||||
},
|
||||
{ status: AUTH_HTTP_STATUS.NOT_FOUND }
|
||||
);
|
||||
}
|
||||
|
||||
export function toAuthRequestLike(request: Request): AuthServerRequestLike {
|
||||
return {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
headers: request.headers
|
||||
};
|
||||
}
|
||||
|
||||
export function authJson(
|
||||
body: unknown,
|
||||
options: { readonly status?: number; readonly cookies?: readonly AuthSetCookie[] } = {}
|
||||
): Response {
|
||||
const headers = new Headers({ [AUTH_HEADER_NAMES.CONTENT_TYPE]: AUTH_CONTENT_TYPES.JSON });
|
||||
for (const cookie of options.cookies ?? [])
|
||||
headers.append(AUTH_HEADER_NAMES.SET_COOKIE, serializeAuthSetCookie(cookie));
|
||||
return new Response(JSON.stringify(body), {
|
||||
status: options.status ?? AUTH_HTTP_STATUS.OK,
|
||||
headers
|
||||
});
|
||||
}
|
||||
|
||||
function statusForAuthError(code: AuthErrorCode): number {
|
||||
switch (code) {
|
||||
case AUTH_ERROR_CODES.CSRF_INVALID:
|
||||
case AUTH_ERROR_CODES.ASSURANCE_REQUIRED:
|
||||
case AUTH_ERROR_CODES.SESSION_REVOKED:
|
||||
case AUTH_ERROR_CODES.TOKEN_REUSE_DETECTED:
|
||||
case AUTH_ERROR_CODES.TENANT_BOUNDARY:
|
||||
return AUTH_HTTP_STATUS.FORBIDDEN;
|
||||
case AUTH_ERROR_CODES.SESSION_REQUIRED:
|
||||
return AUTH_HTTP_STATUS.UNAUTHORIZED;
|
||||
case AUTH_ERROR_CODES.RATE_LIMITED:
|
||||
return AUTH_HTTP_STATUS.TOO_MANY_REQUESTS;
|
||||
case AUTH_ERROR_CODES.ROUTE_NOT_FOUND:
|
||||
return AUTH_HTTP_STATUS.NOT_FOUND;
|
||||
default:
|
||||
return AUTH_HTTP_STATUS.BAD_REQUEST;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue