diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ab1897..84f704d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Re-worked the OIDC integration to adhere to the correct standards and surface more errors to the user. - Deprecated `oidc.redirect_uri` and automated callback URL detection in favor of setting `server.base_url` correctly. - Explicitly added `oidc.use_pkce` to correctly determine PKCE configuration. + - `oidc.token_endpoint_auth_method` is now optional and will attempt to be auto-detected, defaulting to `client_secret_basic` if unavailable (closes [#410](https://github.com/tale/headplane/issues/410)). - Removed several unnecessarily verbose or spammy log messages. - Updated the minimum Docker API used to support the latest Docker versions (via [#370](https://github.com/tale/headplane/pull/370)). - Enhanced the node tag dialog to show a dropdown of assignable tags (via [#362](https://github.com/tale/headplane/pull/362)). diff --git a/app/server/config/config-schema.ts b/app/server/config/config-schema.ts index 8a6e71b..bc3e79b 100644 --- a/app/server/config/config-schema.ts +++ b/app/server/config/config-schema.ts @@ -101,13 +101,12 @@ const oidcConfig = type({ authorization_endpoint: 'string.url?', token_endpoint: 'string.url?', userinfo_endpoint: 'string.url?', + token_endpoint_auth_method: + '"client_secret_basic" | "client_secret_post" | "client_secret_jwt"?', // Old/deprecated options user_storage_file: 'string.lower = "/var/lib/headplane/users.json"', strict_validation: type('unknown').narrow(deprecatedField()).optional(), - token_endpoint_auth_method: type('unknown') - .narrow(deprecatedField()) - .optional(), }); const partialOidcConfig = type({ @@ -125,13 +124,12 @@ const partialOidcConfig = type({ authorization_endpoint: 'string.url?', token_endpoint: 'string.url?', userinfo_endpoint: 'string.url?', + token_endpoint_auth_method: + '"client_secret_basic" | "client_secret_post" | "client_secret_jwt"?', // Old/deprecated options user_storage_file: 'string.lower?', strict_validation: type('unknown').narrow(deprecatedField()).optional(), - token_endpoint_auth_method: type('unknown') - .narrow(deprecatedField()) - .optional(), }); const agentConfig = type({ diff --git a/app/server/web/oidc-connector.ts b/app/server/web/oidc-connector.ts index 9a00f8f..bee4c57 100644 --- a/app/server/web/oidc-connector.ts +++ b/app/server/web/oidc-connector.ts @@ -208,7 +208,53 @@ async function discoveryCoalesce( }, config.client_id, config.client_secret, + negotiateTokenEndpointAuthMethod(config, metadata), ); return oidcClient; } + +/** + * Determines the token endpoint authentication method based on config and metadata. + * + * @param config The OIDC configuration. + * @param metadata The OIDC server metadata. + * @returns The client authentication method for the token endpoint. + */ +function negotiateTokenEndpointAuthMethod( + config: OidcConfig, + metadata: oidc.ServerMetadata, +): oidc.ClientAuth { + if (config.token_endpoint_auth_method != null) { + switch (config.token_endpoint_auth_method) { + case 'client_secret_basic': + return oidc.ClientSecretBasic(config.client_secret); + case 'client_secret_post': + return oidc.ClientSecretPost(config.client_secret); + case 'client_secret_jwt': + return oidc.ClientSecretJwt(config.client_secret); + } + } + + const supported = metadata.token_endpoint_auth_methods_supported; + if (supported != null && supported.length > 0) { + // Prefer client_secret_basic (spec default), otherwise use first available + if (supported.includes('client_secret_basic')) { + return oidc.ClientSecretBasic(config.client_secret); + } + + if (supported.includes('client_secret_post')) { + return oidc.ClientSecretPost(config.client_secret); + } + + if (supported.includes('client_secret_jwt')) { + return oidc.ClientSecretJwt(config.client_secret); + } + } + + log.warn( + 'config', + 'Falling back to client_secret_basic for token endpoint authentication', + ); + return oidc.ClientSecretBasic(config.client_secret); +} diff --git a/config.example.yaml b/config.example.yaml index 6952e91..0ab2d31 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -185,6 +185,11 @@ integration: # token_endpoint: "" # userinfo_endpoint: "" + # The authentication method to use when communicating with the token endpoint. + # This is fully optional and Headplane will attempt to auto-detect the best + # method and fall back to `client_secret_basic` if unsure. + # token_endpoint_auth_method: "client_secret_post" + # The client ID for the OIDC client # For the best experience please ensure this is *identical* to the client_id # you are using for Headscale. because