diff --git a/app/utils/config/headplane.ts b/app/utils/config/headplane.ts index 732b630..621e15c 100644 --- a/app/utils/config/headplane.ts +++ b/app/utils/config/headplane.ts @@ -10,6 +10,7 @@ import { parse } from 'yaml' import { IntegrationFactory, loadIntegration } from '~/integration' import { HeadscaleConfig, loadConfig } from '~/utils/config/headscale' +import { testOidc } from '~/utils/oidc' import log from '~/utils/log' export interface HeadplaneContext { @@ -157,6 +158,7 @@ async function checkOidc(config?: HeadscaleConfig) { let client = process.env.OIDC_CLIENT_ID let secret = process.env.OIDC_CLIENT_SECRET let method = process.env.OIDC_CLIENT_SECRET_METHOD ?? 'client_secret_basic' + let skip = process.env.OIDC_SKIP_CONFIG_VALIDATION === 'true' log.debug('CTXT', 'Checking OIDC environment variables') log.debug('CTXT', 'Issuer: %s', issuer) @@ -171,6 +173,14 @@ async function checkOidc(config?: HeadscaleConfig) { } if (issuer && client && secret) { + if (!skip) { + log.debug('CTXT', 'Validating OIDC configuration from environment variables') + testOidc(issuer, client, secret) + } else { + log.debug('CTXT', 'OIDC_SKIP_CONFIG_VALIDATION is set') + log.debug('CTXT', 'Skipping OIDC configuration validation') + } + return { issuer, client, @@ -214,6 +224,15 @@ async function checkOidc(config?: HeadscaleConfig) { return } + if (config.oidc.only_start_if_oidc_is_available) { + log.debug('CTXT', 'Validating OIDC configuration from headscale config') + testOidc(issuer, client, secret) + return + } else { + log.debug('CTXT', 'OIDC validation is disabled in headscale config') + log.debug('CTXT', 'Skipping OIDC configuration validation') + } + return { issuer, client, diff --git a/app/utils/oidc.ts b/app/utils/oidc.ts index 70da015..e512080 100644 --- a/app/utils/oidc.ts +++ b/app/utils/oidc.ts @@ -17,6 +17,7 @@ import { import { post } from '~/utils/headscale' import { commitSession, getSession } from '~/utils/sessions' +import log from '~/utils/log' import { HeadplaneContext } from './config/headplane' @@ -169,3 +170,30 @@ export async function finishOidc(oidc: OidcConfig, req: Request) { }, }) } + +// Runs at application startup to validate the OIDC configuration +export async function testOidc(issuer: string, client: string, secret: string) { + const oidcClient = { + client_id: client, + client_secret: secret, + token_endpoint_auth_method: 'client_secret_post', + } satisfies Client + + const issuerUrl = new URL(issuer) + + try { + log.debug('OIDC', 'Checking OIDC well-known endpoint') + const response = await discoveryRequest(issuerUrl) + const processed = await processDiscoveryResponse(issuerUrl, response) + if (!processed.authorization_endpoint) { + log.debug('OIDC', 'No authorization endpoint found on the OIDC provider') + return false + } + + log.debug('OIDC', 'Found auth endpoint: %s', processed.authorization_endpoint) + return true + } catch (e) { + log.debug('OIDC', 'Validation failed: %s', e.message) + return false + } +} diff --git a/docs/Configuration.md b/docs/Configuration.md index 5c52170..f403cfa 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -36,6 +36,7 @@ If you use the Headscale configuration integration, these are not required. - **`OIDC_CLIENT_ID`**: The client ID of your OIDC provider. - **`OIDC_CLIENT_SECRET`**: The client secret of your OIDC provider. - **`OIDC_CLIENT_SECRET_METHOD`**: The method used to send the client secret (default: `client_secret_basic`). +- **`OIDC_SKIP_CONFIG_VALIDATION`**: Skip the OIDC configuration validation (default: `false`). - **`ROOT_API_KEY`**: An API key used to issue new ones for sessions (keep expiry fairly long). - **`DISABLE_API_KEY_LOGIN`**: If you want to disable API key login, set this to `true`.