- Updated all package.json name fields and dependency references - Updated all TypeScript/JavaScript imports - Updated .woodpecker/publish.yml filters and registry paths - Updated tools/install.sh scope default - Updated .npmrc registry paths (worktree + host) - Enhanced update-checker.ts with checkForAllUpdates() multi-package support - Updated CLI update command to show table of all packages - Added KNOWN_PACKAGES, formatAllPackagesTable, getInstallAllCommand - Marked checkForUpdate() with @deprecated JSDoc Closes #391
242 lines
7.5 KiB
TypeScript
242 lines
7.5 KiB
TypeScript
export type SupportedSsoProviderId = 'authentik' | 'workos' | 'keycloak';
|
|
export type SsoProtocol = 'oidc' | 'saml';
|
|
export type SsoLoginMode = 'oidc' | 'saml' | null;
|
|
|
|
type EnvMap = Record<string, string | undefined>;
|
|
|
|
export interface GenericOidcProviderConfig {
|
|
providerId: SupportedSsoProviderId;
|
|
clientId: string;
|
|
clientSecret: string;
|
|
discoveryUrl?: string;
|
|
issuer?: string;
|
|
authorizationUrl?: string;
|
|
tokenUrl?: string;
|
|
userInfoUrl?: string;
|
|
scopes: string[];
|
|
pkce?: boolean;
|
|
requireIssuerValidation?: boolean;
|
|
}
|
|
|
|
export interface SsoTeamSyncConfig {
|
|
enabled: boolean;
|
|
claim: string | null;
|
|
}
|
|
|
|
export interface SsoProviderDiscovery {
|
|
id: SupportedSsoProviderId;
|
|
name: string;
|
|
protocols: SsoProtocol[];
|
|
configured: boolean;
|
|
loginMode: SsoLoginMode;
|
|
callbackPath: string | null;
|
|
teamSync: SsoTeamSyncConfig;
|
|
samlFallback: {
|
|
configured: boolean;
|
|
loginUrl: string | null;
|
|
};
|
|
warnings: string[];
|
|
}
|
|
|
|
const DEFAULT_SCOPES = ['openid', 'email', 'profile'];
|
|
|
|
function readEnv(env: EnvMap, key: string): string | undefined {
|
|
const value = env[key]?.trim();
|
|
return value ? value : undefined;
|
|
}
|
|
|
|
function toDiscoveryUrl(issuer: string): string {
|
|
return `${issuer.replace(/\/$/, '')}/.well-known/openid-configuration`;
|
|
}
|
|
|
|
function getTeamSyncClaim(env: EnvMap, envKey: string, fallbackClaim?: string): SsoTeamSyncConfig {
|
|
const claim = readEnv(env, envKey) ?? fallbackClaim ?? null;
|
|
return {
|
|
enabled: claim !== null,
|
|
claim,
|
|
};
|
|
}
|
|
|
|
function buildAuthentikConfig(env: EnvMap): GenericOidcProviderConfig | null {
|
|
const issuer = readEnv(env, 'AUTHENTIK_ISSUER');
|
|
const clientId = readEnv(env, 'AUTHENTIK_CLIENT_ID');
|
|
const clientSecret = readEnv(env, 'AUTHENTIK_CLIENT_SECRET');
|
|
|
|
const fields = [issuer, clientId, clientSecret];
|
|
const presentCount = fields.filter(Boolean).length;
|
|
if (presentCount > 0 && presentCount < fields.length) {
|
|
throw new Error(
|
|
'@mosaicstack/auth: Authentik SSO requires AUTHENTIK_ISSUER, AUTHENTIK_CLIENT_ID, AUTHENTIK_CLIENT_SECRET.',
|
|
);
|
|
}
|
|
if (!issuer || !clientId || !clientSecret) {
|
|
return null;
|
|
}
|
|
|
|
const baseIssuer = issuer.replace(/\/$/, '');
|
|
|
|
return {
|
|
providerId: 'authentik',
|
|
issuer: baseIssuer,
|
|
clientId,
|
|
clientSecret,
|
|
discoveryUrl: toDiscoveryUrl(baseIssuer),
|
|
authorizationUrl: `${baseIssuer}/application/o/authorize/`,
|
|
tokenUrl: `${baseIssuer}/application/o/token/`,
|
|
userInfoUrl: `${baseIssuer}/application/o/userinfo/`,
|
|
scopes: DEFAULT_SCOPES,
|
|
};
|
|
}
|
|
|
|
function buildWorkosConfig(env: EnvMap): GenericOidcProviderConfig | null {
|
|
const issuer = readEnv(env, 'WORKOS_ISSUER');
|
|
const clientId = readEnv(env, 'WORKOS_CLIENT_ID');
|
|
const clientSecret = readEnv(env, 'WORKOS_CLIENT_SECRET');
|
|
|
|
const fields = [issuer, clientId, clientSecret];
|
|
const presentCount = fields.filter(Boolean).length;
|
|
if (presentCount > 0 && presentCount < fields.length) {
|
|
throw new Error(
|
|
'@mosaicstack/auth: WorkOS SSO requires WORKOS_ISSUER, WORKOS_CLIENT_ID, WORKOS_CLIENT_SECRET.',
|
|
);
|
|
}
|
|
if (!issuer || !clientId || !clientSecret) {
|
|
return null;
|
|
}
|
|
|
|
const normalizedIssuer = issuer.replace(/\/$/, '');
|
|
|
|
return {
|
|
providerId: 'workos',
|
|
issuer: normalizedIssuer,
|
|
clientId,
|
|
clientSecret,
|
|
discoveryUrl: toDiscoveryUrl(normalizedIssuer),
|
|
scopes: DEFAULT_SCOPES,
|
|
pkce: true,
|
|
requireIssuerValidation: true,
|
|
};
|
|
}
|
|
|
|
function buildKeycloakConfig(env: EnvMap): GenericOidcProviderConfig | null {
|
|
const explicitIssuer = readEnv(env, 'KEYCLOAK_ISSUER');
|
|
const keycloakUrl = readEnv(env, 'KEYCLOAK_URL');
|
|
const keycloakRealm = readEnv(env, 'KEYCLOAK_REALM');
|
|
const clientId = readEnv(env, 'KEYCLOAK_CLIENT_ID');
|
|
const clientSecret = readEnv(env, 'KEYCLOAK_CLIENT_SECRET');
|
|
|
|
// Derive issuer from KEYCLOAK_URL + KEYCLOAK_REALM if KEYCLOAK_ISSUER not set
|
|
const issuer =
|
|
explicitIssuer ??
|
|
(keycloakUrl && keycloakRealm
|
|
? `${keycloakUrl.replace(/\/$/, '')}/realms/${keycloakRealm}`
|
|
: undefined);
|
|
|
|
const anySet = !!(issuer || clientId || clientSecret);
|
|
if (anySet && (!issuer || !clientId || !clientSecret)) {
|
|
throw new Error(
|
|
'@mosaicstack/auth: Keycloak SSO requires KEYCLOAK_CLIENT_ID, KEYCLOAK_CLIENT_SECRET, KEYCLOAK_ISSUER.',
|
|
);
|
|
}
|
|
if (!issuer || !clientId || !clientSecret) {
|
|
return null;
|
|
}
|
|
|
|
const normalizedIssuer = issuer.replace(/\/$/, '');
|
|
|
|
return {
|
|
providerId: 'keycloak',
|
|
issuer: normalizedIssuer,
|
|
clientId,
|
|
clientSecret,
|
|
discoveryUrl: toDiscoveryUrl(normalizedIssuer),
|
|
scopes: DEFAULT_SCOPES,
|
|
pkce: true,
|
|
requireIssuerValidation: true,
|
|
};
|
|
}
|
|
|
|
function collectWarnings(env: EnvMap, provider: SupportedSsoProviderId): string[] {
|
|
const prefix = provider.toUpperCase();
|
|
const oidcFields = [
|
|
`${prefix}_CLIENT_ID`,
|
|
`${prefix}_CLIENT_SECRET`,
|
|
`${prefix}_ISSUER`,
|
|
] as const;
|
|
const presentOidcFields = oidcFields.filter((field) => readEnv(env, field));
|
|
const warnings: string[] = [];
|
|
|
|
if (presentOidcFields.length > 0 && presentOidcFields.length < oidcFields.length) {
|
|
const missing = oidcFields.filter((field) => !readEnv(env, field));
|
|
warnings.push(`${provider} OIDC is partially configured. Missing: ${missing.join(', ')}`);
|
|
}
|
|
|
|
return warnings;
|
|
}
|
|
|
|
export function buildGenericOidcProviderConfigs(
|
|
env: EnvMap = process.env,
|
|
): GenericOidcProviderConfig[] {
|
|
return [buildAuthentikConfig(env), buildWorkosConfig(env), buildKeycloakConfig(env)].filter(
|
|
(config): config is GenericOidcProviderConfig => config !== null,
|
|
);
|
|
}
|
|
|
|
export function listSsoStartupWarnings(env: EnvMap = process.env): string[] {
|
|
return ['authentik', 'workos', 'keycloak'].flatMap((provider) =>
|
|
collectWarnings(env, provider as SupportedSsoProviderId),
|
|
);
|
|
}
|
|
|
|
export function buildSsoDiscovery(env: EnvMap = process.env): SsoProviderDiscovery[] {
|
|
const oidcConfigs = new Map(
|
|
buildGenericOidcProviderConfigs(env).map((config) => [config.providerId, config]),
|
|
);
|
|
const keycloakSamlLoginUrl = readEnv(env, 'KEYCLOAK_SAML_LOGIN_URL') ?? null;
|
|
|
|
return [
|
|
{
|
|
id: 'authentik',
|
|
name: 'Authentik',
|
|
protocols: ['oidc'],
|
|
configured: oidcConfigs.has('authentik'),
|
|
loginMode: oidcConfigs.has('authentik') ? 'oidc' : null,
|
|
callbackPath: oidcConfigs.has('authentik') ? '/api/auth/oauth2/callback/authentik' : null,
|
|
teamSync: getTeamSyncClaim(env, 'AUTHENTIK_TEAM_SYNC_CLAIM', 'groups'),
|
|
samlFallback: {
|
|
configured: false,
|
|
loginUrl: null,
|
|
},
|
|
warnings: collectWarnings(env, 'authentik'),
|
|
},
|
|
{
|
|
id: 'workos',
|
|
name: 'WorkOS',
|
|
protocols: ['oidc'],
|
|
configured: oidcConfigs.has('workos'),
|
|
loginMode: oidcConfigs.has('workos') ? 'oidc' : null,
|
|
callbackPath: oidcConfigs.has('workos') ? '/api/auth/oauth2/callback/workos' : null,
|
|
teamSync: getTeamSyncClaim(env, 'WORKOS_TEAM_SYNC_CLAIM', 'organization_id'),
|
|
samlFallback: {
|
|
configured: false,
|
|
loginUrl: null,
|
|
},
|
|
warnings: collectWarnings(env, 'workos'),
|
|
},
|
|
{
|
|
id: 'keycloak',
|
|
name: 'Keycloak',
|
|
protocols: ['oidc', 'saml'],
|
|
configured: oidcConfigs.has('keycloak') || keycloakSamlLoginUrl !== null,
|
|
loginMode: oidcConfigs.has('keycloak') ? 'oidc' : keycloakSamlLoginUrl ? 'saml' : null,
|
|
callbackPath: oidcConfigs.has('keycloak') ? '/api/auth/oauth2/callback/keycloak' : null,
|
|
teamSync: getTeamSyncClaim(env, 'KEYCLOAK_TEAM_SYNC_CLAIM', 'groups'),
|
|
samlFallback: {
|
|
configured: keycloakSamlLoginUrl !== null,
|
|
loginUrl: keycloakSamlLoginUrl,
|
|
},
|
|
warnings: collectWarnings(env, 'keycloak'),
|
|
},
|
|
];
|
|
}
|