From 7662d80247b0a17c3e1b76e0808c65e55aaaaa0c Mon Sep 17 00:00:00 2001 From: malo-octo Date: Mon, 8 Jan 2024 17:48:38 +0100 Subject: [PATCH] feat: add keycloak secrets --- package.json | 3 +- src/pages/api/auth/[...nextauth].ts | 150 +++++++++++++++------------- vault/VaultModule.js | 65 ++++++++++++ vault/VaultModule.ts | 48 +++++++++ vault/package.json | 19 ++++ vault/tsconfig.json | 16 +++ 6 files changed, 233 insertions(+), 68 deletions(-) create mode 100644 vault/VaultModule.js create mode 100644 vault/VaultModule.ts create mode 100644 vault/package.json create mode 100644 vault/tsconfig.json diff --git a/package.json b/package.json index 1e608d88..1b7248cf 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,8 @@ "next-auth": "^4.22.3", "react": "18.2.0", "react-dom": "18.2.0", - "tss-react": "^4.8.8" + "tss-react": "^4.8.8", + "node-vault": "^0.10.2" }, "devDependencies": { "@babel/core": "^7.22.9", diff --git a/src/pages/api/auth/[...nextauth].ts b/src/pages/api/auth/[...nextauth].ts index dafd55d2..43869551 100644 --- a/src/pages/api/auth/[...nextauth].ts +++ b/src/pages/api/auth/[...nextauth].ts @@ -3,9 +3,17 @@ import { Account } from "next-auth"; import { User } from "next-auth"; import { JWT } from "next-auth/jwt"; import KeycloakProvider from "next-auth/providers/keycloak"; +import VaultModule from "../../../../vault/VaultModule" import { refreshAccessToken } from "../../../lib/auth"; + +const vaultModule = new VaultModule("integrated"); +const keycloakClientId = vaultModule.readSecret("kv/data/integrated/keycloak_client_id") +const keycloakClientSecret = vaultModule.readSecret("kv/data/integrated/keycloak_client_secret") +const nextauthSecret = vaultModule.readSecret("kv/data/integrated/nextauth_secret") + + interface ExtendedToken extends JWT { accessToken: string; refreshToken: string; @@ -14,75 +22,83 @@ interface ExtendedToken extends JWT { user: User; } -export default NextAuth({ - debug: true, - providers: [ - KeycloakProvider({ - clientId: process.env.KEYCLOAK_CLIENT_ID ?? "", - clientSecret: process.env.KEYCLOAK_CLIENT_SECRET ?? "", - issuer: process.env.KEYCLOAK_URL ?? "", - }), - ], +export default async() => { + const clientId = await keycloakClientId; + const clientSecret = await keycloakClientSecret; + const authSecret = await nextauthSecret; - callbacks: { - //@ts-ignore - async jwt({ - token, - user, - account, - }: { - token: JWT; - user: User; - account: Account; - }) { - // Initial sign in - //console.log("jwt", { token, user, account }); - if (account && user) { - return { - accessToken: account.access_token, - refreshToken: account.refresh_token, - idToken: account.id_token, - accessTokenExpires: account.expires_at - ? account.expires_at * 1000 - : null, - user, - }; - } + return await NextAuth({ + debug: true, + providers: [ + KeycloakProvider({ + clientId: clientId ?? "", + clientSecret: clientSecret ?? "", + issuer: process.env.KEYCLOAK_URL ?? "", + }), + ], + secret: authSecret, - // Return previous token if the access token has not expired yet - if ( - token.accessTokenExpires && - Date.now() < Number(token.accessTokenExpires) - ) { - return token as ExtendedToken; - } + callbacks: { + //@ts-ignore + async jwt({ + token, + user, + account, + }: { + token: JWT; + user: User; + account: Account; + }) { + // Initial sign in + //console.log("jwt", { token, user, account }); + if (account && user) { + return { + accessToken: account.access_token, + refreshToken: account.refresh_token, + idToken: account.id_token, + accessTokenExpires: account.expires_at + ? account.expires_at * 1000 + : null, + user, + }; + } - // Access token has expired, try to update it - return refreshAccessToken(token); - }, - // @ts-ignore - async session({ - session, - token, - }: { - session: Session; - token: ExtendedToken; - }) { - if (token) { - session.user = token.user; - //@ts-ignore - session.accessToken = token.accessToken; - //@ts-ignore - session.refreshToken = token.refreshToken; - //@ts-ignore - session.idToken = token.idToken; - //@ts-ignore - session.accessTokenExpires = token.accessTokenExpires; - //@ts-ignore - session.error = token.error; - } + // Return previous token if the access token has not expired yet + if ( + token.accessTokenExpires && + Date.now() < Number(token.accessTokenExpires) + ) { + return token as ExtendedToken; + } - return session; + // Access token has expired, try to update it + return refreshAccessToken(token); + }, + // @ts-ignore + async session({ + session, + token, + }: { + session: Session; + token: ExtendedToken; + }) { + if (token) { + session.user = token.user; + //@ts-ignore + session.accessToken = token.accessToken; + //@ts-ignore + session.refreshToken = token.refreshToken; + //@ts-ignore + session.idToken = token.idToken; + //@ts-ignore + session.accessTokenExpires = token.accessTokenExpires; + //@ts-ignore + session.error = token.error; + } + + return session; + }, }, - }, -}); + }); + +} diff --git a/vault/VaultModule.js b/vault/VaultModule.js new file mode 100644 index 00000000..a238e4c6 --- /dev/null +++ b/vault/VaultModule.js @@ -0,0 +1,65 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs = __importStar(require("fs")); +const vault = require("node-vault"); +class VaultModule { + constructor(vaultRole) { + this.vaultClient = vault({ + apiVersion: 'v1', + endpoint: "http://vault.vault-dev.svc:8200", + }); + this.vaultRole = vaultRole; + this.isKubelogged = false; + } + async readSecret(path) { + const JWT_TOKEN_FILE = "/var/run/secrets/kubernetes.io/serviceaccount/token"; + const jwt = fs.readFileSync(JWT_TOKEN_FILE); + if (!this.isKubelogged) { + try { + const result = await this.vaultClient.kubernetesLogin({ + "role": this.vaultRole, + "jwt": jwt.toString() + }); + this.vaultClient.token = result.auth.client_token; + } + catch (error) { + console.error('Error authenticating to vault instance:', error.message); + throw error; + } + this.isKubelogged = true; + } + try { + const { data } = await this.vaultClient.read(path); + const obj = Object.keys(data.data); + return data.data[obj[0]]; + } + catch (error) { + console.error('Error reading secret:', error.message); + throw error; + } + } +} +exports.default = VaultModule; diff --git a/vault/VaultModule.ts b/vault/VaultModule.ts new file mode 100644 index 00000000..cca0c799 --- /dev/null +++ b/vault/VaultModule.ts @@ -0,0 +1,48 @@ +import * as fs from 'fs'; + +const vault = require("node-vault"); + + +class VaultModule { + private vaultClient: any; + private readonly vaultRole: string + private isKubelogged: boolean + + constructor(vaultRole: string) { + this.vaultClient = vault({ + apiVersion: 'v1', + endpoint: "http://vault.vault-dev.svc:8200", + }); + this.vaultRole = vaultRole + this.isKubelogged = false + } + + async readSecret(path: string): Promise { + const JWT_TOKEN_FILE="/var/run/secrets/kubernetes.io/serviceaccount/token"; + const jwt = fs.readFileSync(JWT_TOKEN_FILE); + + if (!this.isKubelogged) { + try { + const result = await this.vaultClient.kubernetesLogin({ + "role": this.vaultRole, + "jwt": jwt.toString() + }); + this.vaultClient.token = result.auth.client_token; + } catch (error) { + console.error('Error authenticating to vault instance:', error); + throw error; + } + this.isKubelogged = true + } + try { + const res = await this.vaultClient.read(path); + let obj = Object.keys(res.data.data) + return res.data.data[obj[0]] + } catch (error) { + console.error('Error reading secret:', error); + throw error; + } + } +} + +export default VaultModule; diff --git a/vault/package.json b/vault/package.json new file mode 100644 index 00000000..cc967c28 --- /dev/null +++ b/vault/package.json @@ -0,0 +1,19 @@ +{ + "name": "vault-integrated", + "version": "1.0.0", + "description": "Vault integrated for template application", + "main": "app.js", + "scripts": { + "build": "tsc", + "start": "node index.ts" + }, + "dependencies": { + "node-vault": "^0.10.2" + }, + "devDependencies": { + "typescript": "5.1.6" + }, + "engines": { + "node": "18.11.17" + } +} diff --git a/vault/tsconfig.json b/vault/tsconfig.json new file mode 100644 index 00000000..8271fbdc --- /dev/null +++ b/vault/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "outDir": ".", + "rootDir": ".", + "strict": true, + "esModuleInterop": true + }, + "include": [ + "*.ts" + ], + "exclude": [ + "node_modules" + ] +}