Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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