Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.template
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# OpenID Connect Configuration for Onboarding API
OIDC_ISSUER=
OIDC_CLIENT_ID=
OIDC_CLIENT_ID_MCP=
OIDC_SCOPES=
OIDC_REDIRECT_URI=http://localhost:5173

Expand Down
136 changes: 21 additions & 115 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
"@fastify/cookie": "^11.0.2",
"@fastify/env": "^5.0.2",
"@fastify/http-proxy": "^11.1.2",
"@fastify/secure-session": "^8.2.0",
"@fastify/sensible": "^6.0.3",
"@fastify/session": "^11.1.0",
"@fastify/static": "^8.1.1",
"@fastify/vite": "^8.1.3",
"@hookform/resolvers": "^5.0.0",
Expand Down
3 changes: 0 additions & 3 deletions public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,6 @@
},
"learnButton": "Learn how to do this in code"
},
"App": {
"loading": "Loading..."
},
"Providers": {
"headerProviders": "Providers",
"tableHeaderVersion": "Version",
Expand Down
34 changes: 22 additions & 12 deletions server/config/env.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
import fastifyPlugin from "fastify-plugin";
import fastifyEnv from "@fastify/env";
import fastifyPlugin from 'fastify-plugin';
import fastifyEnv from '@fastify/env';

const schema = {
type: "object",
required: ["OIDC_ISSUER", "OIDC_CLIENT_ID", "OIDC_REDIRECT_URI", "OIDC_SCOPES", "POST_LOGIN_REDIRECT", "COOKIE_SECRET", "API_BACKEND_URL"],
type: 'object',
required: [
'OIDC_ISSUER',
'OIDC_CLIENT_ID',
'OIDC_CLIENT_ID_MCP',
'OIDC_REDIRECT_URI',
'OIDC_SCOPES',
'POST_LOGIN_REDIRECT',
'COOKIE_SECRET',
'API_BACKEND_URL',
],
properties: {
// Application variables (.env)
OIDC_ISSUER: { type: "string" },
OIDC_CLIENT_ID: { type: "string" },
OIDC_REDIRECT_URI: { type: "string" },
OIDC_SCOPES: { type: "string" },
POST_LOGIN_REDIRECT: { type: "string" },
COOKIE_SECRET: { type: "string" },
API_BACKEND_URL: { type: "string" },
OIDC_ISSUER: { type: 'string' },
OIDC_CLIENT_ID: { type: 'string' },
OIDC_CLIENT_ID_MCP: { type: 'string' },
OIDC_REDIRECT_URI: { type: 'string' },
OIDC_SCOPES: { type: 'string' },
POST_LOGIN_REDIRECT: { type: 'string' },
COOKIE_SECRET: { type: 'string' },
API_BACKEND_URL: { type: 'string' },

// System variables
NODE_ENV: { type: "string", enum: ["development", "production"] },
NODE_ENV: { type: 'string', enum: ['development', 'production'] },
},
};

Expand Down
4 changes: 4 additions & 0 deletions server/plugins/auth-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ async function authUtilsPlugin(fastify) {
fastify.decorate("prepareOidcLoginRedirect", (request, oidcConfig, authorizationEndpoint) => {
request.log.info("Preparing OIDC login redirect.");

const { redirectTo } = request.query;
request.session.set("postLoginRedirectRoute", redirectTo);

const { clientId, redirectUri, scopes } = oidcConfig;

const state = crypto.randomBytes(16).toString("hex");
Expand Down Expand Up @@ -143,6 +146,7 @@ async function authUtilsPlugin(fastify) {
refreshToken: tokens.refresh_token,
expiresAt: null,
userInfo: extractUserInfoFromIdToken(request, tokens.id_token),
postLoginRedirectRoute: request.session.get("postLoginRedirectRoute") || "",
};

if (tokens.expires_in && typeof tokens.expires_in === "number") {
Expand Down
42 changes: 25 additions & 17 deletions server/plugins/http-proxy.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import fp from "fastify-plugin";
import httpProxy from "@fastify/http-proxy";
import { COOKIE_NAME_ONBOARDING } from "./secure-session.js";
import { AuthenticationError } from "./auth-utils.js";

function proxyPlugin(fastify) {
Expand All @@ -13,15 +12,21 @@ function proxyPlugin(fastify) {
preHandler: async (request, reply) => {
request.log.info("Entering HTTP proxy preHandler.");

const useCrate = request.headers["x-use-crate"];

const keyAccessToken = useCrate ? "onboarding_accessToken" : "mcp_accessToken";
const keyTokenExpiresAt = useCrate ? "onboarding_tokenExpiresAt" : "mcp_tokenExpiresAt";
const keyRefreshToken = useCrate ? "onboarding_refreshToken" : "mcp_refreshToken";

// Check if there is an access token
const accessToken = request.session.get("accessToken");
const accessToken = request.session.get(keyAccessToken);
if (!accessToken) {
request.log.error("Missing access token.");
return reply.unauthorized("Missing access token.");
}

// Check if the access token is expired or about to expire
const expiresAt = request.session.get("tokenExpiresAt");
const expiresAt = request.session.get(keyTokenExpiresAt);
const now = Date.now();
const REFRESH_BUFFER_SECONDS = 20; // to allow for network latency
if (!expiresAt || now < expiresAt - REFRESH_BUFFER_SECONDS) {
Expand All @@ -32,11 +37,10 @@ function proxyPlugin(fastify) {
request.log.info({ expiresAt: new Date(expiresAt).toISOString() }, "Access token is expired or about to expire; attempting refresh.");

// Check if there is a refresh token
const refreshToken = request.session.get("refreshToken");
const refreshToken = request.session.get(keyRefreshToken);
if (!refreshToken) {
request.log.error("Missing refresh token; deleting session.");
request.session.delete();
reply.clearCookie(COOKIE_NAME_ONBOARDING, { path: "/" });
request.session.destroy();
return reply.unauthorized("Session expired without token refresh capability.");
}

Expand All @@ -50,24 +54,23 @@ function proxyPlugin(fastify) {
}, issuerConfiguration.tokenEndpoint);
if (!refreshedTokenData || !refreshedTokenData.accessToken) {
request.log.error("Token refresh failed (no access token); deleting session.");
request.session.delete();
reply.clearCookie(COOKIE_NAME_ONBOARDING, { path: "/" });
request.session.destroy();
return reply.unauthorized("Session expired and token refresh failed.");
}

request.log.info("Token refresh successful; updating the session.");

request.session.set("accessToken", refreshedTokenData.accessToken);
request.session.set(keyAccessToken, refreshedTokenData.accessToken);
if (refreshedTokenData.refreshToken) {
request.session.set("refreshToken", refreshedTokenData.refreshToken);
request.session.set(keyRefreshToken, refreshedTokenData.refreshToken);
} else {
request.session.delete("refreshToken");
request.session.delete(keyRefreshToken);
}
if (refreshedTokenData.expiresIn) {
const newExpiresAt = Date.now() + (refreshedTokenData.expiresIn * 1000);
request.session.set("tokenExpiresAt", newExpiresAt);
request.session.set(keyTokenExpiresAt, newExpiresAt);
} else {
request.session.delete("tokenExpiresAt");
request.session.delete(keyTokenExpiresAt);
}

request.log.info("Token refresh successful and session updated; continuing with the HTTP request.");
Expand All @@ -81,10 +84,15 @@ function proxyPlugin(fastify) {
}
},
replyOptions: {
rewriteRequestHeaders: (req, headers) => ({
...headers,
authorization: req.session.get("accessToken")
}),
rewriteRequestHeaders: (req, headers) => {
const useCrate = req.headers["x-use-crate"];
const accessToken = useCrate ? req.session.get("onboarding_accessToken") : `${req.session.get("onboarding_accessToken")},${req.session.get("mcp_accessToken")}`;

return {
...headers,
authorization: accessToken,
}
},
},
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import secureSession from "@fastify/secure-session";
import fastifySession from "@fastify/session";
import fp from "fastify-plugin";
import fastifyCookie from "@fastify/cookie";


export const COOKIE_NAME_ONBOARDING = "onboarding";

async function secureSessionPlugin(fastify) {
const { COOKIE_SECRET, NODE_ENV } = fastify.config;

await fastify.register(fastifyCookie);

fastify.register(secureSession, {
secret: Buffer.from(COOKIE_SECRET, "hex"),
cookieName: COOKIE_NAME_ONBOARDING,
fastify.register(fastifySession, {
secret: COOKIE_SECRET,
cookie: {
path: "/",
httpOnly: true,
Expand Down
Loading
Loading