Skip to content

Commit

Permalink
feat: use gateway for authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
olevski committed Mar 7, 2023
1 parent 0447043 commit 52d9a1f
Show file tree
Hide file tree
Showing 9 changed files with 105 additions and 80 deletions.
16 changes: 13 additions & 3 deletions server/src/authentication/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { TokenSet } from "openid-client";

import config from "../config";
import logger from "../logger";
import { Authenticator } from "./index";
import { IAuthenticator } from "../interfaces";
import { getOrCreateSessionId } from "./routes";
import { serializeCookie } from "../utils";
import { WsMessage } from "../websocket/WsMessages";
Expand Down Expand Up @@ -60,7 +60,7 @@ function addAuthInvalid(req: express.Request): void {
}


function renkuAuth(authenticator: Authenticator) {
function renkuAuth(authenticator: IAuthenticator) {
return async (req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> => {
// get or create session
const sessionId = getOrCreateSessionId(req, res);
Expand All @@ -80,6 +80,11 @@ function renkuAuth(authenticator: Authenticator) {
}
}

if (tokens && tokens.access_token == "do-not-inject") {
// Nothing should be injected or passed on, authentication done in gateway
next();
}

if (tokens)
addAuthToken(req, tokens.access_token);
else
Expand All @@ -89,7 +94,7 @@ function renkuAuth(authenticator: Authenticator) {
};
}

async function wsRenkuAuth(authenticator: Authenticator, sessionId: string):
async function wsRenkuAuth(authenticator: IAuthenticator, sessionId: string):
Promise<WsMessage | Record<string, string>> {
let tokens: TokenSet;
try {
Expand All @@ -104,6 +109,11 @@ async function wsRenkuAuth(authenticator: Authenticator, sessionId: string):
throw error;
}

if (tokens && tokens.access_token == "do-not-inject") {
// Nothing should be injected or passed on, authentication done in gateway
return {}
}

if (tokens) {
const value = config.auth.authHeaderPrefix + tokens.access_token;
return { [config.auth.authHeaderField]: value };
Expand Down
3 changes: 2 additions & 1 deletion server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ const SERVER = {
keepCookies: JSON.parse(process.env.SERVER_KEEP_COOKIES || "[]")
};

const gatewayUrl = process.env.GATEWAY_URL || urlJoin(SERVER.url ?? "", "/api");
// Http url for the k8s gateway service - not full external url of the gateway
const gatewayUrl = process.env.GATEWAY_URL || "http://renku-gateway-auth";

const DEPLOYMENT = {
gatewayUrl,
Expand Down
9 changes: 5 additions & 4 deletions server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import ws from "ws";
import config from "./config";
import logger from "./logger";
import routes from "./routes";
import { Authenticator } from "./authentication";
import { registerAuthenticationRoutes } from "./authentication/routes";
import NoOpAuthenticator from "./noop-authentication";
import { IAuthenticator } from "./interfaces";
import { RedisStorage } from "./storage/RedisStorage";
import { errorHandler } from "./utils/errorHandler";
import errorHandlerMiddleware from "./utils/middlewares/errorHandlerMiddleware";
Expand Down Expand Up @@ -62,12 +62,13 @@ initializeSentry(app);
const storage = new RedisStorage();

// configure authenticator
const authenticator = new Authenticator(storage);
const authenticator: IAuthenticator = new NoOpAuthenticator();
const authPromise = authenticator.init();
authPromise.then(() => {
logger.info("Authenticator started");

registerAuthenticationRoutes(app, authenticator);
// Not neeeded because the oauth login flow is handled by the gateway now
// registerAuthenticationRoutes(app, authenticator);
// The error handler middleware is needed here because the registration of authentication
// routes is asynchronous and the middleware has to be registered after them
app.use(errorHandlerMiddleware);
Expand Down
28 changes: 28 additions & 0 deletions server/src/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*!
* Copyright 2023 - 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 { TokenSet } from "openid-client";

interface IAuthenticator {
ready: boolean
getTokens: (sessionId: string, autoRefresh: boolean) => Promise<TokenSet>
refreshTokens: (sessionId: string, tokens?: TokenSet, removeIfFailed?: boolean) => Promise<TokenSet>
init: () => Promise<boolean>
}

export { IAuthenticator };
42 changes: 42 additions & 0 deletions server/src/noop-authentication/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*!
* Copyright 2023 - 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 { TokenSet } from "openid-client";

/**
* NoOpAuthenticator is used to skip any authetication or header injection/modification
* that would usually be done by a real implementation. Used because the authentication
* is now handled by the gateway.
*/
class NoOpAuthenticator {
ready = true;

async getTokens(sessionId: string, autoRefresh: boolean): Promise<TokenSet> {
return new TokenSet({ access_token: "do-not-inject" });
}

async refreshTokens(sessionId: string, tokens?: TokenSet, removeIfFailed?: boolean): Promise<TokenSet> {
return new TokenSet({ access_token: "do-not-inject" });
}

async init(): Promise<boolean> {
return true;
}
}

export default NoOpAuthenticator;
62 changes: 2 additions & 60 deletions server/src/routes/apis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ import { createProxyMiddleware } from "http-proxy-middleware";

import config from "../config";
import logger from "../logger";
import { Authenticator } from "../authentication";
import { IAuthenticator } from "../interfaces";
import { renkuAuth } from "../authentication/middleware";
import { getCookieValueByName, serializeCookie } from "../utils";
import { validateCSP } from "../utils/url";
import { lastProjectsMiddleware } from "../utils/middlewares/lastProjectsMiddleware";
import { lastSearchQueriesMiddleware } from "../utils/middlewares/lastSearchQueriesMiddleware";
Expand All @@ -45,72 +44,15 @@ const proxyMiddleware = createProxyMiddleware({
logger.debug(`rewriting path from "${path}" to "${rewrittenPath}" and routing to ${config.deployment.gatewayUrl}`);
return rewrittenPath;
},
onProxyReq: (clientReq) => {
// remove unnecessary cookies to avoid gateway conflicts with auth tokens
const existingCookie = clientReq.getHeader("cookie") as string;
const newCookies: Array<string> = [];
if (existingCookie) {
clientReq.removeHeader("cookie");
for (const cookieName of config.server.keepCookies) {
const cookieValue: string = getCookieValueByName(
existingCookie,
cookieName
);
if (cookieValue) {
newCookies.push(
serializeCookie(cookieName, existingCookie[cookieName])
);
}
}
}
// add anon-id to cookies when the proper header is set.
const anonId = clientReq.getHeader(config.auth.cookiesAnonymousKey);
if (anonId) {
// ? the anon-id MUST start with a letter to prevent k8s limitations
const fullAnonId = config.auth.anonPrefix + anonId;
newCookies.push(
serializeCookie(config.auth.cookiesAnonymousKey, fullAnonId)
);
}
if (newCookies.length > 0)
clientReq.setHeader("cookie", newCookies.join("; "));
},
onProxyRes: (clientRes, req: express.Request, res: express.Response) => {
// Add CORS for sentry
res.setHeader("Access-Control-Allow-Headers", "sentry-trace");

// handle auth expiration -- we change the response status to avoid browser caching
const expHeader = req.get(config.auth.invalidHeaderField);
if (expHeader != null) {
clientRes.headers[config.auth.invalidHeaderField] = expHeader;
if (expHeader === config.auth.invalidHeaderExpired) {
// We return a different response to prevent side effects from caching mechanism on 30x responses
logger.warn(`Authentication expired when trying to reach ${req.originalUrl}. Attaching auth headers.`);
res.status(500);
res.setHeader(config.auth.invalidHeaderField, expHeader);
res.json({ error: "Invalid authentication tokens" });
}
}

// Prevent gateway from setting anon-id cookies. That's not needed in the UI anymore
const setCookie = null ?? clientRes.headers["set-cookie"];
if (setCookie == null || !setCookie.length)
return;
const allowedSetCookie = [];
for (const cookie of setCookie) {
if (!cookie.startsWith(config.auth.cookiesAnonymousKey))
allowedSetCookie.push(cookie);
}
if (!allowedSetCookie.length)
clientRes.headers["set-cookie"] = null;
else
clientRes.headers["set-cookie"] = allowedSetCookie;
}
});


function registerApiRoutes(app: express.Application,
prefix: string, authenticator: Authenticator, storage: Storage): void {
prefix: string, authenticator: IAuthenticator, storage: Storage): void {

// Locally defined APIs
if (config.sentry.enabled && config.sentry.debugMode) {
Expand Down
6 changes: 3 additions & 3 deletions server/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ import express from "express";
import config from "../config";
import registerInternalRoutes from "./internal";
import registerApiRoutes from "./apis";
import { Authenticator } from "../authentication";
import { IAuthenticator } from "../interfaces";
import { Storage } from "../storage";

function register(app: express.Application, prefix: string, authenticator: Authenticator, storage: Storage): void {
registerInternalRoutes(app, authenticator);
function register(app: express.Application, prefix: string, authenticator: IAuthenticator, storage: Storage): void {
registerInternalRoutes(app, authenticator, storage);

// Testing ingress
app.get(prefix, (req, res) => {
Expand Down
9 changes: 5 additions & 4 deletions server/src/routes/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@
import express from "express";
import logger from "../logger";

import { Authenticator } from "../authentication";
import { IAuthenticator } from "../interfaces";
import { Storage } from "../storage";


let storageFailures = 0;

function registerInternalRoutes(app: express.Application, authenticator: Authenticator): void {
function registerInternalRoutes(app: express.Application, authenticator: IAuthenticator, storage: Storage): void {
// define a route handler for the default home page
app.get("/", (req, res) => {
res.send("UI server up and working -- internal route '/'");
Expand All @@ -38,7 +39,7 @@ function registerInternalRoutes(app: express.Application, authenticator: Authent
// define a route handler for the liveness probe
app.get("/liveness", async (req, res) => {
// Check storage status
const storageStatus = authenticator.storage.getStatus();
const storageStatus = storage.getStatus();
if (storageStatus !== "ready")
storageFailures++;
else if (storageFailures !== 0)
Expand All @@ -58,7 +59,7 @@ function registerInternalRoutes(app: express.Application, authenticator: Authent
// define a route handler for the startup probe
app.get("/startup", (req, res) => {
// check if storage is ready
if (!authenticator.storage.ready)
if (!storage.ready)
res.status(503).send("Storage (i.e. Redis) not ready");
// check if authenticator is ready
else if (!authenticator.ready)
Expand Down
10 changes: 5 additions & 5 deletions server/src/websocket/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import config from "../config";
import logger from "../logger";
import { checkWsClientMessage, WsMessage, WsClientMessage } from "./WsMessages";
import { Storage } from "../storage";
import { Authenticator } from "../authentication";
import { IAuthenticator } from "../interfaces";
import { wsRenkuAuth } from "../authentication/middleware";
import { getCookieValueByName } from "../utils";
import { handlerRequestServerVersion, heartbeatRequestServerVersion } from "./handlers/clientVersion";
Expand Down Expand Up @@ -107,7 +107,7 @@ const shortLoopFunctions: Array<Function> = [ // eslint-disable-line
* @param apiClient - api to fetch data
*/
async function channelLongLoop(
sessionId: string, authenticator: Authenticator, storage: Storage, apiClient: APIClient) {
sessionId: string, authenticator: IAuthenticator, storage: Storage, apiClient: APIClient) {
const infoPrefix = `${sessionId} - long loop:`;

// checking user
Expand Down Expand Up @@ -160,7 +160,7 @@ async function channelLongLoop(
* @param apiClient - api client
*/
async function channelShortLoop(
sessionId: string, authenticator: Authenticator, storage: Storage, apiClient: APIClient) {
sessionId: string, authenticator: IAuthenticator, storage: Storage, apiClient: APIClient) {
const infoPrefix = `${sessionId} - short loop:`;

// checking user
Expand Down Expand Up @@ -217,7 +217,7 @@ async function channelShortLoop(
* @param apiClient - api client
*/
function configureWebsocket(
server: ws.Server, authenticator: Authenticator, storage: Storage, apiClient: APIClient): void {
server: ws.Server, authenticator: IAuthenticator, storage: Storage, apiClient: APIClient): void {
server.on("connection", async (socket, request) => {
// ? Should we Upgrade here? And verify the Origin since same-origin policy doesn't work for WS?

Expand Down Expand Up @@ -385,7 +385,7 @@ function getWsClientMessageHandler(
* @returns error with WsMessage or headers
*/
async function getAuthHeaders(
authenticator: Authenticator, sessionId: string, infoPrefix = ""
authenticator: IAuthenticator, sessionId: string, infoPrefix = ""
): Promise<WsMessage | Record<string, string>> {
try {
const authHeaders = await wsRenkuAuth(authenticator, sessionId);
Expand Down

0 comments on commit 52d9a1f

Please sign in to comment.