feat(auth): Add external authentication (OAuth/OIDC & LDAP) with admin management, UI, and encryption support
This commit is contained in:
188
ts/api/handlers/oauth.api.ts
Normal file
188
ts/api/handlers/oauth.api.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* OAuth API handlers
|
||||
* Public endpoints for OAuth/OIDC and LDAP authentication flows
|
||||
*/
|
||||
|
||||
import type { IApiContext, IApiResponse } from '../router.ts';
|
||||
import { AuthProvider, PlatformSettings } from '../../models/index.ts';
|
||||
import { externalAuthService } from '../../services/external.auth.service.ts';
|
||||
|
||||
export class OAuthApi {
|
||||
/**
|
||||
* GET /api/v1/auth/providers
|
||||
* List active authentication providers (public info only)
|
||||
*/
|
||||
public async listProviders(ctx: IApiContext): Promise<IApiResponse> {
|
||||
try {
|
||||
const settings = await PlatformSettings.get();
|
||||
const providers = await AuthProvider.getActiveProviders();
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
providers: providers.map((p) => p.toPublicInfo()),
|
||||
localAuthEnabled: settings.auth.localAuthEnabled,
|
||||
defaultProviderId: settings.auth.defaultProviderId,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[OAuthApi] List providers error:', error);
|
||||
return {
|
||||
status: 500,
|
||||
body: { error: 'Failed to list providers' },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/auth/oauth/:id/authorize
|
||||
* Initiate OAuth flow - redirects to provider
|
||||
*/
|
||||
public async authorize(ctx: IApiContext): Promise<IApiResponse> {
|
||||
try {
|
||||
const { id } = ctx.params;
|
||||
const returnUrl = ctx.url.searchParams.get('returnUrl') || undefined;
|
||||
|
||||
const { authUrl } = await externalAuthService.initiateOAuth(id, returnUrl);
|
||||
|
||||
// Return redirect response
|
||||
return {
|
||||
status: 302,
|
||||
headers: { Location: authUrl },
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[OAuthApi] Authorize error:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Authorization failed';
|
||||
return {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: `/login?error=${encodeURIComponent(errorMessage)}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/auth/oauth/:id/callback
|
||||
* Handle OAuth callback from provider
|
||||
*/
|
||||
public async callback(ctx: IApiContext): Promise<IApiResponse> {
|
||||
try {
|
||||
const code = ctx.url.searchParams.get('code');
|
||||
const state = ctx.url.searchParams.get('state');
|
||||
const error = ctx.url.searchParams.get('error');
|
||||
const errorDescription = ctx.url.searchParams.get('error_description');
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: `/login?error=${encodeURIComponent(errorDescription || error)}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!code || !state) {
|
||||
return {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: '/login?error=missing_parameters',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const result = await externalAuthService.handleOAuthCallback(
|
||||
{ code, state },
|
||||
{ ipAddress: ctx.ip, userAgent: ctx.userAgent }
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: `/login?error=${encodeURIComponent(result.errorCode || 'auth_failed')}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Redirect to OAuth callback page with tokens
|
||||
const params = new URLSearchParams({
|
||||
accessToken: result.accessToken!,
|
||||
refreshToken: result.refreshToken!,
|
||||
sessionId: result.sessionId!,
|
||||
});
|
||||
|
||||
return {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: `/oauth-callback?${params.toString()}`,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[OAuthApi] Callback error:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Callback failed';
|
||||
return {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: `/login?error=${encodeURIComponent(errorMessage)}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/auth/ldap/:id/login
|
||||
* LDAP authentication with username/password
|
||||
*/
|
||||
public async ldapLogin(ctx: IApiContext): Promise<IApiResponse> {
|
||||
try {
|
||||
const { id } = ctx.params;
|
||||
const body = await ctx.request.json();
|
||||
const { username, password } = body;
|
||||
|
||||
if (!username || !password) {
|
||||
return {
|
||||
status: 400,
|
||||
body: { error: 'Username and password are required' },
|
||||
};
|
||||
}
|
||||
|
||||
const result = await externalAuthService.authenticateLdap(id, username, password, {
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
status: 401,
|
||||
body: {
|
||||
error: result.errorMessage,
|
||||
code: result.errorCode,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
user: {
|
||||
id: result.user!.id,
|
||||
email: result.user!.email,
|
||||
username: result.user!.username,
|
||||
displayName: result.user!.displayName,
|
||||
isSystemAdmin: result.user!.isSystemAdmin,
|
||||
},
|
||||
accessToken: result.accessToken,
|
||||
refreshToken: result.refreshToken,
|
||||
sessionId: result.sessionId,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[OAuthApi] LDAP login error:', error);
|
||||
return {
|
||||
status: 500,
|
||||
body: { error: 'LDAP login failed' },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user