Skip to content

Commit

Permalink
chore(devtools): update auth flow (refinedev#5144)
Browse files Browse the repository at this point in the history
* chore(devtools-server): handle auth response and store token

* chore(devtools-ui): switch to native flow

* chore(devtools-server): handle provider mismatches

* chore(devtools-server): convert to jwt

* chore(devtools-server): handle logout

* chore(devtools-ui): update logout flow

* chore(devtools-server): provider error check

* chore(devtools-server): update token url

* chore: add changeset
  • Loading branch information
aliemir authored Oct 23, 2023
1 parent 8d1b7c5 commit be419eb
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 36 deletions.
5 changes: 5 additions & 0 deletions .changeset/eighty-months-yawn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@refinedev/devtools-server": patch
---

Updated auth flows and auth management to cover wider use cases.
1 change: 1 addition & 0 deletions packages/devtools-server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.persist.json
1 change: 1 addition & 0 deletions packages/devtools-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"express": "^4.18.2",
"@refinedev/devtools-ui": "1.1.8",
"@refinedev/devtools-shared": "1.1.2",
"@ory/client": "^1.1.25",
"http-proxy-middleware": "^2.0.6",
"boxen": "^5.1.2",
"chalk": "^4.1.2",
Expand Down
168 changes: 143 additions & 25 deletions packages/devtools-server/src/serve-proxy.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import { type Express, RequestHandler } from "express";
import { readJSON, writeJSON } from "fs-extra";
import { FrontendApi } from "@ory/client";
import { createProxyMiddleware, Options } from "http-proxy-middleware";
import path from "path";
import { REFINE_API_URL, SERVER_PORT } from "./constants";
import { getProjectIdFromPackageJson } from "./project-id/get-project-id-from-package-json";

const onProxyRes: Options["onProxyRes"] | undefined = (proxyRes) => {
if (proxyRes.headers["set-cookie"]) {
proxyRes.headers["set-cookie"]?.forEach((cookie, i) => {
if (
proxyRes &&
proxyRes.headers &&
proxyRes.headers["set-cookie"]
) {
proxyRes.headers["set-cookie"][i] = cookie.replace(
"Secure;",
"",
);
}
});
import type { Express, RequestHandler } from "express";

let currentProjectId: string | null | false = null;
const projectIdAppender: RequestHandler = async (req, res, next) => {
if (!currentProjectId) {
currentProjectId = await getProjectIdFromPackageJson();
}

if (currentProjectId) {
req.headers["x-project-id"] = currentProjectId;
}

next();
};

const restream: Options["onProxyReq"] = function (proxyReq, req) {
Expand All @@ -31,20 +31,112 @@ const restream: Options["onProxyReq"] = function (proxyReq, req) {
}
};

let currentProjectId: string | null | false = null;
const projectIdAppender: RequestHandler = async (req, res, next) => {
if (!currentProjectId) {
currentProjectId = await getProjectIdFromPackageJson();
const tokenize = async (token: string) => {
try {
const ORY_URL = `${REFINE_API_URL}/.auth`;

const ory = new FrontendApi({
isJsonMime: () => true,
basePath: ORY_URL,
baseOptions: {
withCredentials: true,
},
});

const { data } = await ory.toSession({
xSessionToken: token,
tokenizeAs: "jwt_template_1",
});

return data?.tokenized;
} catch (err) {
//
}

if (currentProjectId) {
req.headers["x-project-id"] = currentProjectId;
return undefined;
};

const saveAuth = async (token?: string, jwt?: string) => {
try {
writeJSON(path.join(__dirname, "..", ".persist.json"), {
token: token,
jwt: jwt,
});
} catch (error) {
//
}
};

const loadAuth = async () => {
try {
const persist = await readJSON(
path.join(__dirname, "..", ".persist.json"),
);
return persist as { token?: string; jwt?: string };
} catch (error) {
//
}

next();
return {
token: undefined,
jwt: undefined,
};
};

const handleLogoutToken: (
token?: string,
) => NonNullable<Options["onProxyReq"]> = (token) => {
return function (proxyReq, req) {
if (req.url.includes("self-service/logout/api")) {
const bodyData = JSON.stringify({
session_token: token,
});
proxyReq.setHeader("Content-Length", Buffer.byteLength(bodyData));
// stream the content
proxyReq.write(bodyData);
}
};
};

const handleSignInCallbacks: (
onToken: (token?: string, jwt?: string) => void,
) => NonNullable<Options["onProxyRes"]> = (onToken) => {
return function (proxyRes, req, res) {
let body = "";
proxyRes.on("data", (chunk) => {
body += chunk;
});
proxyRes.on("end", () => {
let sessionToken: string | undefined = undefined;
try {
const parsed = JSON.parse(body);
sessionToken = parsed.session_token;
} catch (err) {
//
}
if (!sessionToken) {
if (body?.includes?.("an+account+with+the+same+identifier")) {
res.redirect(
"/after-login?error=An+account+with+the+same+identifier+exists+already",
);
return;
}
res.redirect("/after-login?error=Invalid-session-token");
return;
}

// After grabbing the session_token, convert it to JWT, then redirect to /after-login
tokenize(sessionToken).then((tokenized) => {
onToken(sessionToken, tokenized ?? "");
res.redirect(`/after-login`);
});
});
};
};

export const serveProxy = (app: Express) => {
export const serveProxy = async (app: Express) => {
let { token, jwt } = await loadAuth();

const authProxy = createProxyMiddleware({
target: REFINE_API_URL,
// secure: false,
Expand All @@ -57,7 +149,26 @@ export const serveProxy = (app: Express) => {
headers: {
"auth-base-url-rewrite": `http://localhost:${SERVER_PORT}/api/.auth`,
},
onProxyRes,
selfHandleResponse: true,
onProxyReq: (proxyReq, req, ...rest) => {
if (token) {
proxyReq.setHeader("X-Session-Token", token ?? "");

handleLogoutToken(token)(proxyReq, req, ...rest);
}
},
onProxyRes: (proxyRes, req, res) => {
if (req.url.includes("self-service/methods/oidc/callback")) {
return handleSignInCallbacks((_token, _jwt) => {
token = _token;
jwt = _jwt;
saveAuth(token, jwt);
})(proxyRes, req, res);
} else {
res.writeHead(proxyRes.statusCode || 500, proxyRes.headers);
proxyRes.pipe(res, { end: true });
}
},
});

app.use("/api/.auth", authProxy);
Expand All @@ -68,7 +179,14 @@ export const serveProxy = (app: Express) => {
changeOrigin: true,
logLevel: __DEVELOPMENT__ ? "debug" : "silent",
pathRewrite: { "^/api/.refine": "/.refine" },
onProxyReq: restream,
onProxyReq: (proxyReq, ...rest) => {
if (jwt) {
proxyReq.setHeader("Authorization", `Bearer ${jwt}`);
proxyReq.removeHeader("cookie");
}

restream(proxyReq, ...rest);
},
});

app.use("/api/.refine", projectIdAppender, refineApiProxy);
Expand Down
13 changes: 7 additions & 6 deletions packages/devtools-ui/src/pages/login.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import clsx from "clsx";
import React from "react";
import { useSearchParams } from "react-router-dom";
import { LoginFlow } from "@ory/client";
import { ory } from "src/utils/ory";
import {
DevToolsContext,
DevtoolsEvent,
receive,
} from "@refinedev/devtools-shared";
import clsx from "clsx";
import React from "react";
import { useSearchParams } from "react-router-dom";

import { FeatureSlide, FeatureSlideMobile } from "src/components/feature-slide";
import { GithubIcon } from "src/components/icons/github";
import { GoogleIcon } from "src/components/icons/google";
import { LogoIcon } from "src/components/icons/logo";
import { FeatureSlide, FeatureSlideMobile } from "src/components/feature-slide";
import { ory } from "src/utils/ory";

export const Login = () => {
return (
Expand Down Expand Up @@ -77,7 +78,7 @@ const LoginForm = (props: { className?: string }) => {
try {
const redirectUrl = `${window.location.origin}/after-login`;

const { data } = await ory.createBrowserLoginFlow({
const { data } = await ory.createNativeLoginFlow({
refresh: true,
returnTo: redirectUrl,
});
Expand Down
10 changes: 5 additions & 5 deletions packages/devtools-ui/src/utils/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ export const isAuthenticated = async () => {

export const logoutUser = async () => {
try {
const {
data: { logout_token },
} = await ory.createBrowserLogoutFlow();

await ory.updateLogoutFlow({ token: logout_token });
await ory.performNativeLogout({
performNativeLogoutBody: {
session_token: "",
},
});

return true;
} catch (_) {
Expand Down

0 comments on commit be419eb

Please sign in to comment.