Skip to content

Commit

Permalink
Merge pull request #537 from sinamics/cookie-expire-date
Browse files Browse the repository at this point in the history
Set user device cookie maxAge to 1 year
  • Loading branch information
sinamics authored Sep 1, 2024
2 parents 1755886 + 5ee5cba commit b4b2a74
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 17 deletions.
12 changes: 11 additions & 1 deletion src/components/auth/userDevices.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ const ListUserDevices: React.FC<{ devices: UserDevice[] }> = ({ devices }) => {
},
});

const invalidateUserDevice = async () => {
try {
await fetch("/api/auth/user/invalidateUserDevice", {
method: "POST",
});
} catch (error) {
console.error("Error deleting device cookie:", error);
}
};

const currentDeviceId = session?.user?.deviceId;

const isCurrentDevice = (device: UserDevice) => {
Expand Down Expand Up @@ -119,7 +129,7 @@ const ListUserDevices: React.FC<{ devices: UserDevice[] }> = ({ devices }) => {
className="btn btn-sm btn-primary"
onClick={() => {
deleteUserDevice({ deviceId: device.deviceId });
isCurrentDevice(device) && signOut();
isCurrentDevice(device) && signOut().then(() => invalidateUserDevice());
}}
>
{t("userSettings.account.userDevices.logout")}
Expand Down
13 changes: 13 additions & 0 deletions src/pages/api/auth/user/invalidateUserDevice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { NextApiRequest, NextApiResponse } from "next";
import { deleteDeviceCookie } from "~/utils/devices";

export default function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") {
// Set the cookie with an expiration date in the past
deleteDeviceCookie(res);

res.status(200).json({ message: "Device cookie deleted" });
} else {
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
2 changes: 1 addition & 1 deletion src/server/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ export const getAuthOptions = (
* @see https://next-auth.js.org/configuration/callbacks#sign-in-callback
*/
signIn: signInCallback(req, res),
jwt: jwtCallback(),
jwt: jwtCallback(res),
session: sessionCallback(req),
redirect({ url, baseUrl }) {
// Allows relative callback URLs
Expand Down
9 changes: 6 additions & 3 deletions src/server/callbacks/jwt.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { prisma } from "../db";
import { deleteDeviceCookie } from "~/utils/devices";

export function jwtCallback() {
export function jwtCallback(res) {
return async function jwt({ token, user, trigger, account, session, profile }) {
// console.log(user);

if (trigger === "update") {
if (session.update) {
const updateObject: Record<string, string | Date> = {};
Expand Down Expand Up @@ -78,8 +77,12 @@ export function jwtCallback() {
});

if (!userDevice) {
// Delete the device cookie
deleteDeviceCookie(res);

// Device doesn't exist, invalidate the deviceId in the token
token.deviceId = undefined;

return token;
}

Expand Down
20 changes: 8 additions & 12 deletions src/server/callbacks/signin.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { ErrorCode } from "~/utils/errorCode";
import { prisma } from "../db";
import { DeviceInfo, parseUA } from "~/utils/devices";
import {
createDeviceCookie,
DEVICE_SALT_COOKIE_NAME,
DeviceInfo,
parseUA,
} from "~/utils/devices";
import { isRunningInDocker } from "~/utils/docker";
import { IncomingMessage } from "http";
import { User } from "@prisma/client";
import { sendMailWithTemplate } from "~/utils/mail";
import { MailTemplateKey } from "~/utils/enums";
import { GetServerSidePropsContext } from "next";
import { parse, serialize } from "cookie";
import { parse } from "cookie";
import { randomBytes } from "crypto";

const DEVICE_SALT_COOKIE_NAME = "next-auth.did-token";

async function createUser(userData: User, isOauth = false): Promise<Partial<User>> {
const userCount = await prisma.user.count();

Expand Down Expand Up @@ -106,14 +109,7 @@ function getOrCreateDeviceSalt(

if (!deviceId) {
deviceId = randomBytes(16).toString("hex");
response.setHeader("Set-Cookie", [
serialize(DEVICE_SALT_COOKIE_NAME, deviceId, {
httpOnly: true,
secure: false,
sameSite: "lax",
path: "/",
}),
]);
createDeviceCookie(response, deviceId);
}

return deviceId;
Expand Down
53 changes: 53 additions & 0 deletions src/utils/devices.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { serialize } from "cookie";
import { GetServerSidePropsContext } from "next";
import UAParser from "ua-parser-js";
import { prisma } from "~/server/db";

export const DEVICE_SALT_COOKIE_NAME = "next-auth.did-token";
export interface ParsedUA {
deviceType: string;
browser: string;
Expand Down Expand Up @@ -51,3 +55,52 @@ export async function validateDeviceId(

return true;
}

/**
* Creates a user device cookie.
*
* @param res - The server response object.
* @param deviceId - The device ID.
*/
export const createDeviceCookie = (
res: GetServerSidePropsContext["res"],
deviceId: string,
) => {
try {
res.setHeader("Set-Cookie", [
serialize(DEVICE_SALT_COOKIE_NAME, deviceId, {
httpOnly: true,
secure: false,
sameSite: "lax",
maxAge: 1 * 365 * 24 * 60 * 60, // 1 year
expires: new Date(Date.now() + 1 * 365 * 24 * 60 * 60 * 1000), // 1 year
path: "/",
}),
]);
} catch (error) {
console.error("Error creating device cookie:", error);
}
};

/**
* Deletes or invalidates the user device cookie.
*
* @param res - The server response object.
* @param deviceId - The ID of the device to delete the cookie for.
*/
export const deleteDeviceCookie = (res: GetServerSidePropsContext["res"]) => {
try {
res.setHeader("Set-Cookie", [
serialize(DEVICE_SALT_COOKIE_NAME, "", {
httpOnly: true,
secure: false,
sameSite: "lax",
maxAge: -1,
expires: new Date(0),
path: "/",
}),
]);
} catch (error) {
console.error("Error deleting device cookie:", error);
}
};

0 comments on commit b4b2a74

Please sign in to comment.