fix(webapp): correctly generate JWT tokens for preview branches after triggering a run (fix #2678) (#2695)
This commit is contained in:
@@ -11,6 +11,7 @@ import { prisma } from "~/db.server";
|
||||
import { env } from "~/env.server";
|
||||
import { ApiAuthenticationResultSuccess, getOneTimeUseToken } from "~/services/apiAuth.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { extractJwtSigningSecretKey } from "~/services/realtime/jwtAuth.server";
|
||||
import { determineRealtimeStreamsVersion } from "~/services/realtime/v1StreamsGlobal.server";
|
||||
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||
import { resolveIdempotencyKeyTTL } from "~/utils/idempotencyKeys.server";
|
||||
@@ -85,7 +86,7 @@ const { action, loader } = createActionApiRoute(
|
||||
isCached: false,
|
||||
}),
|
||||
buildResponseHeaders: async (responseBody, cachedEntity) => {
|
||||
return await responseHeaders(cachedEntity, authentication, triggerClient);
|
||||
return await responseHeaders(cachedEntity, authentication);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -140,7 +141,12 @@ const { action, loader } = createActionApiRoute(
|
||||
|
||||
await saveRequestIdempotency(requestIdempotencyKey, "trigger", result.run.id);
|
||||
|
||||
const $responseHeaders = await responseHeaders(result.run, authentication, triggerClient);
|
||||
const $responseHeaders = await responseHeaders(result.run, authentication);
|
||||
|
||||
logger.debug("responseHeaders authentication", {
|
||||
authentication,
|
||||
responseHeaders: $responseHeaders,
|
||||
});
|
||||
|
||||
return json(
|
||||
{
|
||||
@@ -170,39 +176,26 @@ const { action, loader } = createActionApiRoute(
|
||||
|
||||
async function responseHeaders(
|
||||
run: Pick<TaskRun, "friendlyId">,
|
||||
authentication: ApiAuthenticationResultSuccess,
|
||||
triggerClient?: string | null
|
||||
authentication: ApiAuthenticationResultSuccess
|
||||
): Promise<Record<string, string>> {
|
||||
const { environment, realtime } = authentication;
|
||||
|
||||
const claimsHeader = JSON.stringify({
|
||||
const claims = {
|
||||
sub: environment.id,
|
||||
pub: true,
|
||||
scopes: [`read:runs:${run.friendlyId}`],
|
||||
realtime,
|
||||
};
|
||||
|
||||
const jwt = await internal_generateJWT({
|
||||
secretKey: extractJwtSigningSecretKey(environment),
|
||||
payload: claims,
|
||||
expirationTime: "1h",
|
||||
});
|
||||
|
||||
if (triggerClient === "browser") {
|
||||
const claims = {
|
||||
sub: environment.id,
|
||||
pub: true,
|
||||
scopes: [`read:runs:${run.friendlyId}`],
|
||||
realtime,
|
||||
};
|
||||
|
||||
const jwt = await internal_generateJWT({
|
||||
secretKey: environment.apiKey,
|
||||
payload: claims,
|
||||
expirationTime: "1h",
|
||||
});
|
||||
|
||||
return {
|
||||
"x-trigger-jwt-claims": claimsHeader,
|
||||
"x-trigger-jwt": jwt,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
"x-trigger-jwt-claims": claimsHeader,
|
||||
"x-trigger-jwt-claims": JSON.stringify(claims),
|
||||
"x-trigger-jwt": jwt,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { OutOfEntitlementError } from "~/v3/services/triggerTask.server";
|
||||
import { HeadersSchema } from "./api.v1.tasks.$taskId.trigger";
|
||||
import { determineRealtimeStreamsVersion } from "~/services/realtime/v1StreamsGlobal.server";
|
||||
import { extractJwtSigningSecretKey } from "~/services/realtime/jwtAuth.server";
|
||||
|
||||
const { action, loader } = createActionApiRoute(
|
||||
{
|
||||
@@ -163,7 +164,7 @@ async function responseHeaders(
|
||||
};
|
||||
|
||||
const jwt = await generateJWT({
|
||||
secretKey: environment.apiKey,
|
||||
secretKey: extractJwtSigningSecretKey(environment),
|
||||
payload: claims,
|
||||
expirationTime: "1h",
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ import { BatchProcessingStrategy } from "~/v3/services/batchTriggerV3.server";
|
||||
import { OutOfEntitlementError } from "~/v3/services/triggerTask.server";
|
||||
import { HeadersSchema } from "./api.v1.tasks.$taskId.trigger";
|
||||
import { determineRealtimeStreamsVersion } from "~/services/realtime/v1StreamsGlobal.server";
|
||||
import { extractJwtSigningSecretKey } from "~/services/realtime/jwtAuth.server";
|
||||
|
||||
const { action, loader } = createActionApiRoute(
|
||||
{
|
||||
@@ -178,7 +179,7 @@ async function responseHeaders(
|
||||
};
|
||||
|
||||
const jwt = await generateJWT({
|
||||
secretKey: environment.apiKey,
|
||||
secretKey: extractJwtSigningSecretKey(environment),
|
||||
payload: claims,
|
||||
expirationTime: "1h",
|
||||
});
|
||||
|
||||
@@ -236,6 +236,8 @@ async function authenticateApiKeyWithFailure(
|
||||
case "PUBLIC_JWT": {
|
||||
const validationResults = await validatePublicJwtKey(result.apiKey);
|
||||
|
||||
logger.debug("validatePublicJwtKey", { validationResults });
|
||||
|
||||
if (!validationResults.ok) {
|
||||
return validationResults;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { json } from "@remix-run/server-runtime";
|
||||
import { validateJWT } from "@trigger.dev/core/v3/jwt";
|
||||
import { findEnvironmentById } from "~/models/runtimeEnvironment.server";
|
||||
import { AuthenticatedEnvironment } from "../apiAuth.server";
|
||||
import { logger } from "../logger.server";
|
||||
|
||||
export type ValidatePublicJwtKeySuccess = {
|
||||
ok: true;
|
||||
@@ -38,6 +39,8 @@ export async function validatePublicJwtKey(token: string): Promise<ValidatePubli
|
||||
environment.parentEnvironment?.apiKey ?? environment.apiKey
|
||||
);
|
||||
|
||||
logger.debug("validateJWT result", { result });
|
||||
|
||||
if (!result.ok) {
|
||||
switch (result.code) {
|
||||
case "ERR_JWT_EXPIRED": {
|
||||
@@ -89,6 +92,12 @@ export function isPublicJWT(token: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
export function extractJwtSigningSecretKey(
|
||||
environment: AuthenticatedEnvironment & { parentEnvironment?: { apiKey: string } }
|
||||
) {
|
||||
return environment.parentEnvironment?.apiKey ?? environment.apiKey;
|
||||
}
|
||||
|
||||
function extractJWTSub(token: string): string | undefined {
|
||||
// Split the token
|
||||
const parts = token.split(".");
|
||||
|
||||
Generated
+16
-29
@@ -2271,6 +2271,22 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/cli-v3
|
||||
|
||||
references/issue-2687:
|
||||
dependencies:
|
||||
'@trigger.dev/sdk':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/trigger-sdk
|
||||
dotenv:
|
||||
specifier: ^16.4.5
|
||||
version: 16.4.7
|
||||
tsx:
|
||||
specifier: ^4.0.0
|
||||
version: 4.20.6
|
||||
devDependencies:
|
||||
trigger.dev:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/cli-v3
|
||||
|
||||
references/nextjs-realtime:
|
||||
dependencies:
|
||||
'@ai-sdk/openai':
|
||||
@@ -6546,7 +6562,6 @@ packages:
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-arm64@0.16.17:
|
||||
@@ -6607,7 +6622,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-arm@0.15.18:
|
||||
@@ -6677,7 +6691,6 @@ packages:
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-x64@0.16.17:
|
||||
@@ -6738,7 +6751,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/darwin-arm64@0.16.17:
|
||||
@@ -6799,7 +6811,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/darwin-x64@0.16.17:
|
||||
@@ -6860,7 +6871,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/freebsd-arm64@0.16.17:
|
||||
@@ -6921,7 +6931,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/freebsd-x64@0.16.17:
|
||||
@@ -6982,7 +6991,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-arm64@0.16.17:
|
||||
@@ -7043,7 +7051,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-arm@0.16.17:
|
||||
@@ -7104,7 +7111,6 @@ packages:
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-ia32@0.16.17:
|
||||
@@ -7165,7 +7171,6 @@ packages:
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-loong64@0.15.18:
|
||||
@@ -7235,7 +7240,6 @@ packages:
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-mips64el@0.16.17:
|
||||
@@ -7296,7 +7300,6 @@ packages:
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-ppc64@0.16.17:
|
||||
@@ -7357,7 +7360,6 @@ packages:
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-riscv64@0.16.17:
|
||||
@@ -7418,7 +7420,6 @@ packages:
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-s390x@0.16.17:
|
||||
@@ -7479,7 +7480,6 @@ packages:
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-x64@0.16.17:
|
||||
@@ -7540,7 +7540,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/netbsd-arm64@0.25.1:
|
||||
@@ -7549,7 +7548,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/netbsd-x64@0.16.17:
|
||||
@@ -7610,7 +7608,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/openbsd-arm64@0.23.0:
|
||||
@@ -7627,7 +7624,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/openbsd-x64@0.16.17:
|
||||
@@ -7688,7 +7684,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/sunos-x64@0.16.17:
|
||||
@@ -7749,7 +7744,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-arm64@0.16.17:
|
||||
@@ -7810,7 +7804,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-ia32@0.16.17:
|
||||
@@ -7871,7 +7864,6 @@ packages:
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-x64@0.16.17:
|
||||
@@ -7932,7 +7924,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@eslint-community/eslint-utils@4.4.0(eslint@8.31.0):
|
||||
@@ -25006,7 +24997,6 @@ packages:
|
||||
'@esbuild/win32-arm64': 0.25.1
|
||||
'@esbuild/win32-ia32': 0.25.1
|
||||
'@esbuild/win32-x64': 0.25.1
|
||||
dev: true
|
||||
|
||||
/escalade@3.2.0:
|
||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||
@@ -26482,7 +26472,6 @@ packages:
|
||||
resolution: {integrity: sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==}
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
dev: true
|
||||
|
||||
/get-uri@6.0.1:
|
||||
resolution: {integrity: sha512-7ZqONUVqaabogsYNWlYj0t3YZaL6dhuEueZXGF+/YVmf6dHmaFg8/6psJKqhx9QykIDKzpGcy2cn4oV4YC7V/Q==}
|
||||
@@ -33697,7 +33686,6 @@ packages:
|
||||
|
||||
/resolve-pkg-maps@1.0.0:
|
||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||
dev: true
|
||||
|
||||
/resolve.exports@2.0.2:
|
||||
resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==}
|
||||
@@ -36153,7 +36141,6 @@ packages:
|
||||
get-tsconfig: 4.7.6
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
dev: true
|
||||
|
||||
/tsx@4.7.1:
|
||||
resolution: {integrity: sha512-8d6VuibXHtlN5E3zFkgY8u4DX7Y3Z27zvvPKVmLon/D4AjuKzarkUBTLDBgj9iTQ0hg5xM7c/mYiRVM+HETf0g==}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
TRIGGER_SECRET_KEY=tr_dev_...
|
||||
TRIGGER_API_URL=https://api.trigger.dev
|
||||
TRIGGER_BRANCH=
|
||||
@@ -0,0 +1,28 @@
|
||||
# Issue 2687 Reproduction
|
||||
|
||||
This reference project reproduces the issue where `Realtime` returns 401 when using `createTriggerPublicToken`.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Make sure your Trigger.dev instance is running (webapp).
|
||||
2. Copy `.env.example` to `.env` and fill in your details:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
3. Edit `.env` to set your `TRIGGER_SECRET_KEY` and `TRIGGER_API_URL`.
|
||||
|
||||
## Running the reproduction
|
||||
|
||||
Run the reproduction script:
|
||||
|
||||
```bash
|
||||
pnpm run repro
|
||||
```
|
||||
|
||||
## What it does
|
||||
|
||||
1. Generates a `triggerPublicToken` for `issue-2687-task`.
|
||||
2. Triggers the task using this token.
|
||||
3. Attempts to connect to the Realtime endpoint for the resulting run using the same token.
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "references-issue-2687",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"trigger.dev": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@trigger.dev/sdk": "workspace:*",
|
||||
"dotenv": "^16.4.5",
|
||||
"tsx": "^4.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "trigger dev",
|
||||
"deploy": "trigger deploy",
|
||||
"repro": "tsx src/repro.ts"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import "dotenv/config";
|
||||
import { auth, tasks, runs, configure } from "@trigger.dev/sdk/v3";
|
||||
|
||||
const taskId = "issue-2687-task";
|
||||
|
||||
async function main() {
|
||||
if (!process.env.TRIGGER_SECRET_KEY) {
|
||||
console.error("TRIGGER_SECRET_KEY is not set. Please set it to your project's secret key.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const apiUrl = process.env.TRIGGER_API_URL || "https://api.trigger.dev";
|
||||
const branch = process.env.TRIGGER_PREVIEW_BRANCH;
|
||||
const secretKey = process.env.TRIGGER_SECRET_KEY;
|
||||
|
||||
console.log(`Using API URL: ${apiUrl}`);
|
||||
console.log(`Using Secret Key: ${secretKey}`);
|
||||
if (branch) {
|
||||
console.log(`Using Branch: ${branch}`);
|
||||
}
|
||||
|
||||
// 1. Generate the public token (Server-side)
|
||||
// We need the secret key for this part, so we use the environment variable which the SDK picks up automatically for this call.
|
||||
console.log("Generating public token...");
|
||||
try {
|
||||
// Ensure we are using the correct API URL for the generation as well, if it matters (it validates against the env).
|
||||
configure({ baseURL: apiUrl, accessToken: secretKey, previewBranch: branch });
|
||||
|
||||
const token = await auth.createTriggerPublicToken(taskId);
|
||||
console.log("Token generated.");
|
||||
|
||||
// 2. Trigger the task using the public token
|
||||
console.log(`Triggering task ${taskId}...`);
|
||||
|
||||
// Use auth.withAuth to temporarily scope the SDK to the public token
|
||||
await auth.withAuth(
|
||||
{ accessToken: token, baseURL: apiUrl, previewBranch: branch },
|
||||
async () => {
|
||||
const handle = await tasks.trigger(taskId, { foo: "bar" });
|
||||
console.log(`Task triggered. Run ID: ${handle.id}`);
|
||||
|
||||
let tokenToUse = token;
|
||||
if (handle.publicAccessToken) {
|
||||
console.log("Received publicAccessToken in handle, using it for realtime.");
|
||||
console.log(`Public Access Token: ${handle.publicAccessToken}`);
|
||||
tokenToUse = handle.publicAccessToken;
|
||||
} else {
|
||||
console.log("Using initial token for subsequent requests.");
|
||||
}
|
||||
|
||||
// 3. Access Run details (Simulating Realtime/Read access)
|
||||
// If the token changed (which it might if the API returns a specific read-only token), we should use that.
|
||||
|
||||
if (tokenToUse !== token) {
|
||||
await auth.withAuth(
|
||||
{ accessToken: tokenToUse, baseURL: apiUrl, previewBranch: branch },
|
||||
async () => {
|
||||
console.log(`Subscribing to run ${handle.id} with new token...`);
|
||||
for await (const run of runs.subscribeToRun(handle.id)) {
|
||||
console.log(`Run update received. Status: ${run.status}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
console.log(`Subscribing to run ${handle.id} with initial token...`);
|
||||
for await (const run of runs.subscribeToRun(handle.id)) {
|
||||
console.log(`Run update received. Status: ${run.status}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Realtime/Read access verified.");
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,9 @@
|
||||
import { task } from "@trigger.dev/sdk/v3";
|
||||
|
||||
export const myTask = task({
|
||||
id: "issue-2687-task",
|
||||
run: async (payload: any) => {
|
||||
console.log("Task running with payload:", payload);
|
||||
return { message: "Hello World" };
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { defineConfig } from "@trigger.dev/sdk/v3";
|
||||
|
||||
export default defineConfig({
|
||||
project: process.env.TRIGGER_PROJECT_REF!,
|
||||
logLevel: "log",
|
||||
maxDuration: 3600,
|
||||
retries: {
|
||||
enabledInDev: true,
|
||||
default: {
|
||||
maxAttempts: 3,
|
||||
minTimeoutInMs: 1000,
|
||||
maxTimeoutInMs: 10000,
|
||||
factor: 2,
|
||||
randomize: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user