mirror of
https://github.com/tale/headplane.git
synced 2026-08-12 14:46:52 +00:00
feat: add lazy retrying oidc connector
This commit is contained in:
+98
-103
@@ -1,142 +1,137 @@
|
||||
import { join } from 'node:path';
|
||||
import { exit, versions } from 'node:process';
|
||||
import { createHonoServer } from 'react-router-hono-server/node';
|
||||
import log from '~/utils/log';
|
||||
import { loadIntegration } from './config/integration';
|
||||
import { loadConfig } from './config/load';
|
||||
import { createDbClient } from './db/client.server';
|
||||
import { createHeadscaleInterface } from './headscale/api';
|
||||
import { loadHeadscaleConfig } from './headscale/config-loader';
|
||||
import { createHeadplaneAgent } from './hp-agent';
|
||||
import { createSessionStorage } from './web/sessions';
|
||||
import { join } from "node:path";
|
||||
import { exit, versions } from "node:process";
|
||||
import { createHonoServer } from "react-router-hono-server/node";
|
||||
|
||||
import log from "~/utils/log";
|
||||
|
||||
import { loadIntegration } from "./config/integration";
|
||||
import { loadConfig } from "./config/load";
|
||||
import { createDbClient } from "./db/client.server";
|
||||
import { createHeadscaleInterface } from "./headscale/api";
|
||||
import { loadHeadscaleConfig } from "./headscale/config-loader";
|
||||
import { createHeadplaneAgent } from "./hp-agent";
|
||||
import { createSessionStorage } from "./web/sessions";
|
||||
|
||||
declare global {
|
||||
const __PREFIX__: string;
|
||||
const __VERSION__: string;
|
||||
const __PREFIX__: string;
|
||||
const __VERSION__: string;
|
||||
}
|
||||
|
||||
// MARK: Side-Effects
|
||||
// This module contains a side-effect because everything running here
|
||||
// exists for the lifetime of the process, making it appropriate.
|
||||
log.info('server', 'Running Node.js %s', versions.node);
|
||||
log.info("server", "Running Node.js %s", versions.node);
|
||||
let config: HeadplaneConfig;
|
||||
|
||||
try {
|
||||
config = await loadConfig();
|
||||
config = await loadConfig();
|
||||
} catch (error) {
|
||||
if (error instanceof ConfigError) {
|
||||
log.error('server', 'Unable to load configuration: %s', error.message);
|
||||
}
|
||||
if (error instanceof ConfigError) {
|
||||
log.error("server", "Unable to load configuration: %s", error.message);
|
||||
}
|
||||
|
||||
exit(1);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
const db = await createDbClient(join(config.server.data_path, 'hp_persist.db'));
|
||||
const agents = await createHeadplaneAgent(
|
||||
config.integration?.agent,
|
||||
config.headscale.url,
|
||||
db,
|
||||
);
|
||||
const db = await createDbClient(join(config.server.data_path, "hp_persist.db"));
|
||||
const agents = await createHeadplaneAgent(config.integration?.agent, config.headscale.url, db);
|
||||
|
||||
const hsApi = await createHeadscaleInterface(
|
||||
config.headscale.url,
|
||||
config.headscale.tls_cert_path,
|
||||
);
|
||||
const hsApi = await createHeadscaleInterface(config.headscale.url, config.headscale.tls_cert_path);
|
||||
|
||||
// We also use this file to load anything needed by the react router code.
|
||||
// These are usually per-request things that we need access to, like the
|
||||
// helper that can issue and revoke cookies.
|
||||
export type LoadContext = typeof appLoadContext;
|
||||
|
||||
import 'react-router';
|
||||
import { HeadplaneConfig } from './config/config-schema';
|
||||
import { ConfigError } from './config/error';
|
||||
import { createOidcConnector } from './web/oidc-connector';
|
||||
import "react-router";
|
||||
import { HeadplaneConfig } from "./config/config-schema";
|
||||
import { ConfigError } from "./config/error";
|
||||
import { createLazyOidcConnector } from "./web/oidc-connector";
|
||||
|
||||
declare module 'react-router' {
|
||||
interface AppLoadContext extends LoadContext {}
|
||||
declare module "react-router" {
|
||||
interface AppLoadContext extends LoadContext {}
|
||||
}
|
||||
|
||||
const appLoadContext = {
|
||||
config,
|
||||
hs: await loadHeadscaleConfig(
|
||||
config.headscale.config_path,
|
||||
config.headscale.config_strict,
|
||||
config.headscale.dns_records_path,
|
||||
),
|
||||
config,
|
||||
hs: await loadHeadscaleConfig(
|
||||
config.headscale.config_path,
|
||||
config.headscale.config_strict,
|
||||
config.headscale.dns_records_path,
|
||||
),
|
||||
|
||||
// TODO: Better cookie options in config
|
||||
sessions: await createSessionStorage({
|
||||
secret: config.server.cookie_secret,
|
||||
db,
|
||||
oidcUsersFile: config.oidc?.user_storage_file,
|
||||
cookie: {
|
||||
name: '_hp_auth',
|
||||
secure: config.server.cookie_secure,
|
||||
maxAge: config.server.cookie_max_age,
|
||||
domain: config.server.cookie_domain,
|
||||
},
|
||||
}),
|
||||
// TODO: Better cookie options in config
|
||||
sessions: await createSessionStorage({
|
||||
secret: config.server.cookie_secret,
|
||||
db,
|
||||
oidcUsersFile: config.oidc?.user_storage_file,
|
||||
cookie: {
|
||||
name: "_hp_auth",
|
||||
secure: config.server.cookie_secure,
|
||||
maxAge: config.server.cookie_max_age,
|
||||
domain: config.server.cookie_domain,
|
||||
},
|
||||
}),
|
||||
|
||||
hsApi,
|
||||
agents,
|
||||
integration: await loadIntegration(config.integration),
|
||||
oidcConnector: config.oidc
|
||||
? await createOidcConnector(
|
||||
config.server.base_url,
|
||||
config.oidc,
|
||||
hsApi.getRuntimeClient(config.oidc.headscale_api_key),
|
||||
)
|
||||
: undefined,
|
||||
db,
|
||||
hsApi,
|
||||
agents,
|
||||
integration: await loadIntegration(config.integration),
|
||||
oidcConnector: config.oidc
|
||||
? createLazyOidcConnector(
|
||||
config.server.base_url,
|
||||
config.oidc,
|
||||
hsApi.getRuntimeClient(config.oidc.headscale_api_key),
|
||||
)
|
||||
: undefined,
|
||||
db,
|
||||
};
|
||||
|
||||
declare module 'react-router' {
|
||||
interface AppLoadContext extends LoadContext {}
|
||||
declare module "react-router" {
|
||||
interface AppLoadContext extends LoadContext {}
|
||||
}
|
||||
|
||||
export default createHonoServer({
|
||||
overrideGlobalObjects: true,
|
||||
port: config.server.port,
|
||||
hostname: config.server.host,
|
||||
beforeAll: async (app) => {
|
||||
app.use(__PREFIX__, async (c) => {
|
||||
return c.redirect(`${__PREFIX__}/`);
|
||||
});
|
||||
},
|
||||
serveStaticOptions: {
|
||||
publicAssets: {
|
||||
// This is part of our monkey-patch for react-router-hono-server
|
||||
// To see the first part, go to the patches/ directory.
|
||||
rewriteRequestPath: (path) => path.replace(`${__PREFIX__}`, ''),
|
||||
},
|
||||
clientAssets: {
|
||||
// This is part of our monkey-patch for react-router-hono-server
|
||||
// To see the first part, go to the patches/ directory.
|
||||
rewriteRequestPath: (path) => path.replace(`${__PREFIX__}`, ''),
|
||||
},
|
||||
},
|
||||
overrideGlobalObjects: true,
|
||||
port: config.server.port,
|
||||
hostname: config.server.host,
|
||||
beforeAll: async (app) => {
|
||||
app.use(__PREFIX__, async (c) => {
|
||||
return c.redirect(`${__PREFIX__}/`);
|
||||
});
|
||||
},
|
||||
serveStaticOptions: {
|
||||
publicAssets: {
|
||||
// This is part of our monkey-patch for react-router-hono-server
|
||||
// To see the first part, go to the patches/ directory.
|
||||
rewriteRequestPath: (path) => path.replace(`${__PREFIX__}`, ""),
|
||||
},
|
||||
clientAssets: {
|
||||
// This is part of our monkey-patch for react-router-hono-server
|
||||
// To see the first part, go to the patches/ directory.
|
||||
rewriteRequestPath: (path) => path.replace(`${__PREFIX__}`, ""),
|
||||
},
|
||||
},
|
||||
|
||||
// Only log in development mode
|
||||
defaultLogger: import.meta.env.DEV,
|
||||
getLoadContext() {
|
||||
// TODO: This is the place where we can handle reverse proxy translation
|
||||
// This is better than doing it in the OIDC client, since we can do it
|
||||
// for all requests, not just OIDC ones.
|
||||
return appLoadContext;
|
||||
},
|
||||
// Only log in development mode
|
||||
defaultLogger: import.meta.env.DEV,
|
||||
getLoadContext() {
|
||||
// TODO: This is the place where we can handle reverse proxy translation
|
||||
// This is better than doing it in the OIDC client, since we can do it
|
||||
// for all requests, not just OIDC ones.
|
||||
return appLoadContext;
|
||||
},
|
||||
|
||||
listeningListener(info) {
|
||||
log.info('server', 'Running on %s:%s', info.address, info.port);
|
||||
},
|
||||
listeningListener(info) {
|
||||
log.info("server", "Running on %s:%s", info.address, info.port);
|
||||
},
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
log.info('server', 'Received SIGINT, shutting down...');
|
||||
process.exit(0);
|
||||
process.on("SIGINT", () => {
|
||||
log.info("server", "Received SIGINT, shutting down...");
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
log.info('server', 'Received SIGTERM, shutting down...');
|
||||
process.exit(0);
|
||||
process.on("SIGTERM", () => {
|
||||
log.info("server", "Received SIGTERM, shutting down...");
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
+255
-193
@@ -1,41 +1,116 @@
|
||||
import * as oidc from 'openid-client';
|
||||
import log from '~/utils/log';
|
||||
import type { HeadplaneConfig } from '../config/config-schema';
|
||||
import type { RuntimeApiClient } from '../headscale/api/endpoints';
|
||||
import { isDataUnauthorizedError } from '../headscale/api/error-client';
|
||||
import * as oidc from "openid-client";
|
||||
|
||||
export type OidcConfig = NonNullable<HeadplaneConfig['oidc']>;
|
||||
import log from "~/utils/log";
|
||||
|
||||
import type { HeadplaneConfig } from "../config/config-schema";
|
||||
import type { RuntimeApiClient } from "../headscale/api/endpoints";
|
||||
|
||||
import { isDataUnauthorizedError } from "../headscale/api/error-client";
|
||||
|
||||
export type OidcConfig = NonNullable<HeadplaneConfig["oidc"]>;
|
||||
|
||||
/**
|
||||
* Errors that can occur during OIDC connector setup and validation.
|
||||
*/
|
||||
export type OidcConnectorError =
|
||||
| 'INVALID_API_KEY'
|
||||
| 'MISSING_AUTHORIZATION_ENDPOINT'
|
||||
| 'MISSING_TOKEN_ENDPOINT'
|
||||
| 'MISSING_USERINFO_ENDPOINT'
|
||||
| 'MISSING_REQUIRED_CLAIMS'
|
||||
| 'UNKNOWN_ERROR';
|
||||
| "INVALID_API_KEY"
|
||||
| "MISSING_AUTHORIZATION_ENDPOINT"
|
||||
| "MISSING_TOKEN_ENDPOINT"
|
||||
| "MISSING_USERINFO_ENDPOINT"
|
||||
| "MISSING_REQUIRED_CLAIMS"
|
||||
| "DISCOVERY_FAILED"
|
||||
| "UNKNOWN_ERROR";
|
||||
|
||||
/**
|
||||
* Represents a "configured" OIDC setup for Headplane.
|
||||
* This may include mis-configured versions too and will surface error messages.
|
||||
*/
|
||||
export type OidcConnector =
|
||||
| {
|
||||
isValid: true;
|
||||
isExclusive: boolean;
|
||||
usePKCE: boolean;
|
||||
client: oidc.Configuration;
|
||||
apiKey: string;
|
||||
scope: string;
|
||||
extraParams?: Record<string, string>;
|
||||
}
|
||||
| {
|
||||
isValid: false;
|
||||
isExclusive: false;
|
||||
errors: OidcConnectorError[];
|
||||
};
|
||||
| {
|
||||
isValid: true;
|
||||
isExclusive: boolean;
|
||||
usePKCE: boolean;
|
||||
client: oidc.Configuration;
|
||||
apiKey: string;
|
||||
scope: string;
|
||||
extraParams?: Record<string, string>;
|
||||
}
|
||||
| {
|
||||
isValid: false;
|
||||
isExclusive: false;
|
||||
errors: OidcConnectorError[];
|
||||
};
|
||||
|
||||
/**
|
||||
* A lazy OIDC connector that retries initialization on failure.
|
||||
* This allows OIDC to recover from transient startup failures (e.g., network issues,
|
||||
* OIDC provider temporarily unavailable) without requiring a server restart.
|
||||
*/
|
||||
export interface LazyOidcConnector {
|
||||
/**
|
||||
* Get the current OIDC connector state.
|
||||
* If a previous attempt failed, this will retry initialization.
|
||||
* Successful results are cached until invalidated.
|
||||
*/
|
||||
get(): Promise<OidcConnector>;
|
||||
|
||||
/**
|
||||
* Force a re-initialization of the OIDC connector on the next get() call.
|
||||
* Useful for manually triggering a retry after configuration changes.
|
||||
*/
|
||||
invalidate(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a lazy OIDC connector that retries on failure.
|
||||
* Successful initialization is cached; failed attempts are retried on each get() call.
|
||||
*
|
||||
* @param baseUrl The base URL of the Headplane server.
|
||||
* @param config The OIDC configuration.
|
||||
* @param client The Headscale runtime API client.
|
||||
* @returns A lazy OIDC connector that retries on failure.
|
||||
*/
|
||||
export function createLazyOidcConnector(
|
||||
baseUrl: string | undefined,
|
||||
config: OidcConfig,
|
||||
client: RuntimeApiClient,
|
||||
): LazyOidcConnector {
|
||||
let cachedConnector: OidcConnector | undefined;
|
||||
let initPromise: Promise<OidcConnector> | undefined;
|
||||
|
||||
return {
|
||||
async get(): Promise<OidcConnector> {
|
||||
if (cachedConnector?.isValid) {
|
||||
return cachedConnector;
|
||||
}
|
||||
|
||||
if (initPromise) {
|
||||
return initPromise;
|
||||
}
|
||||
|
||||
initPromise = createOidcConnector(baseUrl, config, client);
|
||||
try {
|
||||
const connector = await initPromise;
|
||||
if (connector.isValid) {
|
||||
cachedConnector = connector;
|
||||
log.info("auth", "OIDC connector initialized successfully");
|
||||
} else {
|
||||
log.warn("auth", "OIDC connector initialization failed, will retry on next request");
|
||||
}
|
||||
return connector;
|
||||
} finally {
|
||||
// Clear the promise so we can retry on next call if it failed
|
||||
initPromise = undefined;
|
||||
}
|
||||
},
|
||||
|
||||
invalidate(): void {
|
||||
cachedConnector = undefined;
|
||||
initPromise = undefined;
|
||||
log.info("auth", "OIDC connector cache invalidated");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an OIDC connector based on the configuration and Headscale API.
|
||||
@@ -46,63 +121,63 @@ export type OidcConnector =
|
||||
* @param client The Headscale runtime API client.
|
||||
* @returns An OIDC connector with validation status.
|
||||
*/
|
||||
export async function createOidcConnector(
|
||||
baseUrl: string | undefined,
|
||||
config: OidcConfig,
|
||||
client: RuntimeApiClient,
|
||||
async function createOidcConnector(
|
||||
baseUrl: string | undefined,
|
||||
config: OidcConfig,
|
||||
client: RuntimeApiClient,
|
||||
): Promise<OidcConnector> {
|
||||
if (baseUrl == null && config.redirect_uri == null) {
|
||||
log.warn(
|
||||
'config',
|
||||
'OIDC is enabled but `server.base_url` is not set in the config. Starting in Headplane 0.7.0 this will be required for OIDC to function properly and will throw errors if not set, see https://headplane.net/features/sso#configuring-oidc for more information.',
|
||||
);
|
||||
}
|
||||
if (baseUrl == null && config.redirect_uri == null) {
|
||||
log.warn(
|
||||
"config",
|
||||
"OIDC is enabled but `server.base_url` is not set in the config. Starting in Headplane 0.7.0 this will be required for OIDC to function properly and will throw errors if not set, see https://headplane.net/features/sso#configuring-oidc for more information.",
|
||||
);
|
||||
}
|
||||
|
||||
const errors: OidcConnectorError[] = [];
|
||||
if (!config.headscale_api_key) {
|
||||
errors.push('INVALID_API_KEY');
|
||||
return {
|
||||
isValid: false,
|
||||
isExclusive: false,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
const errors: OidcConnectorError[] = [];
|
||||
if (!config.headscale_api_key) {
|
||||
errors.push("INVALID_API_KEY");
|
||||
return {
|
||||
isValid: false,
|
||||
isExclusive: false,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await client.getApiKeys();
|
||||
} catch (error) {
|
||||
if (isDataUnauthorizedError(error)) {
|
||||
errors.push('INVALID_API_KEY');
|
||||
return {
|
||||
isValid: false,
|
||||
isExclusive: false,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
try {
|
||||
await client.getApiKeys();
|
||||
} catch (error) {
|
||||
if (isDataUnauthorizedError(error)) {
|
||||
errors.push("INVALID_API_KEY");
|
||||
return {
|
||||
isValid: false,
|
||||
isExclusive: false,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
// MARK: Otherwise assume the API key is valid since the API request
|
||||
// failed for another reason that isn't 401 and we are optimistic
|
||||
}
|
||||
// MARK: Otherwise assume the API key is valid since the API request
|
||||
// failed for another reason that isn't 401 and we are optimistic
|
||||
}
|
||||
|
||||
const oidcClientOrErrors = await discoveryCoalesce(config);
|
||||
if (Array.isArray(oidcClientOrErrors)) {
|
||||
errors.push(...oidcClientOrErrors);
|
||||
return {
|
||||
isValid: false,
|
||||
isExclusive: false,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
const oidcClientOrErrors = await discoveryCoalesce(config);
|
||||
if (Array.isArray(oidcClientOrErrors)) {
|
||||
errors.push(...oidcClientOrErrors);
|
||||
return {
|
||||
isValid: false,
|
||||
isExclusive: false,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: true,
|
||||
isExclusive: config.disable_api_key_login,
|
||||
usePKCE: config.use_pkce,
|
||||
client: oidcClientOrErrors,
|
||||
apiKey: config.headscale_api_key,
|
||||
scope: config.scope,
|
||||
extraParams: config.extra_params,
|
||||
};
|
||||
return {
|
||||
isValid: true,
|
||||
isExclusive: config.disable_api_key_login,
|
||||
usePKCE: config.use_pkce,
|
||||
client: oidcClientOrErrors,
|
||||
apiKey: config.headscale_api_key,
|
||||
scope: config.scope,
|
||||
extraParams: config.extra_params,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,105 +188,95 @@ export async function createOidcConnector(
|
||||
* @returns The coalesced OIDC configuration or an array of errors.
|
||||
*/
|
||||
async function discoveryCoalesce(
|
||||
config: OidcConfig,
|
||||
config: OidcConfig,
|
||||
): Promise<oidc.Configuration | OidcConnectorError[]> {
|
||||
let metadata: oidc.ServerMetadata;
|
||||
try {
|
||||
const client = await oidc.discovery(
|
||||
new URL(config.issuer),
|
||||
config.client_id,
|
||||
);
|
||||
metadata = client.serverMetadata();
|
||||
if (config.use_pkce === true && !client.serverMetadata().supportsPKCE()) {
|
||||
log.warn(
|
||||
'config',
|
||||
'OIDC provider does not support PKCE, but it is enabled in the config',
|
||||
);
|
||||
}
|
||||
let metadata: oidc.ServerMetadata;
|
||||
let discoveryFailed = false;
|
||||
|
||||
if (metadata.claims_supported != null) {
|
||||
if (!metadata.claims_supported.includes('sub')) {
|
||||
log.error('config', 'OIDC provider does not support `sub` claim');
|
||||
return ['MISSING_REQUIRED_CLAIMS'];
|
||||
}
|
||||
try {
|
||||
const client = await oidc.discovery(new URL(config.issuer), config.client_id);
|
||||
metadata = client.serverMetadata();
|
||||
if (config.use_pkce === true && !client.serverMetadata().supportsPKCE()) {
|
||||
log.warn("config", "OIDC provider does not support PKCE, but it is enabled in the config");
|
||||
}
|
||||
|
||||
if (!metadata.claims_supported.includes('name')) {
|
||||
if (
|
||||
!(
|
||||
metadata.claims_supported.includes('given_name') &&
|
||||
metadata.claims_supported.includes('family_name')
|
||||
)
|
||||
) {
|
||||
log.warn(
|
||||
'config',
|
||||
'OIDC provider does not support `name`, `given_name`, or `family_name` claims',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (metadata.claims_supported != null) {
|
||||
if (!metadata.claims_supported.includes("sub")) {
|
||||
log.error("config", "OIDC provider does not support `sub` claim");
|
||||
return ["MISSING_REQUIRED_CLAIMS"];
|
||||
}
|
||||
|
||||
if (
|
||||
!metadata.claims_supported.includes('preferred_username') &&
|
||||
!metadata.claims_supported.includes('email')
|
||||
) {
|
||||
log.warn(
|
||||
'config',
|
||||
'OIDC provider does not support `preferred_username` or `email` claims',
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
log.error(
|
||||
'config',
|
||||
'Failed to auto-configure OIDC endpoints via discovery',
|
||||
);
|
||||
log.warn(
|
||||
'config',
|
||||
'OIDC server may not support discovery, using manual config',
|
||||
);
|
||||
metadata = {
|
||||
issuer: config.issuer,
|
||||
};
|
||||
}
|
||||
if (!metadata.claims_supported.includes("name")) {
|
||||
if (
|
||||
!(
|
||||
metadata.claims_supported.includes("given_name") &&
|
||||
metadata.claims_supported.includes("family_name")
|
||||
)
|
||||
) {
|
||||
log.warn(
|
||||
"config",
|
||||
"OIDC provider does not support `name`, `given_name`, or `family_name` claims",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const errors: OidcConnectorError[] = [];
|
||||
const authorization_endpoint =
|
||||
config.authorization_endpoint ?? metadata.authorization_endpoint;
|
||||
if (
|
||||
!metadata.claims_supported.includes("preferred_username") &&
|
||||
!metadata.claims_supported.includes("email")
|
||||
) {
|
||||
log.warn("config", "OIDC provider does not support `preferred_username` or `email` claims");
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
log.warn("oidc", "Failed to reach OIDC provider for discovery, will retry on next request");
|
||||
discoveryFailed = true;
|
||||
metadata = {
|
||||
issuer: config.issuer,
|
||||
};
|
||||
}
|
||||
|
||||
const token_endpoint = config.token_endpoint ?? metadata.token_endpoint;
|
||||
const authorization_endpoint = config.authorization_endpoint ?? metadata.authorization_endpoint;
|
||||
const token_endpoint = config.token_endpoint ?? metadata.token_endpoint;
|
||||
const userinfo_endpoint = config.userinfo_endpoint ?? metadata.userinfo_endpoint;
|
||||
|
||||
const userinfo_endpoint =
|
||||
config.userinfo_endpoint ?? metadata.userinfo_endpoint;
|
||||
const hasMissingEndpoints = !authorization_endpoint || !token_endpoint || !userinfo_endpoint;
|
||||
|
||||
if (!authorization_endpoint) {
|
||||
errors.push('MISSING_AUTHORIZATION_ENDPOINT');
|
||||
}
|
||||
if (discoveryFailed && hasMissingEndpoints) {
|
||||
return ["DISCOVERY_FAILED"];
|
||||
}
|
||||
|
||||
if (!token_endpoint) {
|
||||
errors.push('MISSING_TOKEN_ENDPOINT');
|
||||
}
|
||||
const errors: OidcConnectorError[] = [];
|
||||
|
||||
if (!userinfo_endpoint) {
|
||||
errors.push('MISSING_USERINFO_ENDPOINT');
|
||||
}
|
||||
if (!authorization_endpoint) {
|
||||
errors.push("MISSING_AUTHORIZATION_ENDPOINT");
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return errors;
|
||||
}
|
||||
if (!token_endpoint) {
|
||||
errors.push("MISSING_TOKEN_ENDPOINT");
|
||||
}
|
||||
|
||||
const oidcClient = new oidc.Configuration(
|
||||
{
|
||||
...metadata,
|
||||
issuer: config.issuer,
|
||||
authorization_endpoint,
|
||||
token_endpoint,
|
||||
userinfo_endpoint,
|
||||
},
|
||||
config.client_id,
|
||||
config.client_secret,
|
||||
negotiateTokenEndpointAuthMethod(config, metadata),
|
||||
);
|
||||
if (!userinfo_endpoint) {
|
||||
errors.push("MISSING_USERINFO_ENDPOINT");
|
||||
}
|
||||
|
||||
return oidcClient;
|
||||
if (errors.length > 0) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
const oidcClient = new oidc.Configuration(
|
||||
{
|
||||
...metadata,
|
||||
issuer: config.issuer,
|
||||
authorization_endpoint,
|
||||
token_endpoint,
|
||||
userinfo_endpoint,
|
||||
},
|
||||
config.client_id,
|
||||
config.client_secret,
|
||||
negotiateTokenEndpointAuthMethod(config, metadata),
|
||||
);
|
||||
|
||||
return oidcClient;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -222,39 +287,36 @@ async function discoveryCoalesce(
|
||||
* @returns The client authentication method for the token endpoint.
|
||||
*/
|
||||
function negotiateTokenEndpointAuthMethod(
|
||||
config: OidcConfig,
|
||||
metadata: oidc.ServerMetadata,
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
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_post")) {
|
||||
return oidc.ClientSecretPost(config.client_secret);
|
||||
}
|
||||
|
||||
if (supported.includes('client_secret_jwt')) {
|
||||
return oidc.ClientSecretJwt(config.client_secret);
|
||||
}
|
||||
}
|
||||
if (supported.includes("client_secret_jwt")) {
|
||||
return oidc.ClientSecretJwt(config.client_secret);
|
||||
}
|
||||
}
|
||||
|
||||
log.warn(
|
||||
'config',
|
||||
'Falling back to client_secret_post for token endpoint authentication',
|
||||
);
|
||||
return oidc.ClientSecretPost(config.client_secret);
|
||||
log.warn("config", "Falling back to client_secret_post for token endpoint authentication");
|
||||
return oidc.ClientSecretPost(config.client_secret);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user