import { AuthAccessTokenType, AuthAccessWriteScope, AuthAdministrativeScopes, AuthStandardClientScopes, type AuthAccessTokenResult, type AuthBrowserSessionResult, type AuthClientMetadata, type AuthClientSession, type AuthCreatePairingCredentialInput, type AuthEnvironmentScope, type AuthPairingLink, type AuthPairingCredentialResult, type AuthSessionId, type AuthSessionState, type ServerAuthDescriptor, type ServerAuthSessionMethod, type AuthWebSocketTicketResult,} from "@t3tools/contracts";import { encodeOAuthScope } from "@t3tools/shared/oauthScope";import * as Context from "effect/Context";import * as Crypto from "effect/Crypto";import * as Data from "effect/Data";import * as DateTime from "effect/DateTime";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest"; import * as EnvironmentAuthPolicy from "./EnvironmentAuthPolicy.ts";import * as PairingGrantStore from "./PairingGrantStore.ts";import * as ServerSecretStore from "./ServerSecretStore.ts";import * as SessionStore from "./SessionStore.ts";import { verifyRequestDpopProof } from "./dpop.ts";import { layerConfig as SqlitePersistenceLayer } from "../persistence/Layers/Sqlite.ts"; export const DEFAULT_SESSION_SUBJECT = "cli-issued-session";export const INTERNAL_ADMINISTRATIVE_BOOTSTRAP_SUBJECT = "administrative-bootstrap"; export interface IssuedPairingLink { readonly id: string; readonly credential: string; readonly scopes: ReadonlyArray<AuthEnvironmentScope>; readonly subject: string; readonly label?: string; readonly createdAt: DateTime.Utc; readonly expiresAt: DateTime.Utc;} export interface IssuedBearerSession { readonly sessionId: AuthSessionId; readonly token: string; readonly method: "bearer-access-token"; readonly scopes: ReadonlyArray<AuthEnvironmentScope>; readonly subject: string; readonly client: AuthClientMetadata; readonly expiresAt: DateTime.Utc;} export interface AuthenticatedSession { readonly sessionId: AuthSessionId; readonly subject: string; readonly method: ServerAuthSessionMethod; readonly scopes: ReadonlyArray<AuthEnvironmentScope>; readonly proofKeyThumbprint?: string; readonly expiresAt?: DateTime.DateTime;} export class ServerAuthInternalError extends Data.TaggedError("ServerAuthInternalError")<{ readonly message: string; readonly cause?: unknown;}> {} export class ServerAuthInvalidCredentialError extends Data.TaggedError( "ServerAuthInvalidCredentialError",)<{ readonly reason: "missing_credential" | "invalid_credential"; readonly cause?: unknown;}> {} export class ServerAuthInvalidRequestError extends Data.TaggedError( "ServerAuthInvalidRequestError",)<{ readonly reason: "invalid_scope" | "scope_not_granted";}> {} export class ServerAuthForbiddenOperationError extends Data.TaggedError( "ServerAuthForbiddenOperationError",)<{ readonly reason: "current_session_revoke_not_allowed";}> {} export interface EnvironmentAuthShape { readonly getDescriptor: () => Effect.Effect<ServerAuthDescriptor>; readonly getSessionState: ( request: HttpServerRequest.HttpServerRequest, ) => Effect.Effect<AuthSessionState, ServerAuthInternalError>; readonly createBrowserSession: ( credential: string, requestMetadata: AuthClientMetadata, ) => Effect.Effect< { readonly response: AuthBrowserSessionResult; readonly sessionToken: string; }, ServerAuthInvalidCredentialError | ServerAuthInternalError >; readonly exchangeBootstrapCredentialForAccessToken: ( credential: string, requestedScopes: ReadonlyArray<AuthEnvironmentScope> | undefined, requestMetadata: AuthClientMetadata, input?: { readonly proofKeyThumbprint?: string; }, ) => Effect.Effect< AuthAccessTokenResult, ServerAuthInvalidCredentialError | ServerAuthInvalidRequestError | ServerAuthInternalError >; readonly createPairingLink: (input?: { readonly ttl?: Duration.Duration; readonly label?: string; readonly scopes?: ReadonlyArray<AuthEnvironmentScope>; readonly subject?: string; readonly proofKeyThumbprint?: string; }) => Effect.Effect<IssuedPairingLink, ServerAuthInternalError>; readonly issuePairingCredential: ( input?: AuthCreatePairingCredentialInput, ) => Effect.Effect<AuthPairingCredentialResult, ServerAuthInternalError>; readonly issueStartupPairingCredential: () => Effect.Effect< AuthPairingCredentialResult, ServerAuthInternalError >; readonly listPairingLinks: (input?: { readonly excludeSubjects?: ReadonlyArray<string>; }) => Effect.Effect<ReadonlyArray<AuthPairingLink>, ServerAuthInternalError>; readonly revokePairingLink: (id: string) => Effect.Effect<boolean, ServerAuthInternalError>; readonly issueSession: (input?: { readonly ttl?: Duration.Duration; readonly subject?: string; readonly scopes?: ReadonlyArray<AuthEnvironmentScope>; readonly label?: string; }) => Effect.Effect<IssuedBearerSession, ServerAuthInternalError>; readonly listSessions: () => Effect.Effect< ReadonlyArray<AuthClientSession>, ServerAuthInternalError >; readonly revokeSession: ( sessionId: AuthSessionId, ) => Effect.Effect<boolean, ServerAuthInternalError>; readonly revokeOtherSessionsExcept: ( sessionId: AuthSessionId, ) => Effect.Effect<number, ServerAuthInternalError>; readonly listClientSessions: ( currentSessionId: AuthSessionId, ) => Effect.Effect<ReadonlyArray<AuthClientSession>, ServerAuthInternalError>; readonly revokeClientSession: ( currentSessionId: AuthSessionId, targetSessionId: AuthSessionId, ) => Effect.Effect<boolean, ServerAuthForbiddenOperationError | ServerAuthInternalError>; readonly revokeOtherClientSessions: ( currentSessionId: AuthSessionId, ) => Effect.Effect<number, ServerAuthInternalError>; readonly authenticateHttpRequest: ( request: HttpServerRequest.HttpServerRequest, ) => Effect.Effect< AuthenticatedSession, ServerAuthInvalidCredentialError | ServerAuthInternalError >; readonly authenticateWebSocketUpgrade: ( request: HttpServerRequest.HttpServerRequest, ) => Effect.Effect< AuthenticatedSession, ServerAuthInvalidCredentialError | ServerAuthInternalError >; readonly issueWebSocketTicket: ( session: Pick<AuthenticatedSession, "sessionId">, ) => Effect.Effect<AuthWebSocketTicketResult, ServerAuthInternalError>; readonly issueStartupPairingUrl: ( baseUrl: string, ) => Effect.Effect<string, ServerAuthInternalError>;} export class EnvironmentAuth extends Context.Service<EnvironmentAuth, EnvironmentAuthShape>()( "t3/auth/EnvironmentAuth",) {} type BootstrapExchangeResult = { readonly response: AuthBrowserSessionResult; readonly sessionToken: string;}; const AUTHORIZATION_PREFIX = "Bearer ";const DPOP_AUTHORIZATION_PREFIX = "DPoP ";const WEBSOCKET_TICKET_QUERY_PARAM = "wsTicket"; const bySessionPriority = (left: AuthClientSession, right: AuthClientSession) => { const leftCanManage = left.scopes.includes(AuthAccessWriteScope); const rightCanManage = right.scopes.includes(AuthAccessWriteScope); if (leftCanManage !== rightCanManage) { return leftCanManage ? -1 : 1; } if (left.connected !== right.connected) { return left.connected ? -1 : 1; } return right.issuedAt.epochMilliseconds - left.issuedAt.epochMilliseconds;}; const toInternalError = (message: string) => (cause: unknown): ServerAuthInternalError => new ServerAuthInternalError({ message, cause }); export function toBootstrapExchangeError( cause: PairingGrantStore.BootstrapCredentialError,): ServerAuthInvalidCredentialError | ServerAuthInternalError { if (cause._tag === "BootstrapCredentialInternalError") { return new ServerAuthInternalError({ message: "Failed to validate bootstrap credential.", cause, }); } return new ServerAuthInvalidCredentialError({ reason: "invalid_credential", cause, });} const mapSessionVerificationErrors = <A, R>( effect: Effect.Effect<A, SessionStore.SessionCredentialError, R>,): Effect.Effect<A, ServerAuthInvalidCredentialError | ServerAuthInternalError, R> => effect.pipe( Effect.catchTags({ SessionCredentialInvalidError: (cause) => Effect.fail(new ServerAuthInvalidCredentialError({ reason: "invalid_credential", cause })), SessionCredentialInternalError: (cause) => Effect.fail( new ServerAuthInternalError({ message: "Failed to validate session credential.", cause, }), ), }), ); function parseBearerToken(request: HttpServerRequest.HttpServerRequest): string | null { const header = request.headers["authorization"]; if (typeof header !== "string" || !header.startsWith(AUTHORIZATION_PREFIX)) { return null; } const token = header.slice(AUTHORIZATION_PREFIX.length).trim(); return token.length > 0 ? token : null;} function parseDpopToken(request: HttpServerRequest.HttpServerRequest): string | null { const header = request.headers["authorization"]; if (typeof header !== "string" || !header.startsWith(DPOP_AUTHORIZATION_PREFIX)) { return null; } const token = header.slice(DPOP_AUTHORIZATION_PREFIX.length).trim(); return token.length > 0 ? token : null;} export const make = Effect.fn("makeEnvironmentAuth")(function* () { const policy = yield* EnvironmentAuthPolicy.EnvironmentAuthPolicy; const bootstrapCredentials = yield* PairingGrantStore.PairingGrantStore; const sessions = yield* SessionStore.SessionStore; const secretStore = yield* ServerSecretStore.ServerSecretStore; const crypto = yield* Crypto.Crypto; const descriptor = yield* policy.getDescriptor(); const authenticateToken = ( token: string, ): Effect.Effect< AuthenticatedSession, ServerAuthInvalidCredentialError | ServerAuthInternalError > => sessions.verify(token).pipe( Effect.tapErrorTag("SessionCredentialInvalidError", (cause) => Effect.logWarning("Rejected authenticated session credential.").pipe( Effect.annotateLogs({ reason: cause.message, }), ), ), Effect.map((session) => ({ sessionId: session.sessionId, subject: session.subject, method: session.method, scopes: session.scopes, ...(session.proofKeyThumbprint ? { proofKeyThumbprint: session.proofKeyThumbprint } : {}), ...(session.expiresAt ? { expiresAt: session.expiresAt } : {}), })), mapSessionVerificationErrors, ); const authenticateRequest = (request: HttpServerRequest.HttpServerRequest) => { const cookieToken = request.cookies[sessions.cookieName]; const bearerToken = parseBearerToken(request); const dpopToken = parseDpopToken(request); const credential = cookieToken ?? bearerToken ?? dpopToken; if (!credential) { return Effect.fail(new ServerAuthInvalidCredentialError({ reason: "missing_credential" })); } return authenticateToken(credential).pipe( Effect.flatMap((session) => { if (session.proofKeyThumbprint) { if (!dpopToken || dpopToken !== credential) { return Effect.fail( new ServerAuthInvalidCredentialError({ reason: "invalid_credential", cause: "DPoP-bound access token requires DPoP authorization.", }), ); } return verifyRequestDpopProof({ request, expectedThumbprint: session.proofKeyThumbprint, expectedAccessToken: dpopToken, }).pipe( Effect.provideService(ServerSecretStore.ServerSecretStore, secretStore), Effect.provideService(Crypto.Crypto, crypto), Effect.as(session), ); } if (dpopToken) { return Effect.fail( new ServerAuthInvalidCredentialError({ reason: "invalid_credential", cause: "DPoP authorization requires a proof-bound access token.", }), ); } return Effect.succeed(session); }), ); }; const getSessionState: EnvironmentAuthShape["getSessionState"] = (request) => authenticateRequest(request).pipe( Effect.map( (session) => ({ authenticated: true, auth: descriptor, scopes: session.scopes, sessionMethod: session.method, ...(session.expiresAt ? { expiresAt: DateTime.toUtc(session.expiresAt) } : {}), }) satisfies AuthSessionState, ), Effect.catchTag("ServerAuthInvalidCredentialError", () => Effect.succeed({ authenticated: false, auth: descriptor, } satisfies AuthSessionState), ), Effect.withSpan("EnvironmentAuth.getSessionState"), ); const createBrowserSession: EnvironmentAuthShape["createBrowserSession"] = ( credential, requestMetadata, ) => bootstrapCredentials.consume(credential).pipe( Effect.mapError(toBootstrapExchangeError), Effect.flatMap((grant) => sessions .issue({ method: "browser-session-cookie", subject: grant.subject, scopes: grant.scopes, client: { ...requestMetadata, ...(grant.label ? { label: grant.label } : {}), }, }) .pipe( Effect.mapError( (cause) => new ServerAuthInternalError({ message: "Failed to issue authenticated session.", cause, }), ), ), ), Effect.map( (session) => ({ response: { authenticated: true, scopes: session.scopes, sessionMethod: session.method, expiresAt: DateTime.toUtc(session.expiresAt), } satisfies AuthBrowserSessionResult, sessionToken: session.token, }) satisfies BootstrapExchangeResult, ), Effect.withSpan("EnvironmentAuth.createBrowserSession"), ); const exchangeBootstrapCredentialForAccessToken: EnvironmentAuthShape["exchangeBootstrapCredentialForAccessToken"] = (credential, requestedScopes, requestMetadata, input) => bootstrapCredentials.consume(credential, input).pipe( Effect.mapError(toBootstrapExchangeError), Effect.flatMap((grant) => Effect.gen(function* () { const grantedScopes = requestedScopes ?? grant.scopes; if (!grantedScopes.every((scope) => grant.scopes.includes(scope))) { return yield* new ServerAuthInvalidRequestError({ reason: "scope_not_granted", }); } return yield* sessions .issue({ method: input?.proofKeyThumbprint ? "dpop-access-token" : "bearer-access-token", subject: grant.subject, scopes: grantedScopes, ...(input?.proofKeyThumbprint ? { proofKeyThumbprint: input.proofKeyThumbprint, ttl: Duration.hours(1), } : {}), client: { ...requestMetadata, ...(grant.label ? { label: grant.label } : {}), }, }) .pipe( Effect.mapError( (cause) => new ServerAuthInternalError({ message: "Failed to issue authenticated access token.", cause, }), ), ); }), ), Effect.flatMap((session) => DateTime.now.pipe( Effect.map( (now) => ({ access_token: session.token, issued_token_type: AuthAccessTokenType, token_type: input?.proofKeyThumbprint ? "DPoP" : "Bearer", expires_in: Math.max( 0, Math.floor( (session.expiresAt.epochMilliseconds - now.epochMilliseconds) / 1000, ), ), scope: encodeOAuthScope(session.scopes), }) satisfies AuthAccessTokenResult, ), ), ), Effect.withSpan("EnvironmentAuth.exchangeBootstrapCredentialForAccessToken"), ); const issuePairingCredentialForSubject = (input: { readonly scopes: ReadonlyArray<AuthEnvironmentScope>; readonly subject: string; readonly label?: string; }) => createPairingLink({ scopes: input.scopes, subject: input.subject, ...(input.label ? { label: input.label } : {}), }).pipe( Effect.map( (issued) => ({ id: issued.id, credential: issued.credential, ...(issued.label ? { label: issued.label } : {}), expiresAt: issued.expiresAt, }) satisfies AuthPairingCredentialResult, ), ); const createPairingLink: EnvironmentAuthShape["createPairingLink"] = Effect.fn( "EnvironmentAuth.createPairingLink", )( function* (input) { const createdAt = yield* DateTime.now; const issued = yield* bootstrapCredentials.issueOneTimeToken({ scopes: input?.scopes ?? AuthStandardClientScopes, subject: input?.subject ?? "one-time-token", ...(input?.ttl ? { ttl: input.ttl } : {}), ...(input?.label ? { label: input.label } : {}), ...(input?.proofKeyThumbprint ? { proofKeyThumbprint: input.proofKeyThumbprint } : {}), }); return { id: issued.id, credential: issued.credential, scopes: input?.scopes ?? AuthStandardClientScopes,Lines 1–500 of 721. Load more
import { AuthAccessTokenType, AuthAccessWriteScope, AuthAdministrativeScopes, AuthStandardClientScopes, type AuthAccessTokenResult, type AuthBrowserSessionResult, type AuthClientMetadata, type AuthClientSession, type AuthCreatePairingCredentialInput, type AuthEnvironmentScope, type AuthPairingLink, type AuthPairingCredentialResult, type AuthSessionId, type AuthSessionState, type ServerAuthDescriptor, type ServerAuthSessionMethod, type AuthWebSocketTicketResult, } from "@t3tools/contracts"; import { encodeOAuthScope } from "@t3tools/shared/oauthScope"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; import * as Data from "effect/Data"; import * as DateTime from "effect/DateTime"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest"; import * as EnvironmentAuthPolicy from "./EnvironmentAuthPolicy.ts"; import * as PairingGrantStore from "./PairingGrantStore.ts"; import * as ServerSecretStore from "./ServerSecretStore.ts"; import * as SessionStore from "./SessionStore.ts"; import { verifyRequestDpopProof } from "./dpop.ts"; import { layerConfig as SqlitePersistenceLayer } from "../persistence/Layers/Sqlite.ts"; export const DEFAULT_SESSION_SUBJECT = "cli-issued-session"; export const INTERNAL_ADMINISTRATIVE_BOOTSTRAP_SUBJECT = "administrative-bootstrap"; export interface IssuedPairingLink { readonly id: string; readonly credential: string; readonly scopes: ReadonlyArray<AuthEnvironmentScope>; readonly subject: string; readonly label?: string; readonly createdAt: DateTime.Utc; readonly expiresAt: DateTime.Utc; } export interface IssuedBearerSession { readonly sessionId: AuthSessionId; readonly token: string; readonly method: "bearer-access-token"; readonly scopes: ReadonlyArray<AuthEnvironmentScope>; readonly subject: string; readonly client: AuthClientMetadata; readonly expiresAt: DateTime.Utc; } export interface AuthenticatedSession { readonly sessionId: AuthSessionId; readonly subject: string; readonly method: ServerAuthSessionMethod; readonly scopes: ReadonlyArray<AuthEnvironmentScope>; readonly proofKeyThumbprint?: string; readonly expiresAt?: DateTime.DateTime; } export class ServerAuthInternalError extends Data.TaggedError("ServerAuthInternalError")<{ readonly message: string; readonly cause?: unknown; }> {} export class ServerAuthInvalidCredentialError extends Data.TaggedError( "ServerAuthInvalidCredentialError", )<{ readonly reason: "missing_credential" | "invalid_credential"; readonly cause?: unknown; }> {} export class ServerAuthInvalidRequestError extends Data.TaggedError( "ServerAuthInvalidRequestError", )<{ readonly reason: "invalid_scope" | "scope_not_granted"; }> {} export class ServerAuthForbiddenOperationError extends Data.TaggedError( "ServerAuthForbiddenOperationError", )<{ readonly reason: "current_session_revoke_not_allowed"; }> {} export interface EnvironmentAuthShape { readonly getDescriptor: () => Effect.Effect<ServerAuthDescriptor>; readonly getSessionState: ( request: HttpServerRequest.HttpServerRequest, ) => Effect.Effect<AuthSessionState, ServerAuthInternalError>; readonly createBrowserSession: ( credential: string, requestMetadata: AuthClientMetadata, ) => Effect.Effect< { readonly response: AuthBrowserSessionResult; readonly sessionToken: string; }, ServerAuthInvalidCredentialError | ServerAuthInternalError >; readonly exchangeBootstrapCredentialForAccessToken: ( credential: string, requestedScopes: ReadonlyArray<AuthEnvironmentScope> | undefined, requestMetadata: AuthClientMetadata, input?: { readonly proofKeyThumbprint?: string; }, ) => Effect.Effect< AuthAccessTokenResult, ServerAuthInvalidCredentialError | ServerAuthInvalidRequestError | ServerAuthInternalError >; readonly createPairingLink: (input?: { readonly ttl?: Duration.Duration; readonly label?: string; readonly scopes?: ReadonlyArray<AuthEnvironmentScope>; readonly subject?: string; readonly proofKeyThumbprint?: string; }) => Effect.Effect<IssuedPairingLink, ServerAuthInternalError>; readonly issuePairingCredential: ( input?: AuthCreatePairingCredentialInput, ) => Effect.Effect<AuthPairingCredentialResult, ServerAuthInternalError>; readonly issueStartupPairingCredential: () => Effect.Effect< AuthPairingCredentialResult, ServerAuthInternalError >; readonly listPairingLinks: (input?: { readonly excludeSubjects?: ReadonlyArray<string>; }) => Effect.Effect<ReadonlyArray<AuthPairingLink>, ServerAuthInternalError>; readonly revokePairingLink: (id: string) => Effect.Effect<boolean, ServerAuthInternalError>; readonly issueSession: (input?: { readonly ttl?: Duration.Duration; readonly subject?: string; readonly scopes?: ReadonlyArray<AuthEnvironmentScope>; readonly label?: string; }) => Effect.Effect<IssuedBearerSession, ServerAuthInternalError>; readonly listSessions: () => Effect.Effect< ReadonlyArray<AuthClientSession>, ServerAuthInternalError >; readonly revokeSession: ( sessionId: AuthSessionId, ) => Effect.Effect<boolean, ServerAuthInternalError>; readonly revokeOtherSessionsExcept: ( sessionId: AuthSessionId, ) => Effect.Effect<number, ServerAuthInternalError>; readonly listClientSessions: ( currentSessionId: AuthSessionId, ) => Effect.Effect<ReadonlyArray<AuthClientSession>, ServerAuthInternalError>; readonly revokeClientSession: ( currentSessionId: AuthSessionId, targetSessionId: AuthSessionId, ) => Effect.Effect<boolean, ServerAuthForbiddenOperationError | ServerAuthInternalError>; readonly revokeOtherClientSessions: ( currentSessionId: AuthSessionId, ) => Effect.Effect<number, ServerAuthInternalError>; readonly authenticateHttpRequest: ( request: HttpServerRequest.HttpServerRequest, ) => Effect.Effect< AuthenticatedSession, ServerAuthInvalidCredentialError | ServerAuthInternalError >; readonly authenticateWebSocketUpgrade: ( request: HttpServerRequest.HttpServerRequest, ) => Effect.Effect< AuthenticatedSession, ServerAuthInvalidCredentialError | ServerAuthInternalError >; readonly issueWebSocketTicket: ( session: Pick<AuthenticatedSession, "sessionId">, ) => Effect.Effect<AuthWebSocketTicketResult, ServerAuthInternalError>; readonly issueStartupPairingUrl: ( baseUrl: string, ) => Effect.Effect<string, ServerAuthInternalError>; } export class EnvironmentAuth extends Context.Service<EnvironmentAuth, EnvironmentAuthShape>()( "t3/auth/EnvironmentAuth", ) {} type BootstrapExchangeResult = { readonly response: AuthBrowserSessionResult; readonly sessionToken: string; }; const AUTHORIZATION_PREFIX = "Bearer "; const DPOP_AUTHORIZATION_PREFIX = "DPoP "; const WEBSOCKET_TICKET_QUERY_PARAM = "wsTicket"; const bySessionPriority = (left: AuthClientSession, right: AuthClientSession) => { const leftCanManage = left.scopes.includes(AuthAccessWriteScope); const rightCanManage = right.scopes.includes(AuthAccessWriteScope); if (leftCanManage !== rightCanManage) { return leftCanManage ? -1 : 1; } if (left.connected !== right.connected) { return left.connected ? -1 : 1; } return right.issuedAt.epochMilliseconds - left.issuedAt.epochMilliseconds; }; const toInternalError = (message: string) => (cause: unknown): ServerAuthInternalError => new ServerAuthInternalError({ message, cause }); export function toBootstrapExchangeError( cause: PairingGrantStore.BootstrapCredentialError, ): ServerAuthInvalidCredentialError | ServerAuthInternalError { if (cause._tag === "BootstrapCredentialInternalError") { return new ServerAuthInternalError({ message: "Failed to validate bootstrap credential.", cause, }); } return new ServerAuthInvalidCredentialError({ reason: "invalid_credential", cause, }); } const mapSessionVerificationErrors = <A, R>( effect: Effect.Effect<A, SessionStore.SessionCredentialError, R>, ): Effect.Effect<A, ServerAuthInvalidCredentialError | ServerAuthInternalError, R> => effect.pipe( Effect.catchTags({ SessionCredentialInvalidError: (cause) => Effect.fail(new ServerAuthInvalidCredentialError({ reason: "invalid_credential", cause })), SessionCredentialInternalError: (cause) => Effect.fail( new ServerAuthInternalError({ message: "Failed to validate session credential.", cause, }), ), }), ); function parseBearerToken(request: HttpServerRequest.HttpServerRequest): string | null { const header = request.headers["authorization"]; if (typeof header !== "string" || !header.startsWith(AUTHORIZATION_PREFIX)) { return null; } const token = header.slice(AUTHORIZATION_PREFIX.length).trim(); return token.length > 0 ? token : null; } function parseDpopToken(request: HttpServerRequest.HttpServerRequest): string | null { const header = request.headers["authorization"]; if (typeof header !== "string" || !header.startsWith(DPOP_AUTHORIZATION_PREFIX)) { return null; } const token = header.slice(DPOP_AUTHORIZATION_PREFIX.length).trim(); return token.length > 0 ? token : null; } export const make = Effect.fn("makeEnvironmentAuth")(function* () { const policy = yield* EnvironmentAuthPolicy.EnvironmentAuthPolicy; const bootstrapCredentials = yield* PairingGrantStore.PairingGrantStore; const sessions = yield* SessionStore.SessionStore; const secretStore = yield* ServerSecretStore.ServerSecretStore; const crypto = yield* Crypto.Crypto; const descriptor = yield* policy.getDescriptor(); const authenticateToken = ( token: string, ): Effect.Effect< AuthenticatedSession, ServerAuthInvalidCredentialError | ServerAuthInternalError > => sessions.verify(token).pipe( Effect.tapErrorTag("SessionCredentialInvalidError", (cause) => Effect.logWarning("Rejected authenticated session credential.").pipe( Effect.annotateLogs({ reason: cause.message, }), ), ), Effect.map((session) => ({ sessionId: session.sessionId, subject: session.subject, method: session.method, scopes: session.scopes, ...(session.proofKeyThumbprint ? { proofKeyThumbprint: session.proofKeyThumbprint } : {}), ...(session.expiresAt ? { expiresAt: session.expiresAt } : {}), })), mapSessionVerificationErrors, ); const authenticateRequest = (request: HttpServerRequest.HttpServerRequest) => { const cookieToken = request.cookies[sessions.cookieName]; const bearerToken = parseBearerToken(request); const dpopToken = parseDpopToken(request); const credential = cookieToken ?? bearerToken ?? dpopToken; if (!credential) { return Effect.fail(new ServerAuthInvalidCredentialError({ reason: "missing_credential" })); } return authenticateToken(credential).pipe( Effect.flatMap((session) => { if (session.proofKeyThumbprint) { if (!dpopToken || dpopToken !== credential) { return Effect.fail( new ServerAuthInvalidCredentialError({ reason: "invalid_credential", cause: "DPoP-bound access token requires DPoP authorization.", }), ); } return verifyRequestDpopProof({ request, expectedThumbprint: session.proofKeyThumbprint, expectedAccessToken: dpopToken, }).pipe( Effect.provideService(ServerSecretStore.ServerSecretStore, secretStore), Effect.provideService(Crypto.Crypto, crypto), Effect.as(session), ); } if (dpopToken) { return Effect.fail( new ServerAuthInvalidCredentialError({ reason: "invalid_credential", cause: "DPoP authorization requires a proof-bound access token.", }), ); } return Effect.succeed(session); }), ); }; const getSessionState: EnvironmentAuthShape["getSessionState"] = (request) => authenticateRequest(request).pipe( Effect.map( (session) => ({ authenticated: true, auth: descriptor, scopes: session.scopes, sessionMethod: session.method, ...(session.expiresAt ? { expiresAt: DateTime.toUtc(session.expiresAt) } : {}), }) satisfies AuthSessionState, ), Effect.catchTag("ServerAuthInvalidCredentialError", () => Effect.succeed({ authenticated: false, auth: descriptor, } satisfies AuthSessionState), ), Effect.withSpan("EnvironmentAuth.getSessionState"), ); const createBrowserSession: EnvironmentAuthShape["createBrowserSession"] = ( credential, requestMetadata, ) => bootstrapCredentials.consume(credential).pipe( Effect.mapError(toBootstrapExchangeError), Effect.flatMap((grant) => sessions .issue({ method: "browser-session-cookie", subject: grant.subject, scopes: grant.scopes, client: { ...requestMetadata, ...(grant.label ? { label: grant.label } : {}), }, }) .pipe( Effect.mapError( (cause) => new ServerAuthInternalError({ message: "Failed to issue authenticated session.", cause, }), ), ), ), Effect.map( (session) => ({ response: { authenticated: true, scopes: session.scopes, sessionMethod: session.method, expiresAt: DateTime.toUtc(session.expiresAt), } satisfies AuthBrowserSessionResult, sessionToken: session.token, }) satisfies BootstrapExchangeResult, ), Effect.withSpan("EnvironmentAuth.createBrowserSession"), ); const exchangeBootstrapCredentialForAccessToken: EnvironmentAuthShape["exchangeBootstrapCredentialForAccessToken"] = (credential, requestedScopes, requestMetadata, input) => bootstrapCredentials.consume(credential, input).pipe( Effect.mapError(toBootstrapExchangeError), Effect.flatMap((grant) => Effect.gen(function* () { const grantedScopes = requestedScopes ?? grant.scopes; if (!grantedScopes.every((scope) => grant.scopes.includes(scope))) { return yield* new ServerAuthInvalidRequestError({ reason: "scope_not_granted", }); } return yield* sessions .issue({ method: input?.proofKeyThumbprint ? "dpop-access-token" : "bearer-access-token", subject: grant.subject, scopes: grantedScopes, ...(input?.proofKeyThumbprint ? { proofKeyThumbprint: input.proofKeyThumbprint, ttl: Duration.hours(1), } : {}), client: { ...requestMetadata, ...(grant.label ? { label: grant.label } : {}), }, }) .pipe( Effect.mapError( (cause) => new ServerAuthInternalError({ message: "Failed to issue authenticated access token.", cause, }), ), ); }), ), Effect.flatMap((session) => DateTime.now.pipe( Effect.map( (now) => ({ access_token: session.token, issued_token_type: AuthAccessTokenType, token_type: input?.proofKeyThumbprint ? "DPoP" : "Bearer", expires_in: Math.max( 0, Math.floor( (session.expiresAt.epochMilliseconds - now.epochMilliseconds) / 1000, ), ), scope: encodeOAuthScope(session.scopes), }) satisfies AuthAccessTokenResult, ), ), ), Effect.withSpan("EnvironmentAuth.exchangeBootstrapCredentialForAccessToken"), ); const issuePairingCredentialForSubject = (input: { readonly scopes: ReadonlyArray<AuthEnvironmentScope>; readonly subject: string; readonly label?: string; }) => createPairingLink({ scopes: input.scopes, subject: input.subject, ...(input.label ? { label: input.label } : {}), }).pipe( Effect.map( (issued) => ({ id: issued.id, credential: issued.credential, ...(issued.label ? { label: issued.label } : {}), expiresAt: issued.expiresAt, }) satisfies AuthPairingCredentialResult, ), ); const createPairingLink: EnvironmentAuthShape["createPairingLink"] = Effect.fn( "EnvironmentAuth.createPairingLink", )( function* (input) { const createdAt = yield* DateTime.now; const issued = yield* bootstrapCredentials.issueOneTimeToken({ scopes: input?.scopes ?? AuthStandardClientScopes, subject: input?.subject ?? "one-time-token", ...(input?.ttl ? { ttl: input.ttl } : {}), ...(input?.label ? { label: input.label } : {}), ...(input?.proofKeyThumbprint ? { proofKeyThumbprint: input.proofKeyThumbprint } : {}), }); return { id: issued.id, credential: issued.credential, scopes: input?.scopes ?? AuthStandardClientScopes,