251 lines
6.5 KiB
TypeScript
251 lines
6.5 KiB
TypeScript
|
|
/**
|
|||
|
|
* OIDC configuration enhancement and self-healing
|
|||
|
|
* Automatically fixes common configuration issues and provides helpful guidance
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
export interface OidcEnhancementResult {
|
|||
|
|
config: any;
|
|||
|
|
fixes: string[];
|
|||
|
|
warnings: string[];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Enhance OIDC configuration with smart defaults and auto-fixes
|
|||
|
|
*/
|
|||
|
|
export function enhanceOidcConfig(config: any): OidcEnhancementResult {
|
|||
|
|
const enhanced = { ...config };
|
|||
|
|
const fixes: string[] = [];
|
|||
|
|
const warnings: string[] = [];
|
|||
|
|
|
|||
|
|
// Auto-add groups scope if missing
|
|||
|
|
if (enhanced.scope && !enhanced.scope.includes('groups')) {
|
|||
|
|
enhanced.scope = enhanced.scope.trim() + ' groups';
|
|||
|
|
fixes.push('✓ Added "groups" to scope for role mapping');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Auto-detect optimal scope based on issuer
|
|||
|
|
if (!enhanced.scope && enhanced.issuer) {
|
|||
|
|
enhanced.scope = detectOptimalScope(enhanced.issuer);
|
|||
|
|
fixes.push(`✓ Auto-detected optimal scope: ${enhanced.scope}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Auto-generate redirect_uri if missing
|
|||
|
|
if (!enhanced.redirect_uri) {
|
|||
|
|
const baseUrl =
|
|||
|
|
process.env.PUBLIC_URL ||
|
|||
|
|
process.env.HEADPLANE_URL ||
|
|||
|
|
'http://localhost:3000';
|
|||
|
|
enhanced.redirect_uri = `${baseUrl}/admin/oidc/callback`;
|
|||
|
|
fixes.push(`✓ Auto-generated redirect_uri: ${enhanced.redirect_uri}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Provider-specific warnings and optimizations
|
|||
|
|
if (enhanced.issuer) {
|
|||
|
|
const providerInsights = getProviderInsights(enhanced.issuer);
|
|||
|
|
warnings.push(...providerInsights);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Environment variable guidance
|
|||
|
|
const envVars = getRecommendedEnvVars();
|
|||
|
|
if (envVars.length > 0) {
|
|||
|
|
warnings.push(
|
|||
|
|
'💡 For easier configuration, consider using environment variables:',
|
|||
|
|
);
|
|||
|
|
warnings.push(...envVars);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { config: enhanced, fixes, warnings };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Detect optimal OIDC scope based on identity provider
|
|||
|
|
*/
|
|||
|
|
function detectOptimalScope(issuer: string): string {
|
|||
|
|
const baseScope = 'openid email profile';
|
|||
|
|
|
|||
|
|
if (!issuer) return baseScope;
|
|||
|
|
|
|||
|
|
// Provider-specific optimizations
|
|||
|
|
if (issuer.includes('accounts.google.com')) {
|
|||
|
|
return baseScope; // Google Workspace needs special setup for groups
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (issuer.includes('login.microsoftonline.com')) {
|
|||
|
|
return baseScope + ' groups'; // Azure AD supports groups scope
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (issuer.includes('keycloak') || issuer.includes('auth.')) {
|
|||
|
|
return baseScope + ' groups roles'; // Keycloak supports both
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (issuer.includes('okta.com') || issuer.includes('oktapreview.com')) {
|
|||
|
|
return baseScope + ' groups'; // Okta supports groups scope
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (issuer.includes('auth0.com')) {
|
|||
|
|
return baseScope + ' groups'; // Auth0 supports groups scope
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Safe default for unknown providers
|
|||
|
|
return baseScope + ' groups';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Get provider-specific insights and configuration tips
|
|||
|
|
*/
|
|||
|
|
function getProviderInsights(issuer: string): string[] {
|
|||
|
|
const insights: string[] = [];
|
|||
|
|
|
|||
|
|
if (issuer.includes('accounts.google.com')) {
|
|||
|
|
insights.push(
|
|||
|
|
'ℹ️ Google: Groups require Google Workspace admin console configuration',
|
|||
|
|
);
|
|||
|
|
insights.push(
|
|||
|
|
' Consider using: HEADPLANE_ADMIN_GROUPS="admin@yourcompany.com"',
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (issuer.includes('login.microsoftonline.com')) {
|
|||
|
|
insights.push(
|
|||
|
|
'ℹ️ Azure AD: Add "groups" claim to token configuration in Azure portal',
|
|||
|
|
);
|
|||
|
|
insights.push(' May require GroupMember.Read.All API permission');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (issuer.includes('keycloak')) {
|
|||
|
|
insights.push(
|
|||
|
|
'ℹ️ Keycloak: Configure group membership mapper in client settings',
|
|||
|
|
);
|
|||
|
|
insights.push(' Groups appear in resource_access or realm_access claims');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (issuer.includes('okta.com')) {
|
|||
|
|
insights.push(
|
|||
|
|
'ℹ️ Okta: Enable group claims in authorization server settings',
|
|||
|
|
);
|
|||
|
|
insights.push(' Groups appear in standard "groups" claim');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (issuer.includes('auth0.com')) {
|
|||
|
|
insights.push('ℹ️ Auth0: Configure group claims in rules or actions');
|
|||
|
|
insights.push(' Groups appear in custom namespace or "groups" claim');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return insights;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Get recommended environment variables for easier configuration
|
|||
|
|
*/
|
|||
|
|
function getRecommendedEnvVars(): string[] {
|
|||
|
|
const hasEnvMapping = [
|
|||
|
|
'HEADPLANE_OWNER_GROUPS',
|
|||
|
|
'HEADPLANE_ADMIN_GROUPS',
|
|||
|
|
'HEADPLANE_NETWORK_ADMIN_GROUPS',
|
|||
|
|
'HEADPLANE_IT_ADMIN_GROUPS',
|
|||
|
|
'HEADPLANE_AUDITOR_GROUPS',
|
|||
|
|
].some((env) => process.env[env]);
|
|||
|
|
|
|||
|
|
if (hasEnvMapping) {
|
|||
|
|
return []; // Already using environment variables
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return [
|
|||
|
|
' HEADPLANE_ADMIN_GROUPS="admin,administrators,managers"',
|
|||
|
|
' HEADPLANE_OWNER_GROUPS="ceo,cto,founders"',
|
|||
|
|
' HEADPLANE_NETWORK_ADMIN_GROUPS="devops,network,sre"',
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Validate configuration and provide helpful error messages
|
|||
|
|
*/
|
|||
|
|
export function validateOidcConfig(config: any): {
|
|||
|
|
valid: boolean;
|
|||
|
|
errors: string[];
|
|||
|
|
} {
|
|||
|
|
const errors: string[] = [];
|
|||
|
|
|
|||
|
|
if (!config.issuer) {
|
|||
|
|
errors.push('❌ Missing issuer URL');
|
|||
|
|
errors.push(
|
|||
|
|
' Example: https://your-provider.com or https://login.microsoftonline.com/tenant/v2.0',
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!config.client_id) {
|
|||
|
|
errors.push('❌ Missing client_id');
|
|||
|
|
errors.push(' This should be provided by your identity provider');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!config.client_secret && !config.client_secret_path) {
|
|||
|
|
errors.push('❌ Missing client_secret or client_secret_path');
|
|||
|
|
errors.push(' For security, consider using client_secret_path');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// URL validation
|
|||
|
|
if (config.issuer && !isValidUrl(config.issuer)) {
|
|||
|
|
errors.push('❌ Invalid issuer URL format');
|
|||
|
|
errors.push(' Must be a valid HTTPS URL');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (config.redirect_uri && !isValidUrl(config.redirect_uri)) {
|
|||
|
|
errors.push('❌ Invalid redirect_uri format');
|
|||
|
|
errors.push(' Must be a valid HTTPS URL (or HTTP for localhost)');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
valid: errors.length === 0,
|
|||
|
|
errors,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function isValidUrl(urlString: string): boolean {
|
|||
|
|
try {
|
|||
|
|
const url = new URL(urlString);
|
|||
|
|
return (
|
|||
|
|
url.protocol === 'https:' ||
|
|||
|
|
(url.hostname === 'localhost' && url.protocol === 'http:')
|
|||
|
|
);
|
|||
|
|
} catch {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Create development OIDC configuration with mock provider
|
|||
|
|
*/
|
|||
|
|
export function createDevOidcConfig() {
|
|||
|
|
if (process.env.NODE_ENV !== 'development') {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
issuer: 'http://localhost:3001/dev-oidc',
|
|||
|
|
client_id: 'dev-headplane',
|
|||
|
|
client_secret: 'dev-secret',
|
|||
|
|
scope: 'openid email profile groups',
|
|||
|
|
redirect_uri: 'http://localhost:3000/admin/oidc/callback',
|
|||
|
|
mock_users: [
|
|||
|
|
{
|
|||
|
|
sub: 'dev-admin',
|
|||
|
|
email: 'admin@dev.local',
|
|||
|
|
name: 'Dev Admin',
|
|||
|
|
groups: ['admin', 'developers'],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
sub: 'dev-user',
|
|||
|
|
email: 'user@dev.local',
|
|||
|
|
name: 'Dev User',
|
|||
|
|
groups: ['developers'],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
sub: 'dev-owner',
|
|||
|
|
email: 'owner@dev.local',
|
|||
|
|
name: 'Dev Owner',
|
|||
|
|
groups: ['ceo', 'founders'],
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
};
|
|||
|
|
}
|