Skip to content

Commit

Permalink
feat!: gateway refactor (#3271)
Browse files Browse the repository at this point in the history
Refactor the `ui-server` to work with the new API Gateway.

Details:
* Remove login functionality from the `ui-server`. Authentication is handled by the API Gateway.
* New `Authenticator` component which verifies bearer tokens
* The websocket handlers now use the session cookie to call the API
  • Loading branch information
leafty authored Sep 2, 2024
1 parent 6263d94 commit 364b7ad
Show file tree
Hide file tree
Showing 22 changed files with 350 additions and 1,002 deletions.
37 changes: 0 additions & 37 deletions server/package-lock.json

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

1 change: 0 additions & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
},
"dependencies": {
"@sentry/node": "^7.60.1",
"cookie-parser": "^1.4.6",
"cross-fetch": "^3.1.8",
"express": "^4.19.2",
"express-prom-bundle": "^6.6.0",
Expand Down
6 changes: 2 additions & 4 deletions server/src/api-client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@ class APIClient {
* Fetch session status
*
*/
async getSessionStatus(
authHeathers: Record<string, string>
): Promise<Response> {
async getSessionStatus(authHeathers: HeadersInit): Promise<Response> {
const sessionsUrl = `${this.gatewayUrl}/notebooks/servers`;
logger.debug(`Fetching session status.`);
const options = {
Expand All @@ -55,7 +53,7 @@ class APIClient {
*/
async kgActivationStatus(
projectId: number,
authHeaders: Headers
authHeaders: HeadersInit
): Promise<Response> {
const headers = new Headers(authHeaders);
const activationStatusURL = `${this.gatewayUrl}/projects/${projectId}/graph/status`;
Expand Down
35 changes: 35 additions & 0 deletions server/src/authentication/authentication.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*!
* Copyright 2024 - Swiss Data Science Center (SDSC)
* A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
* Eidgenössische Technische Hochschule Zürich (ETHZ).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/

import express from "express";

export type User = AnonymousUser | LoggedInUser;

export type AnonymousUser = {
id: "";
anonymousId: string;
};

export type LoggedInUser = {
id: string;
renkuAuthToken: string;
};

export type RequestWithUser = express.Request & {
user?: User | null | undefined;
};
127 changes: 127 additions & 0 deletions server/src/authentication/authenticator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*!
* Copyright 2024 - Swiss Data Science Center (SDSC)
* A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
* Eidgenössische Technische Hochschule Zürich (ETHZ).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/

import express from "express";
import { JWT } from "jose";
import { Client, Issuer } from "openid-client";

import config from "../config";
import logger from "../logger";
import { getCookieValueByName } from "../utils";

import {
AnonymousUser,
LoggedInUser,
RequestWithUser,
User,
} from "./authentication.types";

export class Authenticator {
authServerUrl: string;
issuer: Issuer<Client>;

constructor(authServerUrl: string = config.auth.serverUrl) {
this.authServerUrl = authServerUrl;
}

async init(): Promise<boolean> {
try {
this.issuer = await Issuer.discover(this.authServerUrl);
logger.info("Authenticator initialized");
} catch (error) {
logger.error(
"Cannot initialize the auth client. The authentication server may be down or some paramaters may be wrong. " +
"Please check the next log entry for further details."
);
logger.error(error);
throw error;
}
return true;
}

async authenticate({
authHeader,
sessionId = "",
}: {
authHeader: string;
sessionId?: string;
}): Promise<User> {
const anonUser: AnonymousUser = {
id: "",
anonymousId: sessionId ?? "",
};

const authToken = authHeader
.toLowerCase()
.startsWith(config.auth.authHeaderPrefix)
? authHeader.slice(config.auth.authHeaderPrefix.length).trim()
: authHeader.trim();

if (!authToken) {
return anonUser;
}

try {
const issuer = this.issuer;
if (issuer == null) {
logger.error("The authenticator is not ready.");
return anonUser;
}

const keystore = await issuer.keystore();
const { payload } = JWT.verify(authToken, keystore, { complete: true });
const userId = (payload as { sub?: string })["sub"];
if (userId) {
const user: LoggedInUser = { id: userId, renkuAuthToken: authToken };
logger.debug(`Authentication: authenticated user ${user.id}`);
return user;
}
} catch (error) {
logger.error("Authentication failed:");
logger.error(error);
}
return anonUser;
}

middleware(): (
req: RequestWithUser,
res: express.Response,
next: express.NextFunction
) => Promise<void> {
const authenticate: typeof this.authenticate = this.authenticate.bind(this);

async function authenticationMiddleware(
req: RequestWithUser,
res: express.Response,
next: express.NextFunction
) {
// Do not re-authenticate the request
if (req.user != null) {
return next();
}

const authHeader = req.header(config.auth.authHeaderField);
const sessionId =
getCookieValueByName(req.header("cookie"), config.auth.cookiesKey) ??
"";
req.user = await authenticate({ authHeader, sessionId });
return next();
}
return authenticationMiddleware;
}
}
Loading

0 comments on commit 364b7ad

Please sign in to comment.