Skip to content

Commit a7fc406

Browse files
committed
fix(webapp): keep a delegated token's claims on the authenticated identity
The direct PAT authentication path returned identity only, so a user-actor token reaching it (admin routes and other direct callers) lost its environment scope. The claims now ride on the authentication result itself.
1 parent d00b126 commit a7fc406

2 files changed

Lines changed: 22 additions & 10 deletions

File tree

apps/webapp/app/services/apiAuth.server.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -409,15 +409,11 @@ function getApiKeyResult(apiKey: string): {
409409
}
410410

411411
/**
412-
* The authenticated user-actor. A user-actor token authenticates as its user, so it carries the
413-
* same `userId` a PAT does — plus the token's verified claims, on the actor itself, so any layer
414-
* holding the actor holds its environment scope. A caller that only forwards `{ userId }` would
415-
* silently drop the scope, which is what this shape prevents.
412+
* The authenticated user-actor. A user-actor token authenticates as its user, so it is the same
413+
* shape a PAT authenticates to — that shape now carries the token's verified claims itself, so
414+
* any layer holding the actor holds its environment scope.
416415
*/
417-
export type UserActorAuthenticatedActor = PersonalAccessTokenAuthenticationResult & {
418-
/** Verified claims. Present only when the caller presented a user-actor token. */
419-
userActor?: UserActorClaims;
420-
};
416+
export type UserActorAuthenticatedActor = PersonalAccessTokenAuthenticationResult;
421417

422418
export type AuthenticationResult =
423419
| {

apps/webapp/app/services/personalAccessToken.server.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import { logger } from "./logger.server";
66
import { rbac } from "./rbac.server";
77
import { decryptToken, encryptToken, hashToken } from "~/utils/tokens.server";
88
import { env } from "~/env.server";
9-
import { isUserActorToken } from "@trigger.dev/rbac";
9+
import {
10+
isUserActorToken,
11+
type UserActorClaims,
12+
verifyUserActorToken,
13+
} from "@trigger.dev/rbac";
1014

1115
const tokenValueLength = 40;
1216
//lowercase only, removed 0 and l to avoid confusion
@@ -115,6 +119,11 @@ export async function revokePersonalAccessToken(tokenId: string, userId: string)
115119

116120
export type PersonalAccessTokenAuthenticationResult = {
117121
userId: string;
122+
/**
123+
* Verified claims when the caller presented a delegated user-actor token. They ride on the
124+
* result so no caller can hold the actor without its environment scope.
125+
*/
126+
userActor?: UserActorClaims;
118127
};
119128

120129
/**
@@ -171,7 +180,14 @@ export async function authenticateApiRequestWithPersonalAccessToken(
171180
// The plugin verifies it (identity path → no org context to floor against).
172181
if (isUserActorToken(token)) {
173182
const result = await rbac.authenticateUserActor(request, {});
174-
return result.ok ? { userId: result.userId } : undefined;
183+
if (!result.ok) return undefined;
184+
185+
// The claims travel with the identity: a caller that only saw `{ userId }` would act with no
186+
// environment scope to enforce. A plugin on an older contract omits them, so verify here.
187+
const userActor = result.claims ?? (await verifyUserActorToken(env.SESSION_SECRET, token));
188+
if (!userActor) return undefined;
189+
190+
return { userId: result.userId, userActor };
175191
}
176192

177193
return authenticatePersonalAccessToken(token);

0 commit comments

Comments
 (0)