headplane/app/server/web/oidc.ts

158 lines
4.4 KiB
TypeScript
Raw Normal View History

2025-08-28 22:49:55 -04:00
import * as oidc from 'openid-client';
2025-03-22 01:36:27 -04:00
import log from '~/utils/log';
2025-08-28 22:49:55 -04:00
import { HeadplaneConfig } from '../config/schema';
2025-03-22 01:36:27 -04:00
2025-08-28 22:49:55 -04:00
export type OidcConfig = NonNullable<HeadplaneConfig['oidc']>;
2025-03-22 01:36:27 -04:00
2025-08-28 22:49:55 -04:00
export async function configureOidcAuth(config: OidcConfig) {
log.debug('config', 'Running OIDC discovery for %s', config.issuer);
let clientAuthMethod: oidc.ClientAuth;
switch (config.token_endpoint_auth_method) {
2025-03-22 01:36:27 -04:00
case 'client_secret_basic':
2025-08-28 22:49:55 -04:00
clientAuthMethod = oidc.ClientSecretBasic(config.client_secret!);
break;
case 'client_secret_post':
clientAuthMethod = oidc.ClientSecretPost(config.client_secret!);
break;
2025-03-22 01:36:27 -04:00
case 'client_secret_jwt':
2025-08-28 22:49:55 -04:00
clientAuthMethod = oidc.ClientSecretJwt(config.client_secret!);
break;
2025-03-22 01:36:27 -04:00
default:
throw new Error('Invalid client authentication method');
}
2025-08-28 22:49:55 -04:00
let oidcClient: oidc.Configuration;
2025-05-04 14:43:40 -04:00
try {
2025-08-28 22:49:55 -04:00
const discovery = await oidc.discovery(
2025-05-04 14:43:40 -04:00
new URL(config.issuer),
config.client_id,
2025-08-28 22:49:55 -04:00
config.client_secret!, // TODO: Fix this config schema
clientAuthMethod,
2025-03-22 01:36:27 -04:00
);
2025-08-28 22:49:55 -04:00
const meta = discovery.serverMetadata();
if (!meta.authorization_endpoint) {
2025-03-22 01:36:27 -04:00
log.error(
'config',
2025-05-04 14:43:40 -04:00
'Issuer discovery did not return `authorization_endpoint`',
2025-03-22 01:36:27 -04:00
);
log.error(
'config',
2025-05-04 14:43:40 -04:00
'OIDC server does not support authorization code flow',
2025-03-22 01:36:27 -04:00
);
2025-08-28 22:49:55 -04:00
log.error('config', 'You may need to set this manually in the config');
2025-05-04 14:43:40 -04:00
return;
}
2025-08-28 22:49:55 -04:00
if (!meta.token_endpoint) {
2025-05-04 14:43:40 -04:00
log.error('config', 'Issuer discovery did not return `token_endpoint`');
2025-08-28 22:49:55 -04:00
log.error(
'config',
'OIDC server does not support authorization code flow',
);
log.error('config', 'You may need to set this manually in the config');
2025-05-04 14:43:40 -04:00
return;
}
2025-08-28 22:49:55 -04:00
if (!meta.userinfo_endpoint) {
log.error(
'config',
'Issuer discovery did not return `userinfo_endpoint`',
);
log.error('config', 'OIDC server does not support user info endpoint');
log.error('config', 'You may need to set this manually in the config');
return;
2025-05-04 14:43:40 -04:00
}
2025-08-28 22:49:55 -04:00
if (meta.token_endpoint_auth_methods_supported) {
2025-05-04 14:43:40 -04:00
if (
2025-08-28 22:49:55 -04:00
!meta.token_endpoint_auth_methods_supported.includes(
2025-05-04 14:43:40 -04:00
config.token_endpoint_auth_method,
)
) {
log.error(
'config',
2025-08-28 22:49:55 -04:00
'OIDC server does not support client authentication method %s',
2025-05-04 14:43:40 -04:00
config.token_endpoint_auth_method,
);
log.error(
'config',
2025-08-28 22:49:55 -04:00
'Supported methods: %s',
meta.token_endpoint_auth_methods_supported.join(', '),
2025-05-04 14:43:40 -04:00
);
return;
}
}
2025-08-28 22:49:55 -04:00
log.debug('config', 'OIDC discovery successful');
log.debug(
'config',
'Authorization endpoint: %s',
meta.authorization_endpoint,
);
log.debug('config', 'Token endpoint: %s', meta.token_endpoint);
log.debug('config', 'Userinfo endpoint: %s', meta.userinfo_endpoint);
// Manually construct the endpoints to coalesce with config if needed
oidcClient = new oidc.Configuration(
{
issuer: config.issuer,
authorization_endpoint:
config.authorization_endpoint || meta.authorization_endpoint,
token_endpoint: config.token_endpoint || meta.token_endpoint,
userinfo_endpoint: config.userinfo_endpoint || meta.userinfo_endpoint,
},
config.client_id,
config.client_secret!,
clientAuthMethod,
);
} catch (err) {
log.error('config', 'OIDC discovery failed: %s', err);
log.debug('config', 'Error details: %o', err);
log.error(
'config',
'This may be an error, or the server may not support discovery',
);
if (
!config.authorization_endpoint ||
!config.token_endpoint ||
!config.userinfo_endpoint
) {
2025-03-22 01:36:27 -04:00
log.error(
'config',
2025-08-28 22:49:55 -04:00
'Endpoints are not fully configured, cannot continue',
);
log.error(
'config',
'You must set authorization_endpoint, token_endpoint and userinfo_endpoint manually in the config or fix the discovery issue',
2025-03-22 01:36:27 -04:00
);
return;
}
2025-08-28 22:49:55 -04:00
oidcClient = new oidc.Configuration(
{
issuer: config.issuer,
authorization_endpoint: config.authorization_endpoint,
token_endpoint: config.token_endpoint,
userinfo_endpoint: config.userinfo_endpoint,
},
config.client_id,
config.client_secret!,
clientAuthMethod,
);
log.debug('config', 'Using manually configured endpoints');
2025-05-04 14:43:40 -04:00
log.debug(
'config',
'Authorization endpoint: %s',
2025-08-28 22:49:55 -04:00
config.authorization_endpoint,
2025-05-04 14:43:40 -04:00
);
2025-08-28 22:49:55 -04:00
log.debug('config', 'Token endpoint: %s', config.token_endpoint);
log.debug('config', 'Userinfo endpoint: %s', config.userinfo_endpoint);
2025-03-22 01:36:27 -04:00
}
2025-08-28 22:49:55 -04:00
log.info('config', 'Successfully configured OIDC authentication');
return oidcClient;
2025-03-22 01:36:27 -04:00
}