diff --git a/server/lib/config.ts b/server/lib/config.ts index 9df1d7a..8bac680 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -92,7 +92,18 @@ const configSchema = z.object({ }) .optional(), trust_proxy: z.boolean().optional().default(true), - secret: z.string() + secret: z + .string() + .optional() + .transform(getEnvOrYaml("SERVER_SECRET")) + .pipe( + z + .string() + .min( + 32, + "SERVER_SECRET must be at least 32 characters long" + ) + ) }), traefik: z.object({ http_entrypoint: z.string(), diff --git a/server/lib/crypto.ts b/server/lib/crypto.ts new file mode 100644 index 0000000..db248e8 --- /dev/null +++ b/server/lib/crypto.ts @@ -0,0 +1,37 @@ +import * as crypto from "crypto"; + +const ALGORITHM = "aes-256-gcm"; + +export function encrypt(value: string, key: string): string { + const iv = crypto.randomBytes(12); + const cipher = crypto.createCipheriv(ALGORITHM, key, iv); + + const encrypted = Buffer.concat([ + cipher.update(value, "utf8"), + cipher.final() + ]); + const authTag = cipher.getAuthTag(); + + return [ + iv.toString("base64"), + encrypted.toString("base64"), + authTag.toString("base64") + ].join(":"); +} + +export function decrypt(encryptedValue: string, key: string): string { + const [ivB64, encryptedB64, authTagB64] = encryptedValue.split(":"); + + const iv = Buffer.from(ivB64, "base64"); + const encrypted = Buffer.from(encryptedB64, "base64"); + const authTag = Buffer.from(authTagB64, "base64"); + + const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); + decipher.setAuthTag(authTag); + + const decrypted = Buffer.concat([ + decipher.update(encrypted), + decipher.final() + ]); + return decrypted.toString("utf8"); +} diff --git a/server/routers/idp/createOidcIdp.ts b/server/routers/idp/createOidcIdp.ts index e26064d..b07d287 100644 --- a/server/routers/idp/createOidcIdp.ts +++ b/server/routers/idp/createOidcIdp.ts @@ -9,6 +9,8 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import { idp, idpOidcConfig, idpOrg, orgs } from "@server/db/schemas"; import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; +import { encrypt } from "@server/lib/crypto"; +import config from "@server/lib/config"; const paramsSchema = z.object({}).strict(); @@ -22,7 +24,8 @@ const bodySchema = z identifierPath: z.string().nonempty(), emailPath: z.string().optional(), namePath: z.string().optional(), - scopes: z.array(z.string().nonempty()) + scopes: z.array(z.string().nonempty()), + autoProvision: z.boolean().optional() }) .strict(); @@ -73,9 +76,15 @@ export async function createOidcIdp( identifierPath, emailPath, namePath, - name + name, + autoProvision } = parsedBody.data; + const key = config.getRawConfig().server.secret; + + const encryptedSecret = encrypt(clientSecret, key); + const encryptedClientId = encrypt(clientId, key); + let idpId: number | undefined; await db.transaction(async (trx) => { const [idpRes] = await trx @@ -90,11 +99,11 @@ export async function createOidcIdp( await trx.insert(idpOidcConfig).values({ idpId: idpRes.idpId, - clientId, - clientSecret, + clientId: encryptedClientId, + clientSecret: encryptedSecret, authUrl, tokenUrl, - autoProvision: true, + autoProvision, scopes: JSON.stringify(scopes), identifierPath, emailPath, diff --git a/server/routers/idp/generateOidcUrl.ts b/server/routers/idp/generateOidcUrl.ts index 4cb616e..e73d6b4 100644 --- a/server/routers/idp/generateOidcUrl.ts +++ b/server/routers/idp/generateOidcUrl.ts @@ -13,6 +13,7 @@ import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; import cookie from "cookie"; import jsonwebtoken from "jsonwebtoken"; import config from "@server/lib/config"; +import { decrypt } from "@server/lib/crypto"; const paramsSchema = z .object({ @@ -77,10 +78,21 @@ export async function generateOidcUrl( const parsedScopes = JSON.parse(existingIdp.idpOidcConfig.scopes); + const key = config.getRawConfig().server.secret; + + const decryptedClientId = decrypt( + existingIdp.idpOidcConfig.clientId, + key + ); + const decryptedClientSecret = decrypt( + existingIdp.idpOidcConfig.clientSecret, + key + ); + const redirectUrl = generateOidcRedirectUrl(idpId); const client = new arctic.OAuth2Client( - existingIdp.idpOidcConfig.clientId, - existingIdp.idpOidcConfig.clientSecret, + decryptedClientId, + decryptedClientSecret, redirectUrl ); diff --git a/server/routers/idp/validateOidcCallback.ts b/server/routers/idp/validateOidcCallback.ts index 232838d..1a78fb1 100644 --- a/server/routers/idp/validateOidcCallback.ts +++ b/server/routers/idp/validateOidcCallback.ts @@ -28,6 +28,7 @@ import { generateSessionToken, serializeSessionCookie } from "@server/auth/sessions/app"; +import { decrypt } from "@server/lib/crypto"; const paramsSchema = z .object({ @@ -90,10 +91,21 @@ export async function validateOidcCallback( ); } + const key = config.getRawConfig().server.secret; + + const decryptedClientId = decrypt( + existingIdp.idpOidcConfig.clientId, + key + ); + const decryptedClientSecret = decrypt( + existingIdp.idpOidcConfig.clientSecret, + key + ); + const redirectUrl = generateOidcRedirectUrl(existingIdp.idp.idpId); const client = new arctic.OAuth2Client( - existingIdp.idpOidcConfig.clientId, - existingIdp.idpOidcConfig.clientSecret, + decryptedClientId, + decryptedClientSecret, redirectUrl ); diff --git a/src/app/auth/idp/[idpId]/oidc/callback/ValidateOidcToken.tsx b/src/app/auth/idp/[idpId]/oidc/callback/ValidateOidcToken.tsx index 6e0a1e4..1d6ec80 100644 --- a/src/app/auth/idp/[idpId]/oidc/callback/ValidateOidcToken.tsx +++ b/src/app/auth/idp/[idpId]/oidc/callback/ValidateOidcToken.tsx @@ -6,6 +6,15 @@ import { ValidateOidcUrlCallbackResponse } from "@server/routers/idp"; import { AxiosResponse } from "axios"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; +import { + Card, + CardHeader, + CardTitle, + CardContent, + CardDescription +} from "@/components/ui/card"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Loader2, CheckCircle2, AlertCircle } from "lucide-react"; type ValidateOidcTokenParams = { orgId: string; @@ -13,6 +22,7 @@ type ValidateOidcTokenParams = { code: string | undefined; expectedState: string | undefined; stateCookie: string | undefined; + idp: {name: string}; }; export default function ValidateOidcToken(props: ValidateOidcTokenParams) { @@ -50,6 +60,9 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) { router.push("/"); } + setLoading(false); + await new Promise((resolve) => setTimeout(resolve, 100)); + if (redirectUrl.startsWith("http")) { window.location.href = res.data.data.redirectUrl; // TODO: validate this to make sure it's safe } else { @@ -67,11 +80,36 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) { }, []); return ( - <> -
Loading...
} - {!loading &&Token validated successfully!
} - {error &&Error: {error}
} - > +