feat: support oidc client_secret_path with env interpolation

This commit is contained in:
Aarnav Tale
2025-03-11 18:15:56 -04:00
parent 9c8a2c0120
commit 01f432cedc
5 changed files with 116 additions and 16 deletions
+75 -9
View File
@@ -1,10 +1,15 @@
import { readFile } from 'node:fs/promises';
import * as client from 'openid-client';
import log from '~/utils/log';
import type { AppContext } from '~server/context/app';
import log from '~server/utils/log';
type OidcConfig = NonNullable<AppContext['context']['oidc']>;
declare global {
const __PREFIX__: string;
const __oidc_context: {
valid: boolean;
secret: string;
};
}
// We try our best to infer the callback URI of our Headplane instance
@@ -35,6 +40,57 @@ export function getRedirectUri(req: Request) {
return url.href;
}
let oidcSecret: string | undefined = undefined;
export function getOidcSecret() {
return oidcSecret;
}
async function resolveClientSecret(oidc: OidcConfig) {
if (!oidc.client_secret && !oidc.client_secret_path) {
return;
}
if (oidc.client_secret_path) {
// We need to interpolate environment variables into the path
// Path formatting can be like ${ENV_NAME}/path/to/secret
let path = oidc.client_secret_path;
const matches = path.match(/\${(.*?)}/g);
if (matches) {
for (const match of matches) {
const env = match.slice(2, -1);
const value = process.env[env];
if (!value) {
log.error('CFGX', 'Environment variable %s is not set', env);
return;
}
log.debug('CFGX', 'Interpolating %s with %s', match, value);
path = path.replace(match, value);
}
}
try {
log.debug('CFGX', 'Reading client secret from %s', path);
const secret = await readFile(path, 'utf-8');
if (secret.trim().length === 0) {
log.error('CFGX', 'Empty OIDC client secret');
return;
}
oidcSecret = secret;
} catch (error) {
log.error('CFGX', 'Failed to read client secret from %s', path);
log.error('CFGX', 'Error: %s', error);
log.debug('CFGX', 'Error details: %o', error);
}
}
if (oidc.client_secret) {
oidcSecret = oidc.client_secret;
}
}
function clientAuthMethod(
method: string,
): (secret: string) => client.ClientAuth {
@@ -55,7 +111,7 @@ export async function beginAuthFlow(oidc: OidcConfig, redirect_uri: string) {
new URL(oidc.issuer),
oidc.client_id,
oidc.client_secret,
clientAuthMethod(oidc.token_endpoint_auth_method)(oidc.client_secret),
clientAuthMethod(oidc.token_endpoint_auth_method)(__oidc_context.secret),
);
const codeVerifier = client.randomPKCECodeVerifier();
@@ -97,7 +153,7 @@ export async function finishAuthFlow(oidc: OidcConfig, options: FlowOptions) {
new URL(oidc.issuer),
oidc.client_id,
oidc.client_secret,
clientAuthMethod(oidc.token_endpoint_auth_method)(oidc.client_secret),
clientAuthMethod(oidc.token_endpoint_auth_method)(__oidc_context.secret),
);
let subject: string;
@@ -203,13 +259,27 @@ export function formatError(error: unknown) {
};
}
export function oidcEnabled() {
return __oidc_valid;
}
export async function testOidc(oidc: OidcConfig) {
await resolveClientSecret(oidc);
if (!oidcSecret) {
log.debug(
'OIDC',
'Cannot validate OIDC configuration without a client secret',
);
return false;
}
log.debug('OIDC', 'Discovering OIDC configuration from %s', oidc.issuer);
const secret = await resolveClientSecret(oidc);
const config = await client.discovery(
new URL(oidc.issuer),
oidc.client_id,
oidc.client_secret,
clientAuthMethod(oidc.token_endpoint_auth_method)(oidc.client_secret),
clientAuthMethod(oidc.token_endpoint_auth_method)(oidcSecret),
);
const meta = config.serverMetadata();
@@ -240,13 +310,9 @@ export async function testOidc(oidc: OidcConfig) {
'OIDC server does not support %s',
oidc.token_endpoint_auth_method,
);
return false;
}
} else {
log.warn(
'OIDC',
'OIDC server does not advertise token_endpoint_auth_methods_supported',
);
}
log.debug('OIDC', 'OIDC configuration is valid');