721 lines · 25.8 KB · TypeScript
RawDownload
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.