From f3444bcbce5de0a8f6c047d206335ca26ad00d16 Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Mon, 4 Dec 2023 17:15:05 -0800 Subject: [PATCH 01/16] Add CAM token SSO support --- package.json | 2 ++ src/env.ts | 8 +++++ src/main.ts | 2 ++ src/packages/auth/functions.ts | 65 ++++++++++++++++++++++++++++++++++ src/packages/auth/routes.ts | 61 ++++++++++++++++++++++++++++++- src/packages/files/files.ts | 2 ++ 6 files changed, 139 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 036266d..963ac69 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "altair-express-middleware": "^5.2.11", + "cookie-parser": "^1.4.6", "cors": "^2.8.5", "express": "^4.18.2", "express-rate-limit": "^6.7.0", @@ -32,6 +33,7 @@ "winston": "^3.9.0" }, "devDependencies": { + "@types/cookie-parser": "^1.4.6", "@types/cors": "^2.8.13", "@types/express": "^4.17.17", "@types/jsonwebtoken": "^9.0.2", diff --git a/src/env.ts b/src/env.ts index 71123e1..3e5e5a8 100644 --- a/src/env.ts +++ b/src/env.ts @@ -5,6 +5,8 @@ export type Env = { ALLOWED_ROLES_NO_AUTH: string[]; AUTH_TYPE: string; AUTH_URL: string; + AUTH_UI_URL: string; + AUTH_SSO_TOKEN_NAME: string; DEFAULT_ROLE: string; DEFAULT_ROLE_NO_AUTH: string; GQL_API_URL: string; @@ -30,6 +32,8 @@ export const defaultEnv: Env = { ALLOWED_ROLES_NO_AUTH: ['aerie_admin', 'user', 'viewer'], AUTH_TYPE: 'cam', AUTH_URL: 'https://atb-ocio-12b.jpl.nasa.gov:8443/cam-api', + AUTH_UI_URL: 'https://atb-ocio-12b.jpl.nasa.gov:8443/cam-ui/', + AUTH_SSO_TOKEN_NAME: 'iPlanetDirectoryPro', DEFAULT_ROLE: 'user', DEFAULT_ROLE_NO_AUTH: 'aerie_admin', GQL_API_URL: 'http://localhost:8080/v1/graphql', @@ -87,6 +91,8 @@ export function getEnv(): Env { const ALLOWED_ROLES_NO_AUTH = parseArray(env['ALLOWED_ROLES_NO_AUTH'], defaultEnv.ALLOWED_ROLES_NO_AUTH); const AUTH_TYPE = env['AUTH_TYPE'] ?? defaultEnv.AUTH_TYPE; const AUTH_URL = env['AUTH_URL'] ?? defaultEnv.AUTH_URL; + const AUTH_UI_URL = env['AUTH_UI_URL'] ?? defaultEnv.AUTH_UI_URL; + const AUTH_SSO_TOKEN_NAME = env['AUTH_SSO_TOKEN_NAME'] ?? defaultEnv.AUTH_SSO_TOKEN_NAME; const DEFAULT_ROLE = env['DEFAULT_ROLE'] ?? defaultEnv.DEFAULT_ROLE; const DEFAULT_ROLE_NO_AUTH = env['DEFAULT_ROLE_NO_AUTH'] ?? defaultEnv.DEFAULT_ROLE_NO_AUTH; const GQL_API_URL = env['GQL_API_URL'] ?? defaultEnv.GQL_API_URL; @@ -111,6 +117,8 @@ export function getEnv(): Env { ALLOWED_ROLES_NO_AUTH, AUTH_TYPE, AUTH_URL, + AUTH_UI_URL, + AUTH_SSO_TOKEN_NAME, DEFAULT_ROLE, DEFAULT_ROLE_NO_AUTH, GQL_API_URL, diff --git a/src/main.ts b/src/main.ts index ce4fa0e..6805b3c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,6 +9,7 @@ import { DbMerlin } from './packages/db/db.js'; import initFileRoutes from './packages/files/files.js'; import initHealthRoutes from './packages/health/health.js'; import initSwaggerRoutes from './packages/swagger/swagger.js'; +import cookieParser from 'cookie-parser'; async function main(): Promise { const logger = getLogger('main'); @@ -18,6 +19,7 @@ async function main(): Promise { app.use(helmet({ contentSecurityPolicy: false, crossOriginEmbedderPolicy: false })); app.use(cors()); app.use(express.json()); + app.use(cookieParser()); await DbMerlin.init(); diff --git a/src/packages/auth/functions.ts b/src/packages/auth/functions.ts index fe036a4..cb859e7 100644 --- a/src/packages/auth/functions.ts +++ b/src/packages/auth/functions.ts @@ -41,6 +41,7 @@ export async function getUserRoles( [username], ); + // @ts-ignore if (rowCount > 0) { const [row] = rows; const { hasura_allowed_roles, hasura_default_role } = row; @@ -112,6 +113,70 @@ export function generateJwt(username: string, defaultRole: string, allowedRoles: } } +export async function validateSSOToken(ssoToken: string): Promise { + const { AUTH_URL, AUTH_UI_URL } = getEnv(); + + const body = JSON.stringify({ ssoToken }); + const url = `${AUTH_URL}/ssoToken?action=validate`; + const response = await fetch(url, { body, method: 'POST' }); + const json = await response.json(); + + // @ts-ignore + const { validated = false, errorCode = false } = json; + + if (errorCode) { + return { + message: AUTH_UI_URL, + success: false + }; + } + + return { + message: "", + success: validated + }; +} + +export async function loginSSO(ssoToken: string): Promise { + const { AUTH_TYPE, AUTH_URL, DEFAULT_ROLE, ALLOWED_ROLES, DEFAULT_ROLE_NO_AUTH, ALLOWED_ROLES_NO_AUTH } = getEnv(); + + try { + const body = JSON.stringify({ ssoToken }); + const url = `${AUTH_URL}/userProfile`; + const response = await fetch(url, { body, method: 'POST' }); + const json = await response.json(); + // @ts-ignore + const { userId = "", errorCode = false } = json; + + if (errorCode) { + // @ts-ignore + const { errorMessage } = json; + return { + message: errorMessage, + success: false, + token: null, + }; + } + + const { allowed_roles, default_role } = AUTH_TYPE === "none" + ? await getUserRoles(userId, DEFAULT_ROLE_NO_AUTH, ALLOWED_ROLES_NO_AUTH) + : await getUserRoles(userId, DEFAULT_ROLE, ALLOWED_ROLES); + + return { + message: userId, + success: true, + token: generateJwt(userId, default_role, allowed_roles), + }; + } catch (error) { + return { + message: 'An unexpected error occurred', + success: false, + token: null, + }; + } + +} + export async function login(username: string, password: string): Promise { const { AUTH_TYPE, AUTH_URL, ALLOWED_ROLES, ALLOWED_ROLES_NO_AUTH, DEFAULT_ROLE, DEFAULT_ROLE_NO_AUTH } = getEnv(); diff --git a/src/packages/auth/routes.ts b/src/packages/auth/routes.ts index c3a3c94..fee7896 100644 --- a/src/packages/auth/routes.ts +++ b/src/packages/auth/routes.ts @@ -1,7 +1,7 @@ import type { Express } from 'express'; import rateLimit from 'express-rate-limit'; import { getEnv } from '../../env.js'; -import { login, session } from './functions.js'; +import { login, loginSSO, session, validateSSOToken } from './functions.js'; export default (app: Express) => { const { RATE_LIMITER_LOGIN_MAX } = getEnv(); @@ -47,6 +47,65 @@ export default (app: Express) => { res.json(response); }); + /** + * @swagger + * /auth/loginSSO: + * get: + * parameters: + * - in: cookie + * name: AUTH_SSO_TOKEN_NAME + * schema: + * type: string + * description: SSO token cookie that is named according to the gateway environment variable + * produces: + * - application/json + * responses: + * 200: + * description: AuthResponse + * summary: Login to initiate a session + * tags: + * - Auth + */ + app.get('/auth/loginSSO', loginLimiter, async (req, res) => { + const { AUTH_SSO_TOKEN_NAME } = getEnv(); + const ssoToken = req.cookies[AUTH_SSO_TOKEN_NAME]; + // TODO, switch based on AUTH_TYPE to call different SSO provider adapters + const { token, success, message } = await loginSSO(ssoToken); + const resp = { + token, + success, + message + }; + res.json(resp); + }); + + /** + * @swagger + * /auth/validateSSO: + * get: + * parameters: + * - in: cookie + * name: AUTH_SSO_TOKEN_NAME + * schema: + * type: string + * description: SSO token cookie that is named according to the gateway environment variable + * produces: + * - application/json + * responses: + * 200: + * description: AuthResponse + * summary: Validates a user's SSO token against external auth providers + * tags: + * - Auth + */ + app.get('/auth/validateSSO', loginLimiter, async (req, res) => { + const { AUTH_SSO_TOKEN_NAME } = getEnv(); + const ssoToken = req.cookies[AUTH_SSO_TOKEN_NAME]; + // TODO, switch based on AUTH_TYPE to call different SSO provider adapters + const response = await validateSSOToken(ssoToken); + res.json(response); + }); + /** * @swagger * /auth/session: diff --git a/src/packages/files/files.ts b/src/packages/files/files.ts index f8e8987..ab08e83 100644 --- a/src/packages/files/files.ts +++ b/src/packages/files/files.ts @@ -76,6 +76,7 @@ export default (app: Express) => { [deleted_date, id], ); + // @ts-ignore if (rowCount > 0) { logger.info(`DELETE /file: Marked file as deleted in the database: ${id}`); } else { @@ -133,6 +134,7 @@ export default (app: Express) => { const [row] = rows; const id = row ? row.id : null; + // @ts-ignore if (rowCount > 0) { logger.info(`POST /file: Added file to the database: ${id}`); } else { From bd7770348b9b038f058116b787c96c89cd7223e0 Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Fri, 8 Dec 2023 15:32:04 -0800 Subject: [PATCH 02/16] document new auth env vars --- docs/ENVIRONMENT.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index d7877cb..fa66956 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -5,11 +5,13 @@ This document provides detailed information about environment variables for the | Name | Description | Type | Default | | --------------------------- | ---------------------------------------------------------------------------------------------------- | -------- | ---------------------------------------------- | | `ALLOWED_ROLES` | Allowed roles when authentication is enabled. | `array` | ["user", "viewer"] | -| `ALLOWED_ROLES_NO_AUTH` | Allowed roles when authentication is disabled. | `array` | ["aerie_admin", "user", "viewer"] | +| `ALLOWED_ROLES_NO_AUTH` | Allowed roles when authentication is disabled. | `array` | ["aerie_admin", "user", "viewer"] | | `AUTH_TYPE` | Mode of authentication. Set to `cam` to enable CAM authentication. | `string` | none | -| `AUTH_URL` | URL of CAM REST API. Used if the given `AUTH_TYPE` is set to `cam`. | `string` | https://atb-ocio-12b.jpl.nasa.gov:8443/cam-api | +| `AUTH_URL` | URL of Auth provider's REST API. Used if the given `AUTH_TYPE` is not set to `none`. | `string` | https://atb-ocio-12b.jpl.nasa.gov:8443/cam-api | +| `AUTH_UI_URL` | URL of Auth provider's login UI. Returned to the UI if SSO token is invalid, so user is redirected | `string` | https://atb-ocio-12b.jpl.nasa.gov:8443/cam-ui | +| `AUTH_SSO_TOKEN_NAME` | The name of the SSO token the Gateway should parse cookies for. Likely found in auth provider docs. | `string` | iPlanetDirectoryPro | | `DEFAULT_ROLE` | Default role when authentication is enabled. | `array` | user | -| `DEFAULT_ROLE_NO_AUTH` | Default role when authentication is disabled. | `array` | aerie_admin | +| `DEFAULT_ROLE_NO_AUTH` | Default role when authentication is disabled. | `array` | aerie_admin | | `GQL_API_URL` | URL of GraphQL API for the GraphQL Playground. | `string` | http://localhost:8080/v1/graphql | | `GQL_API_WS_URL` | URL of GraphQL WebSocket API for the GraphQL Playground. | `string` | ws://localhost:8080/v1/graphql | | `HASURA_GRAPHQL_JWT_SECRET` | The JWT secret. Also in Hasura. **Required** even if auth off in Hasura. | `string` | | From 0914c3f01de1257ef4bf19fc562f6e5d1dab3ee7 Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Mon, 11 Dec 2023 10:20:47 -0800 Subject: [PATCH 03/16] fix lint errors --- src/env.ts | 12 ++++++------ src/packages/auth/functions.ts | 14 +++++--------- src/packages/auth/routes.ts | 4 ++-- src/packages/auth/types.ts | 11 +++++++++++ src/packages/files/files.ts | 6 ++---- 5 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/env.ts b/src/env.ts index 3e5e5a8..beaf9f4 100644 --- a/src/env.ts +++ b/src/env.ts @@ -3,10 +3,10 @@ import type { Algorithm } from 'jsonwebtoken'; export type Env = { ALLOWED_ROLES: string[]; ALLOWED_ROLES_NO_AUTH: string[]; + AUTH_SSO_TOKEN_NAME: string; AUTH_TYPE: string; - AUTH_URL: string; AUTH_UI_URL: string; - AUTH_SSO_TOKEN_NAME: string; + AUTH_URL: string; DEFAULT_ROLE: string; DEFAULT_ROLE_NO_AUTH: string; GQL_API_URL: string; @@ -30,10 +30,10 @@ export type Env = { export const defaultEnv: Env = { ALLOWED_ROLES: ['user', 'viewer'], ALLOWED_ROLES_NO_AUTH: ['aerie_admin', 'user', 'viewer'], + AUTH_SSO_TOKEN_NAME: 'iPlanetDirectoryPro', AUTH_TYPE: 'cam', - AUTH_URL: 'https://atb-ocio-12b.jpl.nasa.gov:8443/cam-api', AUTH_UI_URL: 'https://atb-ocio-12b.jpl.nasa.gov:8443/cam-ui/', - AUTH_SSO_TOKEN_NAME: 'iPlanetDirectoryPro', + AUTH_URL: 'https://atb-ocio-12b.jpl.nasa.gov:8443/cam-api', DEFAULT_ROLE: 'user', DEFAULT_ROLE_NO_AUTH: 'aerie_admin', GQL_API_URL: 'http://localhost:8080/v1/graphql', @@ -115,10 +115,10 @@ export function getEnv(): Env { return { ALLOWED_ROLES, ALLOWED_ROLES_NO_AUTH, + AUTH_SSO_TOKEN_NAME, AUTH_TYPE, - AUTH_URL, AUTH_UI_URL, - AUTH_SSO_TOKEN_NAME, + AUTH_URL, DEFAULT_ROLE, DEFAULT_ROLE_NO_AUTH, GQL_API_URL, diff --git a/src/packages/auth/functions.ts b/src/packages/auth/functions.ts index cb859e7..083563f 100644 --- a/src/packages/auth/functions.ts +++ b/src/packages/auth/functions.ts @@ -4,7 +4,7 @@ import fetch from 'node-fetch'; import { getEnv } from '../../env.js'; import getLogger from '../../logger.js'; import { DbMerlin } from '../db/db.js'; -import type { AuthResponse, JsonWebToken, JwtDecode, JwtPayload, JwtSecret, SessionResponse } from './types.js'; +import type { AuthResponse, CAMLoginResponse, CAMValidateResponse, JsonWebToken, JwtDecode, JwtPayload, JwtSecret, SessionResponse } from './types.js'; const logger = getLogger('packages/auth/functions'); @@ -41,8 +41,7 @@ export async function getUserRoles( [username], ); - // @ts-ignore - if (rowCount > 0) { + if (rowCount && rowCount > 0) { const [row] = rows; const { hasura_allowed_roles, hasura_default_role } = row; return { allowed_roles: hasura_allowed_roles, default_role: hasura_default_role }; @@ -119,9 +118,8 @@ export async function validateSSOToken(ssoToken: string): Promise { const body = JSON.stringify({ ssoToken }); const url = `${AUTH_URL}/userProfile`; const response = await fetch(url, { body, method: 'POST' }); - const json = await response.json(); - // @ts-ignore + const json = await response.json() as CAMLoginResponse; const { userId = "", errorCode = false } = json; if (errorCode) { - // @ts-ignore const { errorMessage } = json; return { - message: errorMessage, + message: errorMessage ?? "error logging into CAM", success: false, token: null, }; diff --git a/src/packages/auth/routes.ts b/src/packages/auth/routes.ts index fee7896..bb7148e 100644 --- a/src/packages/auth/routes.ts +++ b/src/packages/auth/routes.ts @@ -72,9 +72,9 @@ export default (app: Express) => { // TODO, switch based on AUTH_TYPE to call different SSO provider adapters const { token, success, message } = await loginSSO(ssoToken); const resp = { - token, + message, success, - message + token, }; res.json(resp); }); diff --git a/src/packages/auth/types.ts b/src/packages/auth/types.ts index 47ed4db..1d77670 100644 --- a/src/packages/auth/types.ts +++ b/src/packages/auth/types.ts @@ -35,3 +35,14 @@ export type UserResponse = { export type User = { id: string; }; + +export type CAMValidateResponse = { + validated?: boolean; + errorCode?: string; +}; + +export type CAMLoginResponse = { + userId?: string; + errorCode?: string; + errorMessage?: string; +}; diff --git a/src/packages/files/files.ts b/src/packages/files/files.ts index ab08e83..85faa13 100644 --- a/src/packages/files/files.ts +++ b/src/packages/files/files.ts @@ -76,8 +76,7 @@ export default (app: Express) => { [deleted_date, id], ); - // @ts-ignore - if (rowCount > 0) { + if (rowCount && rowCount > 0) { logger.info(`DELETE /file: Marked file as deleted in the database: ${id}`); } else { logger.info(`DELETE /file: No file was marked as deleted in the database`); @@ -134,8 +133,7 @@ export default (app: Express) => { const [row] = rows; const id = row ? row.id : null; - // @ts-ignore - if (rowCount > 0) { + if (rowCount && rowCount > 0) { logger.info(`POST /file: Added file to the database: ${id}`); } else { logger.info(`POST /file: No file was added to the database`); From 68b7f75bdc1a57ed852a74031b2ea838a1e97dcb Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Mon, 11 Dec 2023 10:24:46 -0800 Subject: [PATCH 04/16] add lint to CI --- .github/workflows/lint.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..f80ec5a --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,27 @@ +name: lint + +on: + push: + branches: + - develop + pull_request: + branches: + - develop + +jobs: + list: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: '16.13.0' + - name: Install Dev Dependencies and Build + run: | + npm install + npm run build + - name: Lint + run: | + npm run lint From e04e775cd34f800ead6189cc3568395ce258dd2c Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Tue, 12 Dec 2023 15:02:06 -0800 Subject: [PATCH 05/16] Refactor auth into adapters --- src/main.ts | 18 ++- src/packages/auth/adapters/CAMAuthAdapter.ts | 114 ++++++++++++++++++ .../auth/adapters/DefaultAuthAdapter.ts | 15 +++ src/packages/auth/adapters/NoAuthAdapter.ts | 22 ++++ src/packages/auth/functions.ts | 63 +--------- src/packages/auth/routes.ts | 33 +++-- src/packages/auth/types.ts | 16 +-- 7 files changed, 192 insertions(+), 89 deletions(-) create mode 100644 src/packages/auth/adapters/CAMAuthAdapter.ts create mode 100644 src/packages/auth/adapters/DefaultAuthAdapter.ts create mode 100644 src/packages/auth/adapters/NoAuthAdapter.ts diff --git a/src/main.ts b/src/main.ts index 6805b3c..f693463 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,10 +10,14 @@ import initFileRoutes from './packages/files/files.js'; import initHealthRoutes from './packages/health/health.js'; import initSwaggerRoutes from './packages/swagger/swagger.js'; import cookieParser from 'cookie-parser'; +import { AuthAdapter } from './packages/auth/types.js'; +import { NoAuthAdapter } from "./packages/auth/adapters/NoAuthAdapter.js"; +import { CAMAuthAdapter } from "./packages/auth/adapters/CAMAuthAdapter.js"; +import { DefaultAuthAdapter } from "./packages/auth/adapters/DefaultAuthAdapter.js"; async function main(): Promise { const logger = getLogger('main'); - const { PORT } = getEnv(); + const { PORT, AUTH_TYPE } = getEnv(); const app = express(); app.use(helmet({ contentSecurityPolicy: false, crossOriginEmbedderPolicy: false })); @@ -23,8 +27,18 @@ async function main(): Promise { await DbMerlin.init(); + let authHandler: AuthAdapter = DefaultAuthAdapter; + switch (AUTH_TYPE) { + case "none": + authHandler = NoAuthAdapter; + break; + case "cam": + authHandler = CAMAuthAdapter; + break; + } + initApiPlaygroundRoutes(app); - initAuthRoutes(app); + initAuthRoutes(app, authHandler); initFileRoutes(app); initHealthRoutes(app); initSwaggerRoutes(app); diff --git a/src/packages/auth/adapters/CAMAuthAdapter.ts b/src/packages/auth/adapters/CAMAuthAdapter.ts new file mode 100644 index 0000000..b783558 --- /dev/null +++ b/src/packages/auth/adapters/CAMAuthAdapter.ts @@ -0,0 +1,114 @@ +import { getEnv } from '../../../env.js'; +import { generateJwt, getUserRoles } from "../functions.js"; +import type { AuthAdapter, AuthResponse, ValidateResponse } from "../types.js"; + +type CAMValidateResponse = { + validated?: boolean; + errorCode?: string; + errorMessage?: string; +}; + +type CAMInvalidateResponse = { + invalidated?: boolean; + errorCode?: string; + errorMessage?: string; +}; + +type CAMLoginResponse = { + userId?: string; + errorCode?: string; + errorMessage?: string; +}; + +export const CAMAuthAdapter: AuthAdapter = { + + logout: async (cookies: any): Promise => { + + const { AUTH_SSO_TOKEN_NAME, AUTH_URL } = getEnv(); + + const ssoToken = cookies[AUTH_SSO_TOKEN_NAME]; + + const body = JSON.stringify({ ssoToken }); + const url = `${AUTH_URL}/ssoToken?action=invalidate`; + const response = await fetch(url, { body, method: 'DELETE' }); + const { invalidated = false } = await response.json() as CAMInvalidateResponse; + + return invalidated; + }, + + validate: async (cookies: any): Promise => { + + const { AUTH_SSO_TOKEN_NAME, AUTH_URL, AUTH_UI_URL } = getEnv(); + + const ssoToken = cookies[AUTH_SSO_TOKEN_NAME]; + + const body = JSON.stringify({ ssoToken }); + const url = `${AUTH_URL}/ssoToken?action=validate`; + const response = await fetch(url, { body, method: 'POST' }); + const json = await response.json() as CAMValidateResponse; + + const { validated = false, errorCode = false } = json; + + if (errorCode) { + return { + message: "invalid token, redirecting to login UI", + redirectURL: AUTH_UI_URL, + success: false + }; + } + + const loginResp = await loginSSO(ssoToken); + + if (validated) { + return { + message: "valid SSO token", + redirectURL: "", + token: loginResp.token ?? undefined, + userId: loginResp.message, + success: validated + } + } + + return { + message: "invalid SSO token", + redirectURL: AUTH_UI_URL, + success: false + } + }, + +}; + +async function loginSSO(ssoToken: any): Promise { + const { AUTH_URL, DEFAULT_ROLE, ALLOWED_ROLES } = getEnv(); + + try { + const body = JSON.stringify({ ssoToken }); + const url = `${AUTH_URL}/userProfile`; + const response = await fetch(url, { body, method: 'POST' }); + const json = await response.json() as CAMLoginResponse; + const { userId = "", errorCode = false } = json; + + if (errorCode) { + const { errorMessage } = json; + return { + message: errorMessage ?? "error logging into CAM", + success: false, + token: null, + }; + } + + const { allowed_roles, default_role } = await getUserRoles(userId, DEFAULT_ROLE, ALLOWED_ROLES); + + return { + message: userId, + success: true, + token: generateJwt(userId, default_role, allowed_roles), + }; + } catch (error) { + return { + message: 'An unexpected error occurred', + success: false, + token: null, + }; + } +} diff --git a/src/packages/auth/adapters/DefaultAuthAdapter.ts b/src/packages/auth/adapters/DefaultAuthAdapter.ts new file mode 100644 index 0000000..45d3444 --- /dev/null +++ b/src/packages/auth/adapters/DefaultAuthAdapter.ts @@ -0,0 +1,15 @@ +import { getEnv } from '../../../env.js'; +import type { AuthAdapter, ValidateResponse } from "../types.js"; + +export const DefaultAuthAdapter: AuthAdapter = { + logout: async (_cookies: any): Promise => true, + validate: async (_cookies: any): Promise => { + const { AUTH_UI_URL } = getEnv(); + return { + message: "SSO token auth is disabled", + success: false, + redirectURL: AUTH_UI_URL, + } + } +} + diff --git a/src/packages/auth/adapters/NoAuthAdapter.ts b/src/packages/auth/adapters/NoAuthAdapter.ts new file mode 100644 index 0000000..a458a41 --- /dev/null +++ b/src/packages/auth/adapters/NoAuthAdapter.ts @@ -0,0 +1,22 @@ +import { getEnv } from '../../../env.js'; +import { generateJwt, getUserRoles } from "../functions.js"; +import type { AuthAdapter, ValidateResponse } from "../types.js"; + +export const NoAuthAdapter: AuthAdapter = { + logout: async (_cookies: any): Promise => true, + validate: async (_cookies: any): Promise => { + + const { DEFAULT_ROLE_NO_AUTH, ALLOWED_ROLES_NO_AUTH } = getEnv(); + + console.log("auth disabled, returning default roles") + const userId = "default_user"; + const { allowed_roles, default_role } = await getUserRoles(userId, DEFAULT_ROLE_NO_AUTH, ALLOWED_ROLES_NO_AUTH) + + return { + message: userId, + success: true, + token: generateJwt(userId, default_role, allowed_roles) ?? undefined, + }; + } +} + diff --git a/src/packages/auth/functions.ts b/src/packages/auth/functions.ts index 083563f..a64255b 100644 --- a/src/packages/auth/functions.ts +++ b/src/packages/auth/functions.ts @@ -4,7 +4,7 @@ import fetch from 'node-fetch'; import { getEnv } from '../../env.js'; import getLogger from '../../logger.js'; import { DbMerlin } from '../db/db.js'; -import type { AuthResponse, CAMLoginResponse, CAMValidateResponse, JsonWebToken, JwtDecode, JwtPayload, JwtSecret, SessionResponse } from './types.js'; +import type { AuthResponse, JsonWebToken, JwtDecode, JwtPayload, JwtSecret, SessionResponse } from './types.js'; const logger = getLogger('packages/auth/functions'); @@ -112,67 +112,6 @@ export function generateJwt(username: string, defaultRole: string, allowedRoles: } } -export async function validateSSOToken(ssoToken: string): Promise { - const { AUTH_URL, AUTH_UI_URL } = getEnv(); - - const body = JSON.stringify({ ssoToken }); - const url = `${AUTH_URL}/ssoToken?action=validate`; - const response = await fetch(url, { body, method: 'POST' }); - const json = await response.json() as CAMValidateResponse; - - const { validated = false, errorCode = false } = json; - - if (errorCode) { - return { - message: AUTH_UI_URL, - success: false - }; - } - - return { - message: "", - success: validated - }; -} - -export async function loginSSO(ssoToken: string): Promise { - const { AUTH_TYPE, AUTH_URL, DEFAULT_ROLE, ALLOWED_ROLES, DEFAULT_ROLE_NO_AUTH, ALLOWED_ROLES_NO_AUTH } = getEnv(); - - try { - const body = JSON.stringify({ ssoToken }); - const url = `${AUTH_URL}/userProfile`; - const response = await fetch(url, { body, method: 'POST' }); - const json = await response.json() as CAMLoginResponse; - const { userId = "", errorCode = false } = json; - - if (errorCode) { - const { errorMessage } = json; - return { - message: errorMessage ?? "error logging into CAM", - success: false, - token: null, - }; - } - - const { allowed_roles, default_role } = AUTH_TYPE === "none" - ? await getUserRoles(userId, DEFAULT_ROLE_NO_AUTH, ALLOWED_ROLES_NO_AUTH) - : await getUserRoles(userId, DEFAULT_ROLE, ALLOWED_ROLES); - - return { - message: userId, - success: true, - token: generateJwt(userId, default_role, allowed_roles), - }; - } catch (error) { - return { - message: 'An unexpected error occurred', - success: false, - token: null, - }; - } - -} - export async function login(username: string, password: string): Promise { const { AUTH_TYPE, AUTH_URL, ALLOWED_ROLES, ALLOWED_ROLES_NO_AUTH, DEFAULT_ROLE, DEFAULT_ROLE_NO_AUTH } = getEnv(); diff --git a/src/packages/auth/routes.ts b/src/packages/auth/routes.ts index bb7148e..2a1a67f 100644 --- a/src/packages/auth/routes.ts +++ b/src/packages/auth/routes.ts @@ -1,9 +1,10 @@ import type { Express } from 'express'; import rateLimit from 'express-rate-limit'; import { getEnv } from '../../env.js'; -import { login, loginSSO, session, validateSSOToken } from './functions.js'; +import { login, session } from './functions.js'; +import { AuthAdapter } from './types.js'; -export default (app: Express) => { +export default (app: Express, auth: AuthAdapter) => { const { RATE_LIMITER_LOGIN_MAX } = getEnv(); const loginLimiter = rateLimit({ @@ -49,7 +50,7 @@ export default (app: Express) => { /** * @swagger - * /auth/loginSSO: + * /auth/validateSSO: * get: * parameters: * - in: cookie @@ -62,26 +63,25 @@ export default (app: Express) => { * responses: * 200: * description: AuthResponse - * summary: Login to initiate a session + * summary: Validates a user's SSO token against external auth providers * tags: * - Auth */ - app.get('/auth/loginSSO', loginLimiter, async (req, res) => { - const { AUTH_SSO_TOKEN_NAME } = getEnv(); - const ssoToken = req.cookies[AUTH_SSO_TOKEN_NAME]; - // TODO, switch based on AUTH_TYPE to call different SSO provider adapters - const { token, success, message } = await loginSSO(ssoToken); + app.get('/auth/validateSSO', loginLimiter, async (req, res) => { + const { token, success, message, userId, redirectURL } = await auth.validate(req.cookies); const resp = { message, success, token, + userId, + redirectURL }; res.json(resp); }); /** * @swagger - * /auth/validateSSO: + * /auth/logoutSSO: * get: * parameters: * - in: cookie @@ -93,17 +93,14 @@ export default (app: Express) => { * - application/json * responses: * 200: - * description: AuthResponse - * summary: Validates a user's SSO token against external auth providers + * description: boolean + * summary: Invalidates a user's SSO token against external auth providers * tags: * - Auth */ - app.get('/auth/validateSSO', loginLimiter, async (req, res) => { - const { AUTH_SSO_TOKEN_NAME } = getEnv(); - const ssoToken = req.cookies[AUTH_SSO_TOKEN_NAME]; - // TODO, switch based on AUTH_TYPE to call different SSO provider adapters - const response = await validateSSOToken(ssoToken); - res.json(response); + app.get('/auth/logoutSSO', async (req, res) => { + const success = await auth.logout(req.cookies); + res.json({ success }); }); /** diff --git a/src/packages/auth/types.ts b/src/packages/auth/types.ts index 1d77670..2fdb5fb 100644 --- a/src/packages/auth/types.ts +++ b/src/packages/auth/types.ts @@ -36,13 +36,15 @@ export type User = { id: string; }; -export type CAMValidateResponse = { - validated?: boolean; - errorCode?: string; +export type ValidateResponse = { + success: boolean; + message: string; + userId?: string; + token?: string; + redirectURL?: string; }; -export type CAMLoginResponse = { - userId?: string; - errorCode?: string; - errorMessage?: string; +export interface AuthAdapter { + validate(cookies: any): Promise; + logout(cookies: any): Promise; }; From 6df55af7ada12b7a0deead7c7318cf614e3e5173 Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Tue, 12 Dec 2023 15:24:59 -0800 Subject: [PATCH 06/16] fix lint errors --- src/packages/auth/adapters/CAMAuthAdapter.ts | 4 ++-- src/packages/auth/adapters/DefaultAuthAdapter.ts | 6 +++--- src/packages/auth/adapters/NoAuthAdapter.ts | 4 ++-- src/packages/auth/routes.ts | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/packages/auth/adapters/CAMAuthAdapter.ts b/src/packages/auth/adapters/CAMAuthAdapter.ts index b783558..294f465 100644 --- a/src/packages/auth/adapters/CAMAuthAdapter.ts +++ b/src/packages/auth/adapters/CAMAuthAdapter.ts @@ -63,16 +63,16 @@ export const CAMAuthAdapter: AuthAdapter = { return { message: "valid SSO token", redirectURL: "", + success: validated, token: loginResp.token ?? undefined, userId: loginResp.message, - success: validated } } return { message: "invalid SSO token", redirectURL: AUTH_UI_URL, - success: false + success: false, } }, diff --git a/src/packages/auth/adapters/DefaultAuthAdapter.ts b/src/packages/auth/adapters/DefaultAuthAdapter.ts index 45d3444..33ceec2 100644 --- a/src/packages/auth/adapters/DefaultAuthAdapter.ts +++ b/src/packages/auth/adapters/DefaultAuthAdapter.ts @@ -2,13 +2,13 @@ import { getEnv } from '../../../env.js'; import type { AuthAdapter, ValidateResponse } from "../types.js"; export const DefaultAuthAdapter: AuthAdapter = { - logout: async (_cookies: any): Promise => true, - validate: async (_cookies: any): Promise => { + logout: async (): Promise => true, + validate: async (): Promise => { const { AUTH_UI_URL } = getEnv(); return { message: "SSO token auth is disabled", - success: false, redirectURL: AUTH_UI_URL, + success: false, } } } diff --git a/src/packages/auth/adapters/NoAuthAdapter.ts b/src/packages/auth/adapters/NoAuthAdapter.ts index a458a41..f08e52f 100644 --- a/src/packages/auth/adapters/NoAuthAdapter.ts +++ b/src/packages/auth/adapters/NoAuthAdapter.ts @@ -3,8 +3,8 @@ import { generateJwt, getUserRoles } from "../functions.js"; import type { AuthAdapter, ValidateResponse } from "../types.js"; export const NoAuthAdapter: AuthAdapter = { - logout: async (_cookies: any): Promise => true, - validate: async (_cookies: any): Promise => { + logout: async (): Promise => true, + validate: async (): Promise => { const { DEFAULT_ROLE_NO_AUTH, ALLOWED_ROLES_NO_AUTH } = getEnv(); diff --git a/src/packages/auth/routes.ts b/src/packages/auth/routes.ts index 2a1a67f..e9f13d4 100644 --- a/src/packages/auth/routes.ts +++ b/src/packages/auth/routes.ts @@ -71,10 +71,10 @@ export default (app: Express, auth: AuthAdapter) => { const { token, success, message, userId, redirectURL } = await auth.validate(req.cookies); const resp = { message, + redirectURL, success, token, userId, - redirectURL }; res.json(resp); }); From 5f46ec551a62440f61f68e4df56482954ccd671d Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Wed, 13 Dec 2023 14:35:58 -0800 Subject: [PATCH 07/16] simplify CAM validation logic --- src/packages/auth/adapters/CAMAuthAdapter.ts | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/packages/auth/adapters/CAMAuthAdapter.ts b/src/packages/auth/adapters/CAMAuthAdapter.ts index 294f465..0f38151 100644 --- a/src/packages/auth/adapters/CAMAuthAdapter.ts +++ b/src/packages/auth/adapters/CAMAuthAdapter.ts @@ -49,7 +49,7 @@ export const CAMAuthAdapter: AuthAdapter = { const { validated = false, errorCode = false } = json; - if (errorCode) { + if (errorCode || !validated) { return { message: "invalid token, redirecting to login UI", redirectURL: AUTH_UI_URL, @@ -59,20 +59,12 @@ export const CAMAuthAdapter: AuthAdapter = { const loginResp = await loginSSO(ssoToken); - if (validated) { - return { - message: "valid SSO token", - redirectURL: "", - success: validated, - token: loginResp.token ?? undefined, - userId: loginResp.message, - } - } - return { - message: "invalid SSO token", - redirectURL: AUTH_UI_URL, - success: false, + message: "valid SSO token", + redirectURL: "", + success: validated, + token: loginResp.token ?? undefined, + userId: loginResp.message, } }, From 0d4562f581160534fc599125b90e4dd4452d1aaa Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Wed, 13 Dec 2023 16:12:24 -0800 Subject: [PATCH 08/16] add support for referrer based login UI redirection --- src/packages/auth/adapters/CAMAuthAdapter.ts | 16 +++++++++++----- src/packages/auth/routes.ts | 4 ++-- src/packages/auth/types.ts | 6 ++++-- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/packages/auth/adapters/CAMAuthAdapter.ts b/src/packages/auth/adapters/CAMAuthAdapter.ts index 0f38151..13a83ed 100644 --- a/src/packages/auth/adapters/CAMAuthAdapter.ts +++ b/src/packages/auth/adapters/CAMAuthAdapter.ts @@ -2,6 +2,8 @@ import { getEnv } from '../../../env.js'; import { generateJwt, getUserRoles } from "../functions.js"; import type { AuthAdapter, AuthResponse, ValidateResponse } from "../types.js"; +import { Request } from "express"; + type CAMValidateResponse = { validated?: boolean; errorCode?: string; @@ -22,10 +24,10 @@ type CAMLoginResponse = { export const CAMAuthAdapter: AuthAdapter = { - logout: async (cookies: any): Promise => { - + logout: async (req: Request): Promise => { const { AUTH_SSO_TOKEN_NAME, AUTH_URL } = getEnv(); + const cookies = req.cookies; const ssoToken = cookies[AUTH_SSO_TOKEN_NAME]; const body = JSON.stringify({ ssoToken }); @@ -36,10 +38,10 @@ export const CAMAuthAdapter: AuthAdapter = { return invalidated; }, - validate: async (cookies: any): Promise => { - + validate: async (req: Request): Promise => { const { AUTH_SSO_TOKEN_NAME, AUTH_URL, AUTH_UI_URL } = getEnv(); + const cookies = req.cookies; const ssoToken = cookies[AUTH_SSO_TOKEN_NAME]; const body = JSON.stringify({ ssoToken }); @@ -49,10 +51,14 @@ export const CAMAuthAdapter: AuthAdapter = { const { validated = false, errorCode = false } = json; + const redirectTo = req.headers.referrer; + + const redirectURL = `${AUTH_UI_URL}/?goto=${redirectTo}`; + if (errorCode || !validated) { return { message: "invalid token, redirecting to login UI", - redirectURL: AUTH_UI_URL, + redirectURL, success: false }; } diff --git a/src/packages/auth/routes.ts b/src/packages/auth/routes.ts index e9f13d4..7d2a7fd 100644 --- a/src/packages/auth/routes.ts +++ b/src/packages/auth/routes.ts @@ -68,7 +68,7 @@ export default (app: Express, auth: AuthAdapter) => { * - Auth */ app.get('/auth/validateSSO', loginLimiter, async (req, res) => { - const { token, success, message, userId, redirectURL } = await auth.validate(req.cookies); + const { token, success, message, userId, redirectURL } = await auth.validate(req); const resp = { message, redirectURL, @@ -99,7 +99,7 @@ export default (app: Express, auth: AuthAdapter) => { * - Auth */ app.get('/auth/logoutSSO', async (req, res) => { - const success = await auth.logout(req.cookies); + const success = await auth.logout(req); res.json({ success }); }); diff --git a/src/packages/auth/types.ts b/src/packages/auth/types.ts index 2fdb5fb..22fd85e 100644 --- a/src/packages/auth/types.ts +++ b/src/packages/auth/types.ts @@ -1,3 +1,5 @@ +import { Request } from "express"; + export type JsonWebToken = string; export type JwtDecode = { @@ -45,6 +47,6 @@ export type ValidateResponse = { }; export interface AuthAdapter { - validate(cookies: any): Promise; - logout(cookies: any): Promise; + validate(req: Request): Promise; + logout(req: Request): Promise; }; From bf715c14ae084fd30c394e1673e50dbd74eb2078 Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Thu, 14 Dec 2023 14:49:46 -0800 Subject: [PATCH 09/16] rename DefaultAuthAdapter to FakeAuthAdapter, run prettier --- src/main.ts | 14 +++++---- src/packages/auth/adapters/CAMAuthAdapter.ts | 29 +++++++++---------- ...faultAuthAdapter.ts => FakeAuthAdapter.ts} | 13 ++++----- src/packages/auth/adapters/NoAuthAdapter.ts | 16 +++++----- src/packages/auth/types.ts | 4 +-- 5 files changed, 37 insertions(+), 39 deletions(-) rename src/packages/auth/adapters/{DefaultAuthAdapter.ts => FakeAuthAdapter.ts} (59%) diff --git a/src/main.ts b/src/main.ts index f693463..7751515 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,9 +11,9 @@ import initHealthRoutes from './packages/health/health.js'; import initSwaggerRoutes from './packages/swagger/swagger.js'; import cookieParser from 'cookie-parser'; import { AuthAdapter } from './packages/auth/types.js'; -import { NoAuthAdapter } from "./packages/auth/adapters/NoAuthAdapter.js"; -import { CAMAuthAdapter } from "./packages/auth/adapters/CAMAuthAdapter.js"; -import { DefaultAuthAdapter } from "./packages/auth/adapters/DefaultAuthAdapter.js"; +import { NoAuthAdapter } from './packages/auth/adapters/NoAuthAdapter.js'; +import { CAMAuthAdapter } from './packages/auth/adapters/CAMAuthAdapter.js'; +import { FakeAuthAdapter } from './packages/auth/adapters/FakeAuthAdapter.js'; async function main(): Promise { const logger = getLogger('main'); @@ -27,14 +27,16 @@ async function main(): Promise { await DbMerlin.init(); - let authHandler: AuthAdapter = DefaultAuthAdapter; + let authHandler: AuthAdapter = FakeAuthAdapter; switch (AUTH_TYPE) { - case "none": + case 'none': authHandler = NoAuthAdapter; break; - case "cam": + case 'cam': authHandler = CAMAuthAdapter; break; + default: + authHandler = FakeAuthAdapter; } initApiPlaygroundRoutes(app); diff --git a/src/packages/auth/adapters/CAMAuthAdapter.ts b/src/packages/auth/adapters/CAMAuthAdapter.ts index 13a83ed..6b929a5 100644 --- a/src/packages/auth/adapters/CAMAuthAdapter.ts +++ b/src/packages/auth/adapters/CAMAuthAdapter.ts @@ -1,8 +1,9 @@ import { getEnv } from '../../../env.js'; -import { generateJwt, getUserRoles } from "../functions.js"; -import type { AuthAdapter, AuthResponse, ValidateResponse } from "../types.js"; +import { generateJwt, getUserRoles } from '../functions.js'; +import fetch from 'node-fetch'; +import type { AuthAdapter, AuthResponse, ValidateResponse } from '../types.js'; -import { Request } from "express"; +import { Request } from 'express'; type CAMValidateResponse = { validated?: boolean; @@ -23,7 +24,6 @@ type CAMLoginResponse = { }; export const CAMAuthAdapter: AuthAdapter = { - logout: async (req: Request): Promise => { const { AUTH_SSO_TOKEN_NAME, AUTH_URL } = getEnv(); @@ -33,7 +33,7 @@ export const CAMAuthAdapter: AuthAdapter = { const body = JSON.stringify({ ssoToken }); const url = `${AUTH_URL}/ssoToken?action=invalidate`; const response = await fetch(url, { body, method: 'DELETE' }); - const { invalidated = false } = await response.json() as CAMInvalidateResponse; + const { invalidated = false } = (await response.json()) as CAMInvalidateResponse; return invalidated; }, @@ -47,7 +47,7 @@ export const CAMAuthAdapter: AuthAdapter = { const body = JSON.stringify({ ssoToken }); const url = `${AUTH_URL}/ssoToken?action=validate`; const response = await fetch(url, { body, method: 'POST' }); - const json = await response.json() as CAMValidateResponse; + const json = (await response.json()) as CAMValidateResponse; const { validated = false, errorCode = false } = json; @@ -57,23 +57,22 @@ export const CAMAuthAdapter: AuthAdapter = { if (errorCode || !validated) { return { - message: "invalid token, redirecting to login UI", + message: 'invalid token, redirecting to login UI', redirectURL, - success: false + success: false, }; } const loginResp = await loginSSO(ssoToken); return { - message: "valid SSO token", - redirectURL: "", + message: 'valid SSO token', + redirectURL: '', success: validated, token: loginResp.token ?? undefined, userId: loginResp.message, - } + }; }, - }; async function loginSSO(ssoToken: any): Promise { @@ -83,13 +82,13 @@ async function loginSSO(ssoToken: any): Promise { const body = JSON.stringify({ ssoToken }); const url = `${AUTH_URL}/userProfile`; const response = await fetch(url, { body, method: 'POST' }); - const json = await response.json() as CAMLoginResponse; - const { userId = "", errorCode = false } = json; + const json = (await response.json()) as CAMLoginResponse; + const { userId = '', errorCode = false } = json; if (errorCode) { const { errorMessage } = json; return { - message: errorMessage ?? "error logging into CAM", + message: errorMessage ?? 'error logging into CAM', success: false, token: null, }; diff --git a/src/packages/auth/adapters/DefaultAuthAdapter.ts b/src/packages/auth/adapters/FakeAuthAdapter.ts similarity index 59% rename from src/packages/auth/adapters/DefaultAuthAdapter.ts rename to src/packages/auth/adapters/FakeAuthAdapter.ts index 33ceec2..9dc56c0 100644 --- a/src/packages/auth/adapters/DefaultAuthAdapter.ts +++ b/src/packages/auth/adapters/FakeAuthAdapter.ts @@ -1,15 +1,14 @@ import { getEnv } from '../../../env.js'; -import type { AuthAdapter, ValidateResponse } from "../types.js"; +import type { AuthAdapter, ValidateResponse } from '../types.js'; -export const DefaultAuthAdapter: AuthAdapter = { +export const FakeAuthAdapter: AuthAdapter = { logout: async (): Promise => true, validate: async (): Promise => { const { AUTH_UI_URL } = getEnv(); return { - message: "SSO token auth is disabled", + message: 'SSO token auth is disabled', redirectURL: AUTH_UI_URL, success: false, - } - } -} - + }; + }, +}; diff --git a/src/packages/auth/adapters/NoAuthAdapter.ts b/src/packages/auth/adapters/NoAuthAdapter.ts index f08e52f..84c6338 100644 --- a/src/packages/auth/adapters/NoAuthAdapter.ts +++ b/src/packages/auth/adapters/NoAuthAdapter.ts @@ -1,22 +1,20 @@ import { getEnv } from '../../../env.js'; -import { generateJwt, getUserRoles } from "../functions.js"; -import type { AuthAdapter, ValidateResponse } from "../types.js"; +import { generateJwt, getUserRoles } from '../functions.js'; +import type { AuthAdapter, ValidateResponse } from '../types.js'; export const NoAuthAdapter: AuthAdapter = { logout: async (): Promise => true, validate: async (): Promise => { - const { DEFAULT_ROLE_NO_AUTH, ALLOWED_ROLES_NO_AUTH } = getEnv(); - console.log("auth disabled, returning default roles") - const userId = "default_user"; - const { allowed_roles, default_role } = await getUserRoles(userId, DEFAULT_ROLE_NO_AUTH, ALLOWED_ROLES_NO_AUTH) + console.log('auth disabled, returning default roles'); + const userId = 'default_user'; + const { allowed_roles, default_role } = await getUserRoles(userId, DEFAULT_ROLE_NO_AUTH, ALLOWED_ROLES_NO_AUTH); return { message: userId, success: true, token: generateJwt(userId, default_role, allowed_roles) ?? undefined, }; - } -} - + }, +}; diff --git a/src/packages/auth/types.ts b/src/packages/auth/types.ts index 22fd85e..ed1928d 100644 --- a/src/packages/auth/types.ts +++ b/src/packages/auth/types.ts @@ -1,4 +1,4 @@ -import { Request } from "express"; +import { Request } from 'express'; export type JsonWebToken = string; @@ -49,4 +49,4 @@ export type ValidateResponse = { export interface AuthAdapter { validate(req: Request): Promise; logout(req: Request): Promise; -}; +} From 0d3ede45c9ada8daae070ab85c4d6ea0f05db0b2 Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Wed, 27 Dec 2023 08:51:44 -0800 Subject: [PATCH 10/16] convert sso token env var to string array --- docs/ENVIRONMENT.md | 2 +- src/env.ts | 6 +++--- src/packages/auth/adapters/CAMAuthAdapter.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index fa66956..5e4676e 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -9,7 +9,7 @@ This document provides detailed information about environment variables for the | `AUTH_TYPE` | Mode of authentication. Set to `cam` to enable CAM authentication. | `string` | none | | `AUTH_URL` | URL of Auth provider's REST API. Used if the given `AUTH_TYPE` is not set to `none`. | `string` | https://atb-ocio-12b.jpl.nasa.gov:8443/cam-api | | `AUTH_UI_URL` | URL of Auth provider's login UI. Returned to the UI if SSO token is invalid, so user is redirected | `string` | https://atb-ocio-12b.jpl.nasa.gov:8443/cam-ui | -| `AUTH_SSO_TOKEN_NAME` | The name of the SSO token the Gateway should parse cookies for. Likely found in auth provider docs. | `string` | iPlanetDirectoryPro | +| `AUTH_SSO_TOKEN_NAME` | The name of the SSO token the Gateway should parse cookies for. Likely found in auth provider docs. | `array` | ["iPlanetDirectoryPro"] | | `DEFAULT_ROLE` | Default role when authentication is enabled. | `array` | user | | `DEFAULT_ROLE_NO_AUTH` | Default role when authentication is disabled. | `array` | aerie_admin | | `GQL_API_URL` | URL of GraphQL API for the GraphQL Playground. | `string` | http://localhost:8080/v1/graphql | diff --git a/src/env.ts b/src/env.ts index beaf9f4..0d58870 100644 --- a/src/env.ts +++ b/src/env.ts @@ -3,7 +3,7 @@ import type { Algorithm } from 'jsonwebtoken'; export type Env = { ALLOWED_ROLES: string[]; ALLOWED_ROLES_NO_AUTH: string[]; - AUTH_SSO_TOKEN_NAME: string; + AUTH_SSO_TOKEN_NAME: string[]; AUTH_TYPE: string; AUTH_UI_URL: string; AUTH_URL: string; @@ -30,7 +30,7 @@ export type Env = { export const defaultEnv: Env = { ALLOWED_ROLES: ['user', 'viewer'], ALLOWED_ROLES_NO_AUTH: ['aerie_admin', 'user', 'viewer'], - AUTH_SSO_TOKEN_NAME: 'iPlanetDirectoryPro', + AUTH_SSO_TOKEN_NAME: ['iPlanetDirectoryPro'], // default CAM token name AUTH_TYPE: 'cam', AUTH_UI_URL: 'https://atb-ocio-12b.jpl.nasa.gov:8443/cam-ui/', AUTH_URL: 'https://atb-ocio-12b.jpl.nasa.gov:8443/cam-api', @@ -92,7 +92,7 @@ export function getEnv(): Env { const AUTH_TYPE = env['AUTH_TYPE'] ?? defaultEnv.AUTH_TYPE; const AUTH_URL = env['AUTH_URL'] ?? defaultEnv.AUTH_URL; const AUTH_UI_URL = env['AUTH_UI_URL'] ?? defaultEnv.AUTH_UI_URL; - const AUTH_SSO_TOKEN_NAME = env['AUTH_SSO_TOKEN_NAME'] ?? defaultEnv.AUTH_SSO_TOKEN_NAME; + const AUTH_SSO_TOKEN_NAME = parseArray(env['AUTH_SSO_TOKEN_NAME'], defaultEnv.AUTH_SSO_TOKEN_NAME); const DEFAULT_ROLE = env['DEFAULT_ROLE'] ?? defaultEnv.DEFAULT_ROLE; const DEFAULT_ROLE_NO_AUTH = env['DEFAULT_ROLE_NO_AUTH'] ?? defaultEnv.DEFAULT_ROLE_NO_AUTH; const GQL_API_URL = env['GQL_API_URL'] ?? defaultEnv.GQL_API_URL; diff --git a/src/packages/auth/adapters/CAMAuthAdapter.ts b/src/packages/auth/adapters/CAMAuthAdapter.ts index 6b929a5..bf33af1 100644 --- a/src/packages/auth/adapters/CAMAuthAdapter.ts +++ b/src/packages/auth/adapters/CAMAuthAdapter.ts @@ -28,7 +28,7 @@ export const CAMAuthAdapter: AuthAdapter = { const { AUTH_SSO_TOKEN_NAME, AUTH_URL } = getEnv(); const cookies = req.cookies; - const ssoToken = cookies[AUTH_SSO_TOKEN_NAME]; + const ssoToken = cookies[AUTH_SSO_TOKEN_NAME[0]]; const body = JSON.stringify({ ssoToken }); const url = `${AUTH_URL}/ssoToken?action=invalidate`; @@ -42,7 +42,7 @@ export const CAMAuthAdapter: AuthAdapter = { const { AUTH_SSO_TOKEN_NAME, AUTH_URL, AUTH_UI_URL } = getEnv(); const cookies = req.cookies; - const ssoToken = cookies[AUTH_SSO_TOKEN_NAME]; + const ssoToken = cookies[AUTH_SSO_TOKEN_NAME[0]]; const body = JSON.stringify({ ssoToken }); const url = `${AUTH_URL}/ssoToken?action=validate`; From aaf54855dd2173a0848348d1a678f8a5e1bc5a2d Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Wed, 27 Dec 2023 11:38:41 -0800 Subject: [PATCH 11/16] remove FakeAuthAdapter --- src/main.ts | 5 ++--- src/packages/auth/adapters/FakeAuthAdapter.ts | 14 -------------- 2 files changed, 2 insertions(+), 17 deletions(-) delete mode 100644 src/packages/auth/adapters/FakeAuthAdapter.ts diff --git a/src/main.ts b/src/main.ts index 7751515..59f6dab 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,7 +13,6 @@ import cookieParser from 'cookie-parser'; import { AuthAdapter } from './packages/auth/types.js'; import { NoAuthAdapter } from './packages/auth/adapters/NoAuthAdapter.js'; import { CAMAuthAdapter } from './packages/auth/adapters/CAMAuthAdapter.js'; -import { FakeAuthAdapter } from './packages/auth/adapters/FakeAuthAdapter.js'; async function main(): Promise { const logger = getLogger('main'); @@ -27,7 +26,7 @@ async function main(): Promise { await DbMerlin.init(); - let authHandler: AuthAdapter = FakeAuthAdapter; + let authHandler: AuthAdapter; switch (AUTH_TYPE) { case 'none': authHandler = NoAuthAdapter; @@ -36,7 +35,7 @@ async function main(): Promise { authHandler = CAMAuthAdapter; break; default: - authHandler = FakeAuthAdapter; + throw new Error(`invalid auth type env var: ${AUTH_TYPE}`); } initApiPlaygroundRoutes(app); diff --git a/src/packages/auth/adapters/FakeAuthAdapter.ts b/src/packages/auth/adapters/FakeAuthAdapter.ts deleted file mode 100644 index 9dc56c0..0000000 --- a/src/packages/auth/adapters/FakeAuthAdapter.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { getEnv } from '../../../env.js'; -import type { AuthAdapter, ValidateResponse } from '../types.js'; - -export const FakeAuthAdapter: AuthAdapter = { - logout: async (): Promise => true, - validate: async (): Promise => { - const { AUTH_UI_URL } = getEnv(); - return { - message: 'SSO token auth is disabled', - redirectURL: AUTH_UI_URL, - success: false, - }; - }, -}; From f03b30100f706c36fc394aae962db5de75f8f6ee Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Wed, 27 Dec 2023 14:23:44 -0800 Subject: [PATCH 12/16] update NoAuthAdapter to use new UI flow --- src/packages/auth/adapters/NoAuthAdapter.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/packages/auth/adapters/NoAuthAdapter.ts b/src/packages/auth/adapters/NoAuthAdapter.ts index 84c6338..109ee01 100644 --- a/src/packages/auth/adapters/NoAuthAdapter.ts +++ b/src/packages/auth/adapters/NoAuthAdapter.ts @@ -1,20 +1,14 @@ import { getEnv } from '../../../env.js'; -import { generateJwt, getUserRoles } from '../functions.js'; import type { AuthAdapter, ValidateResponse } from '../types.js'; export const NoAuthAdapter: AuthAdapter = { logout: async (): Promise => true, validate: async (): Promise => { - const { DEFAULT_ROLE_NO_AUTH, ALLOWED_ROLES_NO_AUTH } = getEnv(); - - console.log('auth disabled, returning default roles'); - const userId = 'default_user'; - const { allowed_roles, default_role } = await getUserRoles(userId, DEFAULT_ROLE_NO_AUTH, ALLOWED_ROLES_NO_AUTH); - + const { AUTH_UI_URL } = getEnv(); return { - message: userId, - success: true, - token: generateJwt(userId, default_role, allowed_roles) ?? undefined, + message: 'No auth enabled', + redirectURL: AUTH_UI_URL, + success: false, }; }, }; From 0e729ab3a21f310885a1e4baea54911ff4b8641b Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Wed, 10 Jan 2024 09:34:37 -0800 Subject: [PATCH 13/16] add more secure AUTH_TYPE default case --- src/packages/auth/functions.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/packages/auth/functions.ts b/src/packages/auth/functions.ts index a64255b..ad96ba7 100644 --- a/src/packages/auth/functions.ts +++ b/src/packages/auth/functions.ts @@ -151,20 +151,30 @@ export async function login(username: string, password: string): Promise { const { AUTH_TYPE } = getEnv(); - if (AUTH_TYPE === 'cam') { + if (AUTH_TYPE === 'none') { + return { message: `Authentication is disabled`, success: true }; + } else { const { jwtErrorMessage, jwtPayload } = decodeJwt(authorizationHeader); if (jwtPayload) { @@ -172,7 +182,5 @@ export async function session(authorizationHeader: string | undefined): Promise< } else { return { message: jwtErrorMessage, success: false }; } - } else { - return { message: `Authentication is disabled`, success: true }; } } From e20aa37f004e4d53615a25b84518290c640e95ad Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Thu, 11 Jan 2024 10:14:50 -0800 Subject: [PATCH 14/16] update auth env var docs --- docs/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index 5e4676e..da7d1c9 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -9,7 +9,7 @@ This document provides detailed information about environment variables for the | `AUTH_TYPE` | Mode of authentication. Set to `cam` to enable CAM authentication. | `string` | none | | `AUTH_URL` | URL of Auth provider's REST API. Used if the given `AUTH_TYPE` is not set to `none`. | `string` | https://atb-ocio-12b.jpl.nasa.gov:8443/cam-api | | `AUTH_UI_URL` | URL of Auth provider's login UI. Returned to the UI if SSO token is invalid, so user is redirected | `string` | https://atb-ocio-12b.jpl.nasa.gov:8443/cam-ui | -| `AUTH_SSO_TOKEN_NAME` | The name of the SSO token the Gateway should parse cookies for. Likely found in auth provider docs. | `array` | ["iPlanetDirectoryPro"] | +| `AUTH_SSO_TOKEN_NAME` | The name of the SSO tokens the Gateway should parse cookies for. Likely found in auth provider docs. | `array` | ["iPlanetDirectoryPro"] | | `DEFAULT_ROLE` | Default role when authentication is enabled. | `array` | user | | `DEFAULT_ROLE_NO_AUTH` | Default role when authentication is disabled. | `array` | aerie_admin | | `GQL_API_URL` | URL of GraphQL API for the GraphQL Playground. | `string` | http://localhost:8080/v1/graphql | From a5b9788213da0a166a065a9b8e6c308f888d04dc Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Thu, 11 Jan 2024 10:18:20 -0800 Subject: [PATCH 15/16] remove auth type check in `session` handler --- src/packages/auth/functions.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/packages/auth/functions.ts b/src/packages/auth/functions.ts index ad96ba7..207bbfb 100644 --- a/src/packages/auth/functions.ts +++ b/src/packages/auth/functions.ts @@ -170,17 +170,11 @@ export async function login(username: string, password: string): Promise { - const { AUTH_TYPE } = getEnv(); + const { jwtErrorMessage, jwtPayload } = decodeJwt(authorizationHeader); - if (AUTH_TYPE === 'none') { - return { message: `Authentication is disabled`, success: true }; + if (jwtPayload) { + return { message: 'Token is valid', success: true }; } else { - const { jwtErrorMessage, jwtPayload } = decodeJwt(authorizationHeader); - - if (jwtPayload) { - return { message: 'Token is valid', success: true }; - } else { - return { message: jwtErrorMessage, success: false }; - } + return { message: jwtErrorMessage, success: false }; } } From 7b7abc9edba2c175022919ef1dcf54a1e05e0a0d Mon Sep 17 00:00:00 2001 From: "(skovati) Luke" Date: Thu, 25 Jan 2024 12:39:32 -0800 Subject: [PATCH 16/16] Throw unsupported configuration error in `NoAuthAdapter` --- src/packages/auth/adapters/NoAuthAdapter.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/packages/auth/adapters/NoAuthAdapter.ts b/src/packages/auth/adapters/NoAuthAdapter.ts index 109ee01..2d91b4a 100644 --- a/src/packages/auth/adapters/NoAuthAdapter.ts +++ b/src/packages/auth/adapters/NoAuthAdapter.ts @@ -1,14 +1,11 @@ -import { getEnv } from '../../../env.js'; import type { AuthAdapter, ValidateResponse } from '../types.js'; export const NoAuthAdapter: AuthAdapter = { logout: async (): Promise => true, validate: async (): Promise => { - const { AUTH_UI_URL } = getEnv(); - return { - message: 'No auth enabled', - redirectURL: AUTH_UI_URL, - success: false, - }; + throw new Error(` + The UI is configured to use SSO auth, but the Gateway has AUTH_TYPE=none set, which is not a supported configuration. + Disable SSO auth on the UI if JWT-only auth is desired. + `); }, };