Skip to content

Commit

Permalink
Revert "feat: Set httpOnly flag in cookie (#2591)" (#2596)
Browse files Browse the repository at this point in the history
This reverts commit 01bcb76.
  • Loading branch information
DafyddLlyr authored Dec 21, 2023
1 parent da0c42b commit 58492de
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 84 deletions.
102 changes: 55 additions & 47 deletions api.planx.uk/modules/auth/controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { CookieOptions, RequestHandler, Response } from "express";
import { Request } from "express-jwt";

import { isLiveEnv } from "../../helpers";

export const failedLogin: RequestHandler = (_req, _res, next) =>
next({
status: 401,
Expand All @@ -15,55 +17,61 @@ export const logout: RequestHandler = (req, res) => {
};

export const handleSuccess = (req: Request, res: Response) => {
if (!req.user) {
return res.json({
message: "no user",
success: true,
});
}

// Check referrer of original request
// This means requests from Pizzas to the staging API will not get flagged as `isStagingOrProd`
const { returnTo = process.env.EDITOR_URL_EXT } = req.session!;
if (!returnTo) throw Error("Can't generate returnTo URL from session");
if (req.user) {
const { returnTo = process.env.EDITOR_URL_EXT } = req.session!;

const isStagingOrProd = returnTo.includes("editor.planx.");
const domain = (() => {
if (isLiveEnv()) {
if (returnTo?.includes("editor.planx.")) {
// user is logging in to staging from editor.planx.dev
// or production from editor.planx.uk
return `.${new URL(returnTo).host}`;
} else {
// user is logging in from either a netlify preview build,
// or from localhost, to staging (or production... temporarily)
return undefined;
}
} else {
// user is logging in from localhost, to development
return "localhost";
}
})();

isStagingOrProd
? setJWTCookie(returnTo, res, req)
: setJWTSearchParams(returnTo, res, req);
};
if (domain) {
// As domain is set, we know that we're either redirecting back to
// editor.planx.dev/login, editor.planx.uk, or localhost:PORT
// (if this code is running in development). With the respective
// domain set in the cookie.
const cookie: CookieOptions = {
domain,
maxAge: new Date(
new Date().setFullYear(new Date().getFullYear() + 1),
).getTime(),
httpOnly: false,
};

/**
* Handle auth for staging and production
*
* Use a httpOnly cookie to pass the JWT securely back to the client.
* The client will then use the JWT to make authenticated requests to the API.
*/
function setJWTCookie(returnTo: string, res: Response, req: Request) {
const cookie: CookieOptions = {
domain: `.${new URL(returnTo).host}`,
maxAge: new Date(
new Date().setFullYear(new Date().getFullYear() + 1),
).getTime(),
httpOnly: true,
secure: true,
sameSite: "none",
};
if (isLiveEnv()) {
cookie.secure = true;
cookie.sameSite = "none";
}

res.cookie("jwt", req.user!.jwt, cookie);
res.cookie("jwt", req.user.jwt, cookie);

res.redirect(returnTo);
}

/**
* Handle auth for local development and Pizzas
*
* We can't use cookies cross-domain.
* Inject the JWT into the return URL, which can then be set as a cookie by the frontend
*/
function setJWTSearchParams(returnTo: string, res: Response, req: Request) {
const url = new URL(returnTo);
url.searchParams.set("jwt", req.user!.jwt);
res.redirect(url.href);
}
res.redirect(returnTo);
} else {
// Redirect back to localhost:PORT/login (if this API is in staging or
// production), or a netlify preview build url. As the login page is on a
// different domain to whatever this API is running on, we can't set a
// cookie. To solve this issue we inject the JWT into the return url as
// a parameter that can be extracted by the frontend code instead.
const url = new URL(returnTo);
url.searchParams.set("jwt", req.user.jwt);
res.redirect(url.href);
}
} else {
res.json({
message: "no user",
success: true,
});
}
};
11 changes: 9 additions & 2 deletions api.planx.uk/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,19 @@ useSwaggerDocs(app);

app.set("trust proxy", 1);

app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", req.headers.origin);
res.header(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept",
);
next();
});

app.use(
cors({
credentials: true,
methods: "*",
origin: process.env.EDITOR_URL_EXT,
allowedHeaders: "Origin, X-Requested-With, Content-Type, Accept",
}),
);

Expand Down
7 changes: 5 additions & 2 deletions e2e/tests/ui-driven/src/create-flow/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { Browser, Page, Request } from "@playwright/test";
import { createAuthenticatedSession } from "../globalHelpers";

export const isGetUserRequest = (req: Request) =>
req.url().includes("/user/me");
export const isGetUserRequest = (req: Request) => {
const isHasuraRequest = req.url().includes("/graphql");
const isGetUserOperation = req.postData()?.toString().includes("GetUserById");
return Boolean(isHasuraRequest && isGetUserOperation);
};

export async function getAdminPage({
browser,
Expand Down
3 changes: 2 additions & 1 deletion editor.planx.uk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"@tiptap/extension-history": "^2.0.3",
"@tiptap/extension-image": "^2.0.3",
"@tiptap/extension-italic": "^2.0.3",
"@tiptap/extension-link": "^2.1.13",
"@tiptap/extension-link": "^2.0.3",
"@tiptap/extension-list-item": "^2.0.3",
"@tiptap/extension-mention": "^2.1.8",
"@tiptap/extension-ordered-list": "^2.1.8",
Expand All @@ -52,6 +52,7 @@
"graphql-tag": "^2.12.6",
"immer": "^9.0.21",
"js-cookie": "^3.0.5",
"jwt-decode": "^4.0.0",
"lodash": "^4.17.21",
"marked": "^4.3.0",
"mathjs": "^11.8.2",
Expand Down
16 changes: 12 additions & 4 deletions editor.planx.uk/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 25 additions & 12 deletions editor.planx.uk/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ApolloProvider } from "@apollo/client";
import CssBaseline from "@mui/material/CssBaseline";
import { StyledEngineProvider, ThemeProvider } from "@mui/material/styles";
import { MyMap } from "@opensystemslab/map";
import { jwtDecode } from "jwt-decode";
import { getCookie, setCookie } from "lib/cookie";
import ErrorPage from "pages/ErrorPage";
import { AnalyticsProvider } from "pages/FlowEditor/lib/analyticsProvider";
Expand All @@ -35,18 +36,30 @@ if (!window.customElements.get("my-map")) {
}

const hasJWT = (): boolean | void => {
const jwtCookie = getCookie("jwt");
if (jwtCookie) return true;

// If JWT not set via cookie, check search params
const jwtSearchParams = new URLSearchParams(window.location.search).get(
"jwt",
);
if (!jwtSearchParams) return false;

// Remove JWT from URL, and re-run this function
setCookie("jwt", jwtSearchParams);
window.location.href = "/";
let jwt = getCookie("jwt");
if (jwt) {
try {
if (
Number(
(jwtDecode(jwt) as any)["https://hasura.io/jwt/claims"][
"x-hasura-user-id"
],
) > 0
) {
return true;
}
} catch (e) {}
window.location.href = "/logout";
} else {
jwt = new URLSearchParams(window.location.search).get("jwt");
if (jwt) {
setCookie("jwt", jwt);
// set the jwt, and remove it from the url, then re-run this function
window.location.href = "/";
} else {
return false;
}
}
};

const Layout: React.FC<{
Expand Down
21 changes: 7 additions & 14 deletions editor.planx.uk/src/pages/FlowEditor/lib/store/user.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { User, UserTeams } from "@opensystemslab/planx-core/types";
import axios from "axios";
import { _client } from "client";
import { jwtDecode } from "jwt-decode";
import { Team } from "types";
import type { StateCreator } from "zustand";

Expand All @@ -10,7 +10,7 @@ export interface UserStore {
setUser: (user: User) => void;
getUser: () => User | undefined;
canUserEditTeam: (teamSlug: Team["slug"]) => boolean;
initUserStore: () => Promise<void>;
initUserStore: (jwt: string) => Promise<void>;
}

export const userStore: StateCreator<UserStore, [], [], UserStore> = (
Expand All @@ -31,22 +31,15 @@ export const userStore: StateCreator<UserStore, [], [], UserStore> = (
return user.isPlatformAdmin || user.teams.some(hasTeamEditorRole);
},

async initUserStore() {
async initUserStore(jwt: string) {
const { getUser, setUser } = get();

if (getUser()) return;

const user = await getLoggedInUser();
const id = (jwtDecode(jwt) as any)["sub"];
const user = await _client.user.getById(id);
if (!user) throw new Error(`Failed to get user with ID ${id}`);

setUser(user);
},
});

const getLoggedInUser = async () => {
const url = `${process.env.REACT_APP_API_URL}/user/me`;
try {
const response = await axios.get<User>(url, { withCredentials: true });
return response.data;
} catch (error) {
throw Error("Failed to fetch user matching JWT cookie");
}
};
4 changes: 2 additions & 2 deletions editor.planx.uk/src/routes/views/authenticated.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import AuthenticatedLayout from "../../pages/layout/AuthenticatedLayout";

/**
* View wrapper for all authenticated routes
* Initialises user store
* Parses JWT and inits user store
*/
export const authenticatedView = async () => {
const jwt = getCookie("jwt");
if (!jwt) return redirect("/login");

await useStore.getState().initUserStore();
await useStore.getState().initUserStore(jwt);

useStore.getState().setPreviewEnvironment("editor");

Expand Down

0 comments on commit 58492de

Please sign in to comment.