Skip to content

Commit 9aa4d99

Browse files
ericallamTrigger.dev RepoOps
authored andcommitted
fix(webapp,rbac): stop exposing environment ids in public access tokens
Public access tokens no longer carry the environment id in their subject claim. The subject is now the environment id encrypted with a key derived from the server encryption key, so a token held by a browser does not reveal internal ids. Tokens minted before this change keep working. Mono-RevId: 28c843699d1f5278225549bd010cfd23dac46572
1 parent ef6ad5b commit 9aa4d99

20 files changed

Lines changed: 237 additions & 26 deletions

‎apps/webapp/app/env.server.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,7 @@ const EnvironmentSchema = z
416416
"ENCRYPTION_KEY must be exactly 32 bytes"
417417
)
418418
.refine(isNotInsecureSecret, INSECURE_SECRET_MESSAGE),
419+
PUBLIC_TOKEN_SEALED_SUBJECT_ENABLED: BoolEnv.default(false),
419420
// Enables the AWS_KMS secret store provider. Callers opt in per secret; everything else
420421
// stays encrypted with ENCRYPTION_KEY.
421422
SECRET_STORE_AWS_KMS_KEY_ID: z.string().optional(),

‎apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.$agentParam/route.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export const handle: Handle = {
6565
agentPageContext: (data) => playgroundAgentPageContext(data),
6666
};
6767
import { pageMeta } from "~/utils/pageTitle";
68+
import { publicTokenSubject } from "~/services/publicTokenSubject.server";
6869

6970
export const meta = pageMeta(({ params }) => [params.agentParam ?? "Agent", "Playground"]);
7071

@@ -127,7 +128,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
127128
jwt = await internal_generateJWT({
128129
secretKey: extractJwtSigningSecretKey(environment),
129130
payload: {
130-
sub: environment.id,
131+
sub: publicTokenSubject(environment.id),
131132
pub: true,
132133
scopes: [`read:runs:${conv.runFriendlyId}`, `write:inputStreams:${conv.runFriendlyId}`],
133134
},

‎apps/webapp/app/routes/api.v1.auth.jwt.claims.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
22
import { json } from "@remix-run/server-runtime";
33
import { authenticateApiRequest } from "~/services/apiAuth.server";
44
import { logger } from "~/services/logger.server";
5+
import { publicTokenSubject } from "~/services/publicTokenSubject.server";
56

67
export async function action({ request }: LoaderFunctionArgs) {
78
try {
@@ -13,7 +14,7 @@ export async function action({ request }: LoaderFunctionArgs) {
1314
}
1415

1516
const claims = {
16-
sub: authenticationResult.environment.id,
17+
sub: publicTokenSubject(authenticationResult.environment.id),
1718
pub: true,
1819
};
1920

‎apps/webapp/app/routes/api.v1.auth.jwt.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { z } from "zod";
77
import { authenticateBearerWithTelemetry } from "~/services/authTelemetry.server";
88
import { isApiKeyInGraceWindow, presentedBearerToken } from "~/services/apiKeyGraceWindow.server";
99
import { logger } from "~/services/logger.server";
10+
import { publicTokenSubject } from "~/services/publicTokenSubject.server";
1011

1112
const RequestBodySchema = z.object({
1213
claims: z
@@ -119,7 +120,7 @@ export async function action({ request }: LoaderFunctionArgs) {
119120
}
120121

121122
const claims = {
122-
sub: authenticationResult.environment.id,
123+
sub: publicTokenSubject(authenticationResult.environment.id),
123124
pub: true,
124125
...(requestedScopes.length > 0 ? { scopes: requestedScopes } : {}),
125126
};

‎apps/webapp/app/routes/api.v1.projects.$projectRef.$env.jwt.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { env as appEnv } from "~/env.server";
2020
import { assertSourcePatActive } from "~/services/personalAccessToken.server";
2121
import { logger } from "~/services/logger.server";
2222
import { authorizePatEnvironmentAccess } from "~/services/environmentVariableApiAccess.server";
23+
import { publicTokenSubject } from "~/services/publicTokenSubject.server";
2324

2425
const ParamsSchema = z.object({
2526
projectRef: z.string(),
@@ -185,7 +186,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
185186
: undefined);
186187

187188
const claims = {
188-
sub: runtimeEnv.id,
189+
sub: publicTokenSubject(runtimeEnv.id),
189190
pub: true,
190191
...(scopes ? { scopes } : {}),
191192
...(actorUserId

‎apps/webapp/app/routes/api.v1.tasks.$taskId.trigger.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { sanitizeTriggerSource } from "~/utils/triggerSource";
3030
import { runStore } from "~/v3/runStore.server";
3131
import { ServiceValidationError } from "~/v3/services/baseService.server";
3232
import { OutOfEntitlementError, TriggerTaskService } from "~/v3/services/triggerTask.server";
33+
import { publicTokenSubject } from "~/services/publicTokenSubject.server";
3334

3435
const ParamsSchema = z.object({
3536
taskId: z.string(),
@@ -220,7 +221,7 @@ async function responseHeaders(
220221
const { environment, realtime } = authentication;
221222

222223
const claims = {
223-
sub: environment.id,
224+
sub: publicTokenSubject(environment.id),
224225
pub: true,
225226
scopes: [`read:runs:${run.friendlyId}`, `write:inputStreams:${run.friendlyId}`],
226227
realtime,

‎apps/webapp/app/routes/api.v2.tasks.batch.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { clientSafeErrorMessage } from "~/utils/prismaErrors";
2727
import { HeadersSchema } from "./api.v1.tasks.$taskId.trigger";
2828
import { determineRealtimeStreamsVersion } from "~/services/realtime/v1StreamsGlobal.server";
2929
import { extractJwtSigningSecretKey } from "~/services/realtime/jwtAuth.server";
30+
import { publicTokenSubject } from "~/services/publicTokenSubject.server";
3031

3132
const { action, loader } = createActionApiRoute(
3233
{
@@ -215,7 +216,7 @@ async function responseHeaders(
215216
environment: AuthenticatedEnvironment
216217
): Promise<Record<string, string>> {
217218
const claims = {
218-
sub: environment.id,
219+
sub: publicTokenSubject(environment.id),
219220
pub: true,
220221
scopes: [`read:batch:${batch.id}`],
221222
};
@@ -227,7 +228,7 @@ async function responseHeaders(
227228
});
228229

229230
return {
230-
"x-trigger-jwt-claims": JSON.stringify({ sub: environment.id, pub: true }),
231+
"x-trigger-jwt-claims": JSON.stringify({ sub: publicTokenSubject(environment.id), pub: true }),
231232
"x-trigger-jwt": jwt,
232233
};
233234
}

‎apps/webapp/app/routes/api.v3.batches.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { ServiceValidationError } from "~/v3/services/baseService.server";
2828
import { OutOfEntitlementError } from "~/v3/services/triggerTask.server";
2929
import { engine } from "~/v3/runEngine.server";
3030
import { HeadersSchema } from "./api.v1.tasks.$taskId.trigger";
31+
import { publicTokenSubject } from "~/services/publicTokenSubject.server";
3132

3233
/**
3334
* Phase 1 of 2-phase batch API: Create a batch.
@@ -261,15 +262,15 @@ async function responseHeaders(
261262
const jwt = await generateJWT({
262263
secretKey: extractJwtSigningSecretKey(environment),
263264
payload: {
264-
sub: environment.id,
265+
sub: publicTokenSubject(environment.id),
265266
pub: true,
266267
scopes,
267268
},
268269
expirationTime: "1h",
269270
});
270271

271272
return {
272-
"x-trigger-jwt-claims": JSON.stringify({ sub: environment.id, pub: true }),
273+
"x-trigger-jwt-claims": JSON.stringify({ sub: publicTokenSubject(environment.id), pub: true }),
273274
"x-trigger-jwt": jwt,
274275
};
275276
}

‎apps/webapp/app/services/authTelemetry.server.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
SDK_CREDENTIAL_OPERATION_HEADER,
77
SDK_CREDENTIAL_REASON_HEADER,
88
} from "@trigger.dev/core/v3";
9-
import { isPublicJWT } from "@trigger.dev/core/v3/jwt";
9+
import { extractJWTSub, isPublicJWT } from "@trigger.dev/core/v3/jwt";
1010
import { isDefaultDevBranch, sanitizeBranchName } from "@trigger.dev/core/v3/utils/gitBranch";
1111
import type { BearerAuthOptions, BearerAuthResult } from "@trigger.dev/plugins";
1212
import type {
@@ -17,6 +17,7 @@ import type {
1717
RbacResource,
1818
RoleBaseAccessController,
1919
} from "@trigger.dev/rbac";
20+
import { isSealedPublicTokenSubject } from "@trigger.dev/rbac";
2021
import { authenticateRunToken } from "~/services/oidc/runTokenAuth.server";
2122
import { isRunTokenFormat } from "~/services/oidc/runTokenVerify";
2223
import { rbac } from "~/services/rbac.server";
@@ -220,7 +221,11 @@ function classifyCredential(
220221
return { credentialKind: "legacy_public_key", lookupPath: "legacy_public" };
221222
}
222223
if (allowJWT && isPublicJWT(token)) {
223-
return { credentialKind: "public_jwt", lookupPath: "jwt_current" };
224+
const sub = extractJWTSub(token);
225+
return {
226+
credentialKind: "public_jwt",
227+
lookupPath: sub && !isSealedPublicTokenSubject(sub) ? "jwt_legacy_subject" : "jwt_current",
228+
};
224229
}
225230
if (isAdditionalApiKey(token)) {
226231
return { credentialKind: "additional_api_key", lookupPath: "additional" };

‎apps/webapp/app/services/publicAccessTokenResponse.server.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { generateJWT } from "@trigger.dev/core/v3";
22
import { resolveJwtSigningKey } from "@trigger.dev/rbac";
3+
import { publicTokenSubject } from "~/services/publicTokenSubject.server";
34

45
export type PublicAccessTokenEnvironment = {
56
id: string;
@@ -19,15 +20,15 @@ export async function publicAccessTokenResponseHeaders({
1920
const jwt = await generateJWT({
2021
secretKey: resolveJwtSigningKey(environment),
2122
payload: {
22-
sub: environment.id,
23+
sub: publicTokenSubject(environment.id),
2324
pub: true,
2425
scopes,
2526
},
2627
expirationTime,
2728
});
2829

2930
return {
30-
"x-trigger-jwt-claims": JSON.stringify({ sub: environment.id, pub: true }),
31+
"x-trigger-jwt-claims": JSON.stringify({ sub: publicTokenSubject(environment.id), pub: true }),
3132
"x-trigger-jwt": jwt,
3233
};
3334
}

0 commit comments

Comments
 (0)