Skip to content

Use one-time use tokens when triggering or batch triggering from the frontend #1515

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/bright-lizards-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---

Add one-time use public tokens to trigger and batch trigger
7 changes: 5 additions & 2 deletions apps/webapp/app/routes/api.v1.tasks.$taskId.trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { generateJWT as internal_generateJWT, TriggerTaskRequestBody } from "@tr
import { TaskRun } from "@trigger.dev/database";
import { z } from "zod";
import { env } from "~/env.server";
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { AuthenticatedEnvironment, getOneTimeUseToken } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
import { resolveIdempotencyKeyTTL } from "~/utils/idempotencyKeys.server";
Expand Down Expand Up @@ -33,7 +33,7 @@ const { action, loader } = createActionApiRoute(
allowJWT: true,
maxContentLength: env.TASK_PAYLOAD_MAXIMUM_SIZE,
authorization: {
action: "write",
action: "trigger",
resource: (params) => ({ tasks: params.taskId }),
superScopes: ["write:tasks", "admin"],
},
Expand All @@ -59,6 +59,8 @@ const { action, loader } = createActionApiRoute(
? { traceparent, tracestate }
: undefined;

const oneTimeUseToken = await getOneTimeUseToken(authentication);

logger.debug("Triggering task", {
taskId: params.taskId,
idempotencyKey,
Expand All @@ -78,6 +80,7 @@ const { action, loader } = createActionApiRoute(
triggerVersion: triggerVersion ?? undefined,
traceContext,
spanParentAsLink: spanParentAsLink === 1,
oneTimeUseToken,
});

if (!run) {
Expand Down
7 changes: 5 additions & 2 deletions apps/webapp/app/routes/api.v1.tasks.batch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { resolveIdempotencyKeyTTL } from "~/utils/idempotencyKeys.server";
import { BatchTriggerV2Service } from "~/v3/services/batchTriggerV2.server";
import { ServiceValidationError } from "~/v3/services/baseService.server";
import { OutOfEntitlementError } from "~/v3/services/triggerTask.server";
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { AuthenticatedEnvironment, getOneTimeUseToken } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";

const { action, loader } = createActionApiRoute(
Expand All @@ -22,7 +22,7 @@ const { action, loader } = createActionApiRoute(
allowJWT: true,
maxContentLength: env.BATCH_TASK_PAYLOAD_MAXIMUM_SIZE,
authorization: {
action: "write",
action: "batchTrigger",
resource: (_, __, ___, body) => ({
tasks: Array.from(new Set(body.items.map((i) => i.task))),
}),
Expand Down Expand Up @@ -56,6 +56,8 @@ const { action, loader } = createActionApiRoute(
tracestate,
} = headers;

const oneTimeUseToken = await getOneTimeUseToken(authentication);

logger.debug("Batch trigger request", {
idempotencyKey,
idempotencyKeyTTL,
Expand Down Expand Up @@ -86,6 +88,7 @@ const { action, loader } = createActionApiRoute(
triggerVersion: triggerVersion ?? undefined,
traceContext,
spanParentAsLink: spanParentAsLink === 1,
oneTimeUseToken,
});

const $responseHeaders = await responseHeaders(
Expand Down
22 changes: 22 additions & 0 deletions apps/webapp/app/services/apiAuth.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { isPublicJWT, validatePublicJwtKey } from "./realtime/jwtAuth.server";

const ClaimsSchema = z.object({
scopes: z.array(z.string()).optional(),
// One-time use token
otu: z.boolean().optional(),
});

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

export type ApiAuthenticationResultFailure = {
Expand Down Expand Up @@ -146,6 +149,7 @@ export async function authenticateApiKey(
...result,
environment: validationResults.environment,
scopes: parsedClaims.success ? parsedClaims.data.scopes : [],
oneTimeUse: parsedClaims.success ? parsedClaims.data.otu : false,
};
}
}
Expand Down Expand Up @@ -227,6 +231,7 @@ export async function authenticateApiKeyWithFailure(
...result,
environment: validationResults.environment,
scopes: parsedClaims.success ? parsedClaims.data.scopes : [],
oneTimeUse: parsedClaims.success ? parsedClaims.data.otu : false,
};
}
}
Expand Down Expand Up @@ -531,3 +536,20 @@ function calculateJWTExpiration() {

return (Date.now() + DEFAULT_JWT_EXPIRATION_IN_MS) / 1000;
}

export async function getOneTimeUseToken(
auth: ApiAuthenticationResultSuccess
): Promise<string | undefined> {
if (auth.type !== "PUBLIC_JWT") {
return;
}

if (!auth.oneTimeUse) {
return;
}

// Hash the API key to make it unique
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(auth.apiKey));

return Buffer.from(hash).toString("hex");
}
2 changes: 1 addition & 1 deletion apps/webapp/app/services/authorization.server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type AuthorizationAction = "read" | "write"; // Add more actions as needed
export type AuthorizationAction = "read" | "write" | string; // Add more actions as needed

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

Expand Down
19 changes: 17 additions & 2 deletions apps/webapp/app/services/routeBuilders/apiBuilder.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -570,10 +570,25 @@ export function createActionApiRoute<
scopes: authenticationResult.scopes,
});

if (!checkAuthorization(authenticationResult, action, $resource, superScopes)) {
const authorizationResult = checkAuthorization(
authenticationResult,
action,
$resource,
superScopes
);

if (!authorizationResult.authorized) {
return await wrapResponse(
request,
json({ error: "Unauthorized" }, { status: 403 }),
json(
{
error: `Unauthorized: ${authorizationResult.reason}`,
code: "unauthorized",
param: "access_token",
type: "authorization",
},
{ status: 403 }
),
corsStrategy !== "none"
);
}
Expand Down
Loading