Skip to content

Commit

Permalink
feat: withAuth now accepts session token for authorization hearder
Browse files Browse the repository at this point in the history
  • Loading branch information
alexandre-sarfati committed Mar 24, 2024
1 parent a1a7f21 commit 52916a0
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 63 deletions.
177 changes: 115 additions & 62 deletions apps/web/lib/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,59 @@ import {
} from "@dub/utils";
import { Link as LinkProps } from "@prisma/client";
import { createHash } from "crypto";
import { DefaultJWT, decode } from "next-auth/jwt";
import { getServerSession } from "next-auth/next";
import { exceededLimitError } from "../api/errors";
import { PlanProps, WorkspaceProps } from "../types";
import { ratelimit } from "../upstash";
import { authOptions } from "./options";

export interface UserJWT {
email: string;
id: string;
name: string;
image?: string;
}

export interface Session {
user: {
email: string;
id: string;
name: string;
image?: string;
};
user: UserJWT;
}

export interface CustomJWT extends DefaultJWT {
user?: UserJWT;
}

export type CallbackResponse = { error: Response } | { user: UserJWT };

export type AuthorizeResponse =
| {
headers: {};
error: Response;
user: null;
}
| {
headers: {};
error: null;
user: UserJWT;
};

function decodeJWT(token: string): Promise<UserJWT> {
return new Promise(async (resolve, reject) => {
try {
const result: CustomJWT | null = await decode({
token,
secret: process.env.NEXTAUTH_SECRET as string,
});

if (!result?.user) {
return reject("decode invalid");
}

resolve(result.user);
} catch (err) {
reject(err);
}
});
}

export const getSession = async () => {
Expand Down Expand Up @@ -97,7 +137,7 @@ export const withAuth = (
const searchParams = getSearchParams(req.url);
const { linkId } = params || {};

let apiKey: string | undefined = undefined;
let token: string | undefined = undefined;

const authorizationHeader = req.headers.get("Authorization");
if (authorizationHeader) {
Expand All @@ -109,7 +149,7 @@ export const withAuth = (
},
);
}
apiKey = authorizationHeader.replace("Bearer ", "");
token = authorizationHeader.replace("Bearer ", "");
}

const domain = params?.domain || searchParams.domain;
Expand All @@ -130,7 +170,7 @@ export const withAuth = (
// if there's no workspace ID or slug
if (!idOrSlug) {
// for /api/links (POST /api/links) – allow no session (but warn if user provides apiKey)
if (allowAnonymous && !apiKey) {
if (allowAnonymous && !token) {
// @ts-expect-error
return await handler({
req,
Expand All @@ -153,65 +193,78 @@ export const withAuth = (
slug = idOrSlug;
}

if (apiKey) {
const hashedKey = hashToken(apiKey, {
noSecret: true,
});
if (token) {
try {
const user = await decodeJWT(token);
session = {
user: {
id: user.id,
name: user.name || "",
email: user.email || "",
},
};
} catch (e) {
const apiKey = token;

const user = await prisma.user.findFirst({
where: {
tokens: {
some: {
hashedKey,
const hashedKey = hashToken(apiKey, {
noSecret: true,
});

const user = await prisma.user.findFirst({
where: {
tokens: {
some: {
hashedKey,
},
},
},
},
select: {
id: true,
name: true,
email: true,
},
});
if (!user) {
throw new DubApiError({
code: "unauthorized",
message: "Unauthorized: Invalid API key.",
select: {
id: true,
name: true,
email: true,
},
});
}

const { success, limit, reset, remaining } = await ratelimit(
10,
"1 s",
).limit(apiKey);

headers = {
"Retry-After": reset.toString(),
"X-RateLimit-Limit": limit.toString(),
"X-RateLimit-Remaining": remaining.toString(),
"X-RateLimit-Reset": reset.toString(),
};
if (!user) {
throw new DubApiError({
code: "unauthorized",
message: "Unauthorized: Invalid API key.",
});
}

if (!success) {
throw new DubApiError({
code: "rate_limit_exceeded",
message: "Too many requests.",
const { success, limit, reset, remaining } = await ratelimit(
10,
"1 s",
).limit(apiKey);

headers = {
"Retry-After": reset.toString(),
"X-RateLimit-Limit": limit.toString(),
"X-RateLimit-Remaining": remaining.toString(),
"X-RateLimit-Reset": reset.toString(),
};

if (!success) {
throw new DubApiError({
code: "rate_limit_exceeded",
message: "Too many requests.",
});
}
await prisma.token.update({
where: {
hashedKey,
},
data: {
lastUsed: new Date(),
},
});
session = {
user: {
id: user.id,
name: user.name || "",
email: user.email || "",
},
};
}
await prisma.token.update({
where: {
hashedKey,
},
data: {
lastUsed: new Date(),
},
});
session = {
user: {
id: user.id,
name: user.name || "",
email: user.email || "",
},
};
} else {
session = await getSession();
if (!session?.user?.id) {
Expand Down Expand Up @@ -387,7 +440,7 @@ export const withAuth = (
const url = new URL(req.url || "", API_DOMAIN);
if (
workspace.plan === "free" &&
apiKey &&
token &&
url.pathname.includes("/analytics")
) {
throw new DubApiError({
Expand Down
1 change: 0 additions & 1 deletion packages/ui/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,5 @@ export * from "./counting-numbers";
export * from "./icon-menu";
export * from "./inline-snippet";
export * from "./link-preview";
export * from "./product-hunt";
export * from "./progress-bar";
export * from "./tab-select";

0 comments on commit 52916a0

Please sign in to comment.