import { AuthAccessReadScope, AuthAccessWriteScope, AuthStandardClientScopes, AuthOrchestrationOperateScope, AuthOrchestrationReadScope, AuthRelayReadScope, AuthRelayWriteScope, AuthReviewWriteScope, AuthTerminalOperateScope, EnvironmentAuthInvalidError, type EnvironmentAuthInvalidReason, EnvironmentHttpApi, EnvironmentInternalError, type EnvironmentInternalErrorReason, EnvironmentOperationForbiddenError, EnvironmentRequestInvalidError, type EnvironmentRequestInvalidReason, EnvironmentResourceNotFoundError, type EnvironmentResourceNotFoundReason, EnvironmentScopeRequiredError, EnvironmentAuthenticatedAuth, EnvironmentAuthenticatedPrincipal, } from "@t3tools/contracts"; import type { AuthEnvironmentScope } from "@t3tools/contracts"; import { parseAllowedOAuthScope } from "@t3tools/shared/oauthScope"; import { causeErrorTag } from "@t3tools/shared/observability"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import { identity } from "effect/Function"; import * as Layer from "effect/Layer"; import * as Cookies from "effect/unstable/http/Cookies"; import * as HttpEffect from "effect/unstable/http/HttpEffect"; import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http"; import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder"; import * as EnvironmentAuth from "./EnvironmentAuth.ts"; import * as SessionStore from "./SessionStore.ts"; import { traceAuthenticatedRelayRequest, traceRelayRequest } from "../cloud/traceRelayRequest.ts"; import { deriveAuthClientMetadata } from "./utils.ts"; import { verifyRequestDpopProof } from "./dpop.ts"; const CREDENTIAL_RESPONSE_HEADERS = { "cache-control": "no-store", pragma: "no-cache", } as const; const appendCredentialResponseHeaders = HttpEffect.appendPreResponseHandler((_request, response) => Effect.succeed(HttpServerResponse.setHeaders(response, CREDENTIAL_RESPONSE_HEADERS)), ); const appendDpopChallengeHeader = HttpEffect.appendPreResponseHandler((_request, response) => Effect.succeed(HttpServerResponse.setHeader(response, "www-authenticate", "DPoP")), ); const appendDpopChallengeOnUnauthorized = (error: EnvironmentAuthInvalidError) => Effect.gen(function* () { const request = yield* HttpServerRequest.HttpServerRequest; const usesDpop = (request.originalUrl.startsWith("/oauth/token") && request.headers.dpop !== undefined) || request.headers.authorization?.startsWith("DPoP ") === true; if (usesDpop) { yield* appendDpopChallengeHeader; } return yield* error; }); export const currentEnvironmentTraceId = Effect.currentParentSpan.pipe( Effect.map((span) => span.traceId), Effect.orElseSucceed(() => "unavailable"), ); export function annotateEnvironmentRequest(endpoint: string) { return Effect.gen(function* () { const request = yield* HttpServerRequest.HttpServerRequest; const url = HttpServerRequest.toURL(request); const traceId = yield* currentEnvironmentTraceId; yield* Effect.addFinalizer((exit) => exit._tag === "Failure" ? Effect.logWarning("environment api request failed", { endpoint, traceId, errorTag: causeErrorTag(exit.cause), cause: exit.cause, }) : Effect.void, ); yield* Effect.annotateLogsScoped({ "environment.endpoint": endpoint, traceId }); yield* Effect.annotateCurrentSpan({ "environment.endpoint": endpoint, "http.request.method": request.method, "url.path": url._tag === "Some" ? url.value.pathname : "unknown", }); }); } export function failEnvironmentAuthInvalid(reason: EnvironmentAuthInvalidReason) { return currentEnvironmentTraceId.pipe( Effect.flatMap((traceId) => Effect.fail(new EnvironmentAuthInvalidError({ code: "auth_invalid", reason, traceId })), ), ); } export function failEnvironmentInvalidRequest(reason: EnvironmentRequestInvalidReason) { return currentEnvironmentTraceId.pipe( Effect.flatMap((traceId) => Effect.fail(new EnvironmentRequestInvalidError({ code: "invalid_request", reason, traceId })), ), ); } export function failEnvironmentScopeRequired(requiredScope: AuthEnvironmentScope) { return currentEnvironmentTraceId.pipe( Effect.flatMap((traceId) => Effect.fail( new EnvironmentScopeRequiredError({ code: "insufficient_scope", requiredScope, traceId, }), ), ), ); } function failEnvironmentOperationForbidden(reason: "current_session_revoke_not_allowed") { return currentEnvironmentTraceId.pipe( Effect.flatMap((traceId) => Effect.fail( new EnvironmentOperationForbiddenError({ code: "operation_forbidden", reason, traceId, }), ), ), ); } export function failEnvironmentNotFound(reason: EnvironmentResourceNotFoundReason) { return currentEnvironmentTraceId.pipe( Effect.flatMap((traceId) => Effect.fail(new EnvironmentResourceNotFoundError({ code: "not_found", reason, traceId })), ), ); } export function failEnvironmentInternal(reason: EnvironmentInternalErrorReason, error?: unknown) { return Effect.gen(function* () { const traceId = yield* currentEnvironmentTraceId; if (error !== undefined) { yield* Effect.logError("environment api operation failed", { reason, traceId, cause: error, }); } return yield* new EnvironmentInternalError({ code: "internal_error", reason, traceId }); }); } export const requireEnvironmentScope = Effect.fn("environment.auth.requireScope")(function* ( scope: AuthEnvironmentScope, ) { const session = yield* EnvironmentAuthenticatedPrincipal; if (!session.scopes.has(scope)) { return yield* failEnvironmentScopeRequired(scope); } return session; }); export const environmentAuthenticatedAuthLayer = Layer.effect( EnvironmentAuthenticatedAuth, Effect.gen(function* () { const serverAuth = yield* EnvironmentAuth.EnvironmentAuth; return (httpEffect) => Effect.gen(function* () { const request = yield* HttpServerRequest.HttpServerRequest; const session = yield* serverAuth.authenticateHttpRequest(request).pipe( Effect.catchIf(EnvironmentAuth.isServerAuthCredentialError, (error) => failEnvironmentAuthInvalid(EnvironmentAuth.serverAuthCredentialReason(error)), ), Effect.catchIf(EnvironmentAuth.isServerAuthInternalError, (error) => failEnvironmentInternal("internal_error", error), ), ); return yield* httpEffect.pipe( Effect.provideService(EnvironmentAuthenticatedPrincipal, { ...session, scopes: new Set(session.scopes), }), session.subject === "cloud-connect" ? traceAuthenticatedRelayRequest : identity, ); }).pipe(Effect.catchTag("EnvironmentAuthInvalidError", appendDpopChallengeOnUnauthorized)); }), ); export const authHttpApiLayer = HttpApiBuilder.group( EnvironmentHttpApi, "auth", Effect.fnUntraced(function* (handlers) { const serverAuth = yield* EnvironmentAuth.EnvironmentAuth; const sessions = yield* SessionStore.SessionStore; return handlers .handle( "session", Effect.fn("environment.auth.session")( function* (args) { yield* annotateEnvironmentRequest(args.endpoint.name); const request = yield* HttpServerRequest.HttpServerRequest; return yield* serverAuth.getSessionState(request); }, Effect.catchIf(EnvironmentAuth.isServerAuthInternalError, (error) => failEnvironmentInternal("internal_error", error), ), ), ) .handle( "browserSession", Effect.fn("environment.auth.browserSession")( function* (args) { yield* annotateEnvironmentRequest(args.endpoint.name); const request = yield* HttpServerRequest.HttpServerRequest; const result = yield* serverAuth.createBrowserSession( args.payload.credential, deriveAuthClientMetadata({ request }), ); const sessionCookies = yield* Effect.fromResult( Cookies.set(Cookies.empty, sessions.cookieName, result.sessionToken, { expires: DateTime.toDate(result.response.expiresAt), httpOnly: true, path: "/", sameSite: "lax", }), ).pipe(Effect.catch(() => failEnvironmentInternal("browser_session_cookie_failed"))); yield* HttpEffect.appendPreResponseHandler((_request, response) => Effect.succeed(HttpServerResponse.mergeCookies(response, sessionCookies)), ); yield* appendCredentialResponseHeaders; return result.response; }, Effect.catchIf(EnvironmentAuth.isServerAuthCredentialError, (error) => failEnvironmentAuthInvalid(EnvironmentAuth.serverAuthCredentialReason(error)), ), Effect.catchIf(EnvironmentAuth.isServerAuthInternalError, (error) => failEnvironmentInternal("browser_session_issuance_failed", error), ), ), ) .handle( "token", Effect.fn("environment.auth.token")( function* (args) { yield* annotateEnvironmentRequest(args.endpoint.name); const request = yield* HttpServerRequest.HttpServerRequest; const requestedScopes = args.payload.scope === undefined ? undefined : parseAllowedOAuthScope({ value: args.payload.scope, allowedScopes: new Set([ AuthOrchestrationReadScope, AuthOrchestrationOperateScope, AuthTerminalOperateScope, AuthReviewWriteScope, AuthAccessReadScope, AuthAccessWriteScope, AuthRelayReadScope, AuthRelayWriteScope, ]), }); if (requestedScopes === null) { return yield* failEnvironmentInvalidRequest("invalid_scope"); } const proofKeyThumbprint = args.headers.dpop ? yield* verifyRequestDpopProof({ request }).pipe( Effect.catchIf(EnvironmentAuth.isServerAuthCredentialError, () => appendDpopChallengeHeader.pipe( Effect.andThen(failEnvironmentAuthInvalid("invalid_credential")), ), ), Effect.catchIf(EnvironmentAuth.isServerAuthInternalError, (error) => failEnvironmentInternal("access_token_issuance_failed", error), ), ) : undefined; yield* appendCredentialResponseHeaders; return yield* serverAuth.exchangeBootstrapCredentialForAccessToken( args.payload.subject_token, requestedScopes, deriveAuthClientMetadata({ request, presented: { ...(args.payload.client_label ? { label: args.payload.client_label } : {}), ...(args.payload.client_device_type ? { deviceType: args.payload.client_device_type } : {}), ...(args.payload.client_os ? { os: args.payload.client_os } : {}), }, }), proofKeyThumbprint ? { proofKeyThumbprint } : undefined, ); }, traceRelayRequest, Effect.catchIf(EnvironmentAuth.isServerAuthCredentialError, (error) => failEnvironmentAuthInvalid(EnvironmentAuth.serverAuthCredentialReason(error)), ), Effect.catchIf(EnvironmentAuth.isServerAuthInvalidRequestError, (error) => failEnvironmentInvalidRequest(EnvironmentAuth.serverAuthInvalidRequestReason(error)), ), Effect.catchIf(EnvironmentAuth.isServerAuthInternalError, (error) => failEnvironmentInternal("access_token_issuance_failed", error), ), ), ) .handle( "webSocketTicket", Effect.fn("environment.auth.webSocketTicket")( function* (args) { yield* annotateEnvironmentRequest(args.endpoint.name); const session = yield* EnvironmentAuthenticatedPrincipal; yield* appendCredentialResponseHeaders; return yield* serverAuth.issueWebSocketTicket(session); }, Effect.catchIf(EnvironmentAuth.isServerAuthInternalError, (error) => failEnvironmentInternal("websocket_ticket_issuance_failed", error), ), ), ) .handle( "pairingCredential", Effect.fn("environment.auth.pairingCredential")( function* (args) { yield* annotateEnvironmentRequest(args.endpoint.name); const session = yield* requireEnvironmentScope(AuthAccessWriteScope); const delegatedScopes = args.payload.scopes ?? AuthStandardClientScopes; if ( delegatedScopes.length === 0 || new Set(delegatedScopes).size !== delegatedScopes.length ) { return yield* failEnvironmentInvalidRequest("invalid_scope"); } for (const delegatedScope of delegatedScopes) { if (!session.scopes.has(delegatedScope)) { return yield* failEnvironmentScopeRequired(delegatedScope); } } return yield* serverAuth.issuePairingCredential(args.payload); }, Effect.catchIf(EnvironmentAuth.isServerAuthInternalError, (error) => failEnvironmentInternal("pairing_credential_issuance_failed", error), ), ), ) .handle( "pairingLinks", Effect.fn("environment.auth.pairingLinks")( function* (args) { yield* annotateEnvironmentRequest(args.endpoint.name); yield* requireEnvironmentScope(AuthAccessReadScope); return yield* serverAuth.listPairingLinks(); }, Effect.catchIf(EnvironmentAuth.isServerAuthInternalError, (error) => failEnvironmentInternal("pairing_links_load_failed", error), ), ), ) .handle( "revokePairingLink", Effect.fn("environment.auth.revokePairingLink")( function* (args) { yield* annotateEnvironmentRequest(args.endpoint.name); yield* requireEnvironmentScope(AuthAccessWriteScope); const revoked = yield* serverAuth.revokePairingLink(args.payload.id); return { revoked }; }, Effect.catchIf(EnvironmentAuth.isServerAuthInternalError, (error) => failEnvironmentInternal("pairing_link_revoke_failed", error), ), ), ) .handle( "clients", Effect.fn("environment.auth.clients")( function* (args) { yield* annotateEnvironmentRequest(args.endpoint.name); const session = yield* requireEnvironmentScope(AuthAccessReadScope); return yield* serverAuth.listClientSessions(session.sessionId); }, Effect.catchIf(EnvironmentAuth.isServerAuthInternalError, (error) => failEnvironmentInternal("client_sessions_load_failed", error), ), ), ) .handle( "revokeClient", Effect.fn("environment.auth.revokeClient")( function* (args) { yield* annotateEnvironmentRequest(args.endpoint.name); const session = yield* requireEnvironmentScope(AuthAccessWriteScope); const revoked = yield* serverAuth.revokeClientSession( session.sessionId, args.payload.sessionId, ); return { revoked }; }, Effect.catchTag("ServerAuthForbiddenOperationError", () => failEnvironmentOperationForbidden("current_session_revoke_not_allowed"), ), Effect.catchIf(EnvironmentAuth.isServerAuthInternalError, (error) => failEnvironmentInternal("client_session_revoke_failed", error), ), ), ) .handle( "revokeOtherClients", Effect.fn("environment.auth.revokeOtherClients")( function* (args) { yield* annotateEnvironmentRequest(args.endpoint.name); const session = yield* requireEnvironmentScope(AuthAccessWriteScope); const revokedCount = yield* serverAuth.revokeOtherClientSessions(session.sessionId); return { revokedCount }; }, Effect.catchIf(EnvironmentAuth.isServerAuthInternalError, (error) => failEnvironmentInternal("client_session_revoke_failed", error), ), ), ); }), );