Files

170 lines
5.9 KiB
TypeScript

import { getMeter } from "@internal/tracing";
import { isAdditionalApiKey } from "@trigger.dev/core/v3/apiKeys";
import { isPublicJWT } from "@trigger.dev/core/v3/jwt";
import type { BearerAuthOptions } from "@trigger.dev/plugins";
import type {
BearerCredentialKind,
BearerLookupPath,
HostBearerAuthResult,
RbacResource,
} from "@trigger.dev/rbac";
import { authFeatureControls } from "~/services/authFeatureControls.server";
import { rbac } from "~/services/rbac.server";
import { singleton } from "~/utils/singleton";
export type ApiAuthResult = "success" | "invalid" | "forbidden" | "disabled" | "error";
const telemetry = singleton("apiAuthTelemetry", () => {
const meter = getMeter("api-auth");
const attempts = meter.createCounter("api_auth.attempts", {
description: "Completed environment bearer authentication attempts",
});
const duration = meter.createHistogram("api_auth.duration_ms", {
description: "Environment bearer authentication duration",
unit: "ms",
});
meter
.createObservableGauge("api_auth.rollout_mode", {
description: "Active API authentication rollout modes",
})
.addCallback((result) => {
result.observe(1, {
control: "additional_key_lookup",
mode: authFeatureControls.additionalApiKeyLookupEnabled() ? "enabled" : "disabled",
});
});
return { attempts, duration };
});
export async function authenticateBearerWithTelemetry(
request: Request,
options: BearerAuthOptions
): Promise<HostBearerAuthResult> {
const startedAt = performance.now();
const classified = classifyCredential(request, options.allowJWT ?? false);
let final = { ...classified, result: "error" as ApiAuthResult };
try {
const result = await rbac.authenticateBearer(request, options);
// The host LazyController always attaches `resolution`; fall back to the
// format-based classification if a caller (e.g. a test double) omits it.
const resolution = result.resolution ?? classified;
final = {
credentialKind: resolution.credentialKind,
lookupPath: resolution.lookupPath,
result: result.ok
? "success"
: resolution.lookupPath === "additional_skipped"
? "disabled"
: result.status === 403
? "forbidden"
: "invalid",
};
recordAuthAttempt("rbac", final.credentialKind, final.lookupPath, final.result);
return result;
} catch (error) {
recordAuthAttempt("rbac", final.credentialKind, final.lookupPath, final.result);
throw error;
} finally {
telemetry.duration.record(performance.now() - startedAt, {
resolver: "rbac",
credential_kind: final.credentialKind,
result: final.result,
lookup_path: final.lookupPath,
});
}
}
export async function authenticateAuthorizeBearerWithTelemetry(
request: Request,
check: { action: string; resource: RbacResource },
options: BearerAuthOptions
) {
// Keep authentication telemetry consistent with apiBuilder: a valid
// credential records a successful authentication even when the subsequent
// resource authorization fails. Authorization correctness is covered by the
// route tests rather than folded into the authentication-health metric.
const result = await authenticateBearerWithTelemetry(request, options);
if (!result.ok) return result;
if (!result.ability.can(check.action, check.resource)) {
return { ok: false as const, status: 403 as const, error: "Unauthorized" };
}
return result;
}
export async function observeLegacyBearerAuthentication<T extends { ok: boolean } | undefined>(
request: Request,
operation: () => Promise<T>
): Promise<T> {
const startedAt = performance.now();
const classified = classifyCredential(request, true);
const lookupPath: BearerLookupPath =
classified.credentialKind === "additional_api_key" &&
!authFeatureControls.additionalApiKeyLookupEnabled()
? "additional_skipped"
: classified.lookupPath;
let result: ApiAuthResult = "error";
try {
const value = await operation();
result = value?.ok ? "success" : lookupPath === "additional_skipped" ? "disabled" : "invalid";
recordAuthAttempt("legacy", classified.credentialKind, lookupPath, result);
return value;
} catch (error) {
recordAuthAttempt("legacy", classified.credentialKind, lookupPath, result);
throw error;
} finally {
telemetry.duration.record(performance.now() - startedAt, {
resolver: "legacy",
credential_kind: classified.credentialKind,
result,
lookup_path: lookupPath,
});
}
}
function recordAuthAttempt(
resolver: "rbac" | "legacy",
credentialKind: BearerCredentialKind,
lookupPath: BearerLookupPath,
result: ApiAuthResult
) {
telemetry.attempts.add(1, {
resolver,
credential_kind: credentialKind,
result,
lookup_path: lookupPath,
});
}
// Best-effort pre-classification from the raw token format. This is only used
// for the metric attributes when the resolver throws before returning a
// resolution; the resolver's own resolution is authoritative on success/failure.
// Never records the credential itself — only its bounded format class.
function classifyCredential(
request: Request,
allowJWT: boolean
): { credentialKind: BearerCredentialKind; lookupPath: BearerLookupPath } {
const token = request.headers
.get("Authorization")
?.replace(/^Bearer /, "")
.trim();
if (!token) return { credentialKind: "unknown", lookupPath: "not_found" };
if (token.startsWith("pk_")) {
return { credentialKind: "legacy_public_key", lookupPath: "legacy_public" };
}
if (allowJWT && isPublicJWT(token)) {
return { credentialKind: "public_jwt", lookupPath: "jwt_current" };
}
if (isAdditionalApiKey(token)) {
return { credentialKind: "additional_api_key", lookupPath: "additional" };
}
return token.startsWith("tr_")
? { credentialKind: "root_api_key", lookupPath: "root_current" }
: { credentialKind: "unknown", lookupPath: "not_found" };
}