Skip to content

Commit 364ea56

Browse files
authored
Use one-time use tokens when triggering or batch triggering from the frontend (#1515)
* Use one-time use tokens when triggering or batch triggering from the frontend * Add changeset
1 parent 5825272 commit 364ea56

File tree

13 files changed

+949
-463
lines changed

13 files changed

+949
-463
lines changed

.changeset/bright-lizards-wait.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@trigger.dev/sdk": patch
3+
---
4+
5+
Add one-time use public tokens to trigger and batch trigger

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { generateJWT as internal_generateJWT, TriggerTaskRequestBody } from "@tr
33
import { TaskRun } from "@trigger.dev/database";
44
import { z } from "zod";
55
import { env } from "~/env.server";
6-
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
6+
import { AuthenticatedEnvironment, getOneTimeUseToken } from "~/services/apiAuth.server";
77
import { logger } from "~/services/logger.server";
88
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
99
import { resolveIdempotencyKeyTTL } from "~/utils/idempotencyKeys.server";
@@ -33,7 +33,7 @@ const { action, loader } = createActionApiRoute(
3333
allowJWT: true,
3434
maxContentLength: env.TASK_PAYLOAD_MAXIMUM_SIZE,
3535
authorization: {
36-
action: "write",
36+
action: "trigger",
3737
resource: (params) => ({ tasks: params.taskId }),
3838
superScopes: ["write:tasks", "admin"],
3939
},
@@ -59,6 +59,8 @@ const { action, loader } = createActionApiRoute(
5959
? { traceparent, tracestate }
6060
: undefined;
6161

62+
const oneTimeUseToken = await getOneTimeUseToken(authentication);
63+
6264
logger.debug("Triggering task", {
6365
taskId: params.taskId,
6466
idempotencyKey,
@@ -78,6 +80,7 @@ const { action, loader } = createActionApiRoute(
7880
triggerVersion: triggerVersion ?? undefined,
7981
traceContext,
8082
spanParentAsLink: spanParentAsLink === 1,
83+
oneTimeUseToken,
8184
});
8285

8386
if (!run) {

apps/webapp/app/routes/api.v1.tasks.batch.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { resolveIdempotencyKeyTTL } from "~/utils/idempotencyKeys.server";
1212
import { BatchTriggerV2Service } from "~/v3/services/batchTriggerV2.server";
1313
import { ServiceValidationError } from "~/v3/services/baseService.server";
1414
import { OutOfEntitlementError } from "~/v3/services/triggerTask.server";
15-
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
15+
import { AuthenticatedEnvironment, getOneTimeUseToken } from "~/services/apiAuth.server";
1616
import { logger } from "~/services/logger.server";
1717

1818
const { action, loader } = createActionApiRoute(
@@ -22,7 +22,7 @@ const { action, loader } = createActionApiRoute(
2222
allowJWT: true,
2323
maxContentLength: env.BATCH_TASK_PAYLOAD_MAXIMUM_SIZE,
2424
authorization: {
25-
action: "write",
25+
action: "batchTrigger",
2626
resource: (_, __, ___, body) => ({
2727
tasks: Array.from(new Set(body.items.map((i) => i.task))),
2828
}),
@@ -56,6 +56,8 @@ const { action, loader } = createActionApiRoute(
5656
tracestate,
5757
} = headers;
5858

59+
const oneTimeUseToken = await getOneTimeUseToken(authentication);
60+
5961
logger.debug("Batch trigger request", {
6062
idempotencyKey,
6163
idempotencyKeyTTL,
@@ -86,6 +88,7 @@ const { action, loader } = createActionApiRoute(
8688
triggerVersion: triggerVersion ?? undefined,
8789
traceContext,
8890
spanParentAsLink: spanParentAsLink === 1,
91+
oneTimeUseToken,
8992
});
9093

9194
const $responseHeaders = await responseHeaders(

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import { isPublicJWT, validatePublicJwtKey } from "./realtime/jwtAuth.server";
2020

2121
const ClaimsSchema = z.object({
2222
scopes: z.array(z.string()).optional(),
23+
// One-time use token
24+
otu: z.boolean().optional(),
2325
});
2426

2527
type Optional<T, K extends keyof T> = Prettify<Omit<T, K> & Partial<Pick<T, K>>>;
@@ -39,6 +41,7 @@ export type ApiAuthenticationResultSuccess = {
3941
type: "PUBLIC" | "PRIVATE" | "PUBLIC_JWT";
4042
environment: AuthenticatedEnvironment;
4143
scopes?: string[];
44+
oneTimeUse?: boolean;
4245
};
4346

4447
export type ApiAuthenticationResultFailure = {
@@ -146,6 +149,7 @@ export async function authenticateApiKey(
146149
...result,
147150
environment: validationResults.environment,
148151
scopes: parsedClaims.success ? parsedClaims.data.scopes : [],
152+
oneTimeUse: parsedClaims.success ? parsedClaims.data.otu : false,
149153
};
150154
}
151155
}
@@ -227,6 +231,7 @@ export async function authenticateApiKeyWithFailure(
227231
...result,
228232
environment: validationResults.environment,
229233
scopes: parsedClaims.success ? parsedClaims.data.scopes : [],
234+
oneTimeUse: parsedClaims.success ? parsedClaims.data.otu : false,
230235
};
231236
}
232237
}
@@ -531,3 +536,20 @@ function calculateJWTExpiration() {
531536

532537
return (Date.now() + DEFAULT_JWT_EXPIRATION_IN_MS) / 1000;
533538
}
539+
540+
export async function getOneTimeUseToken(
541+
auth: ApiAuthenticationResultSuccess
542+
): Promise<string | undefined> {
543+
if (auth.type !== "PUBLIC_JWT") {
544+
return;
545+
}
546+
547+
if (!auth.oneTimeUse) {
548+
return;
549+
}
550+
551+
// Hash the API key to make it unique
552+
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(auth.apiKey));
553+
554+
return Buffer.from(hash).toString("hex");
555+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export type AuthorizationAction = "read" | "write"; // Add more actions as needed
1+
export type AuthorizationAction = "read" | "write" | string; // Add more actions as needed
22

33
const ResourceTypes = ["tasks", "tags", "runs", "batch"] as const;
44

apps/webapp/app/services/routeBuilders/apiBuilder.server.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -570,10 +570,25 @@ export function createActionApiRoute<
570570
scopes: authenticationResult.scopes,
571571
});
572572

573-
if (!checkAuthorization(authenticationResult, action, $resource, superScopes)) {
573+
const authorizationResult = checkAuthorization(
574+
authenticationResult,
575+
action,
576+
$resource,
577+
superScopes
578+
);
579+
580+
if (!authorizationResult.authorized) {
574581
return await wrapResponse(
575582
request,
576-
json({ error: "Unauthorized" }, { status: 403 }),
583+
json(
584+
{
585+
error: `Unauthorized: ${authorizationResult.reason}`,
586+
code: "unauthorized",
587+
param: "access_token",
588+
type: "authorization",
589+
},
590+
{ status: 403 }
591+
),
577592
corsStrategy !== "none"
578593
);
579594
}

0 commit comments

Comments
 (0)