Der Cookie-Ablauf ist dieser:
Code: Select all
user clicks login with discord
redirected to discord's oauth page
discord sends the data to my backend api
user account is created/updated on my database
redirects back to my authCallback page
sends another request to users/sessions to store the user's data in cookies
/sessions route sets the cookies
Die einzige Route, die die Cookie-Funktion aufruft, ist Benutzer/Sitzungen
Hier ist der notwendige Code:
Ich habe 2 vercel.json, 1 für das Frontend und eine für das Backend (falls jemand es wissen muss)
vercel.json (Frontend):
Code: Select all
{
"rewrites":
[
{
"source": "/(.*)",
"destination": "/"
}
]
}
Code: Select all
{
"builds": [
{
"src": "/index.js",
"use": "@vercel/node"
}
],
"routes": [
{
"src": "/(.*)",
"dest": "/index.js",
"methods": ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]
}
]
}
Code: Select all
const allowedOrigins = [
// Front End
"http://localhost:5173",
prodDomain,
// Back End
"http://localhost:4000",
prodDomainAPI,
];
app.set("trust proxy", 1);
app.use(
cors({
credentials: true,
origin: (origin, callback) => {
if (allowedOrigins.indexOf(origin) !== -1 || !origin) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"));
}
},
})
);
app.use(cookieParser());
app.use(json());
app.use(urlencoded({ extended: true }));
Code: Select all
export const setAuthCookies = (res, userData, token) => {
const commonOptions = {
path: "/",
sameSite: "None",
secure: true,
maxAge: 30 * 24 * 60 * 60 * 1000,
};
res.cookie("authUser", JSON.stringify(userData), {
...commonOptions,
httpOnly: false,
});
res.cookie("authToken", token, {
...commonOptions,
httpOnly: true,
});
return res;
};
export const clearAuthCookies = (res) => {
const clearOptions = {
path: "/",
sameSite: "None",
secure: true,
httpOnly: true,
};
res.clearCookie("authToken", clearOptions);
return res;
};
Code: Select all
users.post("/sessions", requireAuth(), async (req, res) => {
const decodedUserData = req.user.decodedUser;
try {
const existingUser = await getUserByID(decodedUserData.id);
if (!existingUser) {
return res.status(404).send("User not found");
}
const userRole = await getUserRole(decodedUserData.id);
const userData = {
id: existingUser.id,
discord_id: existingUser.discord_id,
username: existingUser.username,
avatar_url: existingUser.avatar_url,
banner_color: existingUser.banner_color,
banner_url: existingUser.banner_url,
role: userRole,
created_at: existingUser.created_at,
};
setAuthCookies(res, userData, req.user.token);
res.status(200).json({
success: true,
message: "Session established successfully",
});
} catch (error) {
console.error("users.POST /sessions", { error });
res.status(500).send("Internal Server Error");
}
});
AuthCallback.jsx (Frontend)
einzuschließen
Code: Select all
export const AuthCallback = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
useEffect(() => {
const run = async () => {
const reason = searchParams.get("reason");
const token = searchParams.get("token");
if (reason) {
let errorMessage = null;
switch (reason) {
case "rate_limited":
errorMessage =
"Discord is rate limiting login attempts. Please wait a few minutes and try again.";
break;
case "auth_failed":
errorMessage = "Authentication failed. Please try again.";
break;
default:
errorMessage = "An error occurred during authentication.";
}
toast.error(errorMessage, {
containerId: "notify-failure",
});
return setTimeout(() => {
navigate("/");
}, 1000);
}
try {
// No token means backend error
if (!token) {
toast.error("Authentication failed. Please try again.", {
containerId: "notify-failure",
});
return setTimeout(() => {
navigate("/");
}, 1000);
}
await axios
.post(
`${API}/users/sessions`,
{},
{
headers: {
Authorization: `Bearer ${token}`,
},
withCredentials: true,
},
)
.then((res) => {
toast.success("Successfully logged in!", {
containerId: "notify-success",
});
})
.catch((err) => {
toast.error("Failed to load user data. Please try again.", {
containerId: "notify-failure",
});
});
} catch (err) {
console.error("OAuth callback error:", err);
toast.error("Failed to load user data. Please try again.", {
containerId: "notify-failure",
});
} finally {
// Only reload if we have a token (success case)
if (token) {
console.log("token received");
setTimeout(() => window.location.reload(), 1000);
}
}
};
run();
}, [navigate, searchParams]);
return (
Authenticating...
Please wait
);
};
Code: Select all
Adding domain to the commonOptions
const commonOptions = {
path: "/",
sameSite: "None",
secure: true,
maxAge: 30 * 24 * 60 * 60 * 1000,
domain: isProd ? prodDomain : "localhost"
};
using sameSite Lax, Strict and None, as well as using lowercase for all 3 (lax, strict, none)
console logging the headers which showed:
'set-cookie': [
'authUser='
'authToken='
]
Mir ist auch aufgefallen, dass es in der Produktion kurz vor der Seitenaktualisierung in Cookies 4 Cookies gibt, jeweils 2 Sätze (authUser, authToken). Eines davon ist meine tatsächliche Domäne und das andere ist meine Domäne mit einem Punkt davor. Ich gehe davon aus, dass das etwas ist, was der Browser automatisch macht, da ich keine Domain einstelle.
Auf Localhost funktioniert das alles einwandfrei, es setzt die Cookies und ich kann mich problemlos anmelden.
In der Produktion werden zwar die Cookies gesetzt, aber sobald die Seite aktualisiert wird, werden beide Cookies automatisch gelöscht.
Ich kann die Daten von /sessions abrufen und das Frontend einfach die Cookies direkt mit js-cookies einrichten lassen, aber ich denke Das wäre weniger sicher, da httpOnly
Mobile version