Handle orders auth errors explicitly
Build and deploy Docker image / build (push) Successful in 2m31s
Build and deploy Docker image / build (push) Successful in 2m31s
This commit is contained in:
+45
-22
@@ -1,5 +1,13 @@
|
||||
import { createRemoteJWKSet, jwtVerify, type JWTPayload } from "jose";
|
||||
import { GraphQLError } from "graphql";
|
||||
import {
|
||||
createRemoteJWKSet,
|
||||
errors,
|
||||
jwtVerify,
|
||||
type JWTPayload,
|
||||
type JWTVerifyOptions,
|
||||
} from "jose";
|
||||
import mercurius, {
|
||||
type ErrorWithProps as MercuriusErrorWithProps,
|
||||
} from "mercurius";
|
||||
import type { FastifyRequest } from "fastify";
|
||||
|
||||
const LOGTO_JWKS_URL =
|
||||
@@ -9,6 +17,7 @@ const LOGTO_ORDERS_AUDIENCE =
|
||||
process.env.LOGTO_ORDERS_AUDIENCE || "https://orders.optovia.ru";
|
||||
|
||||
const jwks = createRemoteJWKSet(new URL(LOGTO_JWKS_URL));
|
||||
const { ErrorWithProps } = mercurius;
|
||||
|
||||
export interface AuthContext {
|
||||
userId?: string;
|
||||
@@ -19,19 +28,41 @@ export interface AuthContext {
|
||||
function getBearerToken(req: FastifyRequest): string {
|
||||
const auth = req.headers.authorization || "";
|
||||
if (!auth.startsWith("Bearer ")) {
|
||||
throw new GraphQLError("Missing Bearer token", {
|
||||
extensions: { code: "UNAUTHENTICATED" },
|
||||
});
|
||||
throw unauthenticated("Missing Bearer token");
|
||||
}
|
||||
const token = auth.slice(7);
|
||||
if (!token || token === "undefined") {
|
||||
throw new GraphQLError("Empty Bearer token", {
|
||||
extensions: { code: "UNAUTHENTICATED" },
|
||||
});
|
||||
throw unauthenticated("Empty Bearer token");
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
function unauthenticated(message = "Unauthorized"): MercuriusErrorWithProps {
|
||||
return new ErrorWithProps(message, { code: "UNAUTHENTICATED" }, 401);
|
||||
}
|
||||
|
||||
function forbidden(message: string): MercuriusErrorWithProps {
|
||||
return new ErrorWithProps(message, { code: "FORBIDDEN" }, 403);
|
||||
}
|
||||
|
||||
async function verifyLogtoJwt(
|
||||
token: string,
|
||||
options: Omit<JWTVerifyOptions, "issuer"> = {},
|
||||
): Promise<JWTPayload> {
|
||||
try {
|
||||
const { payload } = await jwtVerify(token, jwks, {
|
||||
issuer: LOGTO_ISSUER,
|
||||
...options,
|
||||
});
|
||||
return payload;
|
||||
} catch (error) {
|
||||
if (error instanceof errors.JOSEError) {
|
||||
throw unauthenticated();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function optionalBearerToken(req: FastifyRequest): string | null {
|
||||
const auth = req.headers.authorization || "";
|
||||
if (!auth.startsWith("Bearer ")) return null;
|
||||
@@ -71,7 +102,7 @@ export async function publicContext(): Promise<AuthContext> {
|
||||
|
||||
export async function userContext(req: FastifyRequest): Promise<AuthContext> {
|
||||
const token = getBearerToken(req);
|
||||
const { payload } = await jwtVerify(token, jwks, { issuer: LOGTO_ISSUER });
|
||||
const payload = await verifyLogtoJwt(token);
|
||||
return {
|
||||
userId: payload.sub,
|
||||
scopes: scopesFromPayload(payload),
|
||||
@@ -80,8 +111,7 @@ export async function userContext(req: FastifyRequest): Promise<AuthContext> {
|
||||
|
||||
export async function teamContext(req: FastifyRequest): Promise<AuthContext> {
|
||||
const token = getBearerToken(req);
|
||||
const { payload } = await jwtVerify(token, jwks, {
|
||||
issuer: LOGTO_ISSUER,
|
||||
const payload = await verifyLogtoJwt(token, {
|
||||
audience: LOGTO_ORDERS_AUDIENCE,
|
||||
});
|
||||
|
||||
@@ -91,9 +121,7 @@ export async function teamContext(req: FastifyRequest): Promise<AuthContext> {
|
||||
const scopes = scopesFromPayload(payload);
|
||||
|
||||
if (!teamUuid || !scopes.includes("teams:member")) {
|
||||
throw new GraphQLError("Unauthorized", {
|
||||
extensions: { code: "UNAUTHENTICATED" },
|
||||
});
|
||||
throw unauthenticated();
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -108,8 +136,7 @@ export async function managerContext(
|
||||
): Promise<AuthContext> {
|
||||
const token = optionalBearerToken(req);
|
||||
if (token === null) return { scopes: [] };
|
||||
const { payload } = await jwtVerify(token, jwks, {
|
||||
issuer: LOGTO_ISSUER,
|
||||
const payload = await verifyLogtoJwt(token, {
|
||||
audience: LOGTO_ORDERS_AUDIENCE,
|
||||
});
|
||||
const scopes = scopesFromPayload(payload);
|
||||
@@ -117,9 +144,7 @@ export async function managerContext(
|
||||
| string
|
||||
| undefined;
|
||||
if (!payload.sub || !hasManagerClaim(payload)) {
|
||||
throw new GraphQLError("Unauthorized", {
|
||||
extensions: { code: "UNAUTHENTICATED" },
|
||||
});
|
||||
throw unauthenticated();
|
||||
}
|
||||
return {
|
||||
userId: payload.sub,
|
||||
@@ -131,8 +156,6 @@ export async function managerContext(
|
||||
export function requireScopes(ctx: AuthContext, ...required: string[]): void {
|
||||
const missing = required.filter((s) => !ctx.scopes.includes(s));
|
||||
if (missing.length > 0) {
|
||||
throw new GraphQLError(`Missing required scopes: ${missing.join(", ")}`, {
|
||||
extensions: { code: "FORBIDDEN" },
|
||||
});
|
||||
throw forbidden(`Missing required scopes: ${missing.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user