mirror of
https://github.com/tale/headplane.git
synced 2026-08-12 14:46:52 +00:00
feat: support oidc client_secret_path with env interpolation
This commit is contained in:
+75
-9
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user