mirror of
https://github.com/samanhappy/mcphub.git
synced 2026-01-01 12:18:39 -05:00
feat: Add OAuth 2.0 / OIDC SSO login support
- Add OAuth SSO type definitions (OAuthSSOProvider, OAuthSSOConfig, IOAuthLink) - Add oauthSSO field to SystemConfig for provider configuration - Update IUser interface to support OAuth-linked accounts - Create OAuth SSO service with provider management and token exchange - Add SSO controller with login initiation and callback handling - Update frontend login page with SSO provider buttons - Add SSOCallbackPage for handling OAuth redirects - Update database entities and DAOs for OAuth link storage - Add i18n translations for SSO-related UI elements - Add comprehensive unit tests for OAuth SSO service Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
This commit is contained in:
393
tests/services/oauthSSOService.test.ts
Normal file
393
tests/services/oauthSSOService.test.ts
Normal file
@@ -0,0 +1,393 @@
|
||||
// Tests for OAuth SSO Service
|
||||
|
||||
import {
|
||||
isOAuthSSOEnabled,
|
||||
isLocalAuthAllowed,
|
||||
getEnabledProviders,
|
||||
getProviderById,
|
||||
generateAuthorizationUrl,
|
||||
} from '../../src/services/oauthSSOService.js';
|
||||
|
||||
// Mock the config loading
|
||||
jest.mock('../../src/config/index.js', () => ({
|
||||
loadSettings: jest.fn(),
|
||||
}));
|
||||
|
||||
import { loadSettings } from '../../src/config/index.js';
|
||||
|
||||
const mockLoadSettings = loadSettings as jest.MockedFunction<typeof loadSettings>;
|
||||
|
||||
describe('OAuth SSO Service', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('isOAuthSSOEnabled', () => {
|
||||
it('should return false when oauthSSO is not configured', () => {
|
||||
mockLoadSettings.mockReturnValue({
|
||||
mcpServers: {},
|
||||
systemConfig: {},
|
||||
});
|
||||
|
||||
expect(isOAuthSSOEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when oauthSSO.enabled is false', () => {
|
||||
mockLoadSettings.mockReturnValue({
|
||||
mcpServers: {},
|
||||
systemConfig: {
|
||||
oauthSSO: {
|
||||
enabled: false,
|
||||
providers: [
|
||||
{
|
||||
id: 'google',
|
||||
name: 'Google',
|
||||
type: 'google',
|
||||
clientId: 'test-client-id',
|
||||
clientSecret: 'test-client-secret',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(isOAuthSSOEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when no providers are configured', () => {
|
||||
mockLoadSettings.mockReturnValue({
|
||||
mcpServers: {},
|
||||
systemConfig: {
|
||||
oauthSSO: {
|
||||
enabled: true,
|
||||
providers: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(isOAuthSSOEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when enabled and providers exist', () => {
|
||||
mockLoadSettings.mockReturnValue({
|
||||
mcpServers: {},
|
||||
systemConfig: {
|
||||
oauthSSO: {
|
||||
enabled: true,
|
||||
providers: [
|
||||
{
|
||||
id: 'google',
|
||||
name: 'Google',
|
||||
type: 'google',
|
||||
clientId: 'test-client-id',
|
||||
clientSecret: 'test-client-secret',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(isOAuthSSOEnabled()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLocalAuthAllowed', () => {
|
||||
it('should return true by default when not configured', () => {
|
||||
mockLoadSettings.mockReturnValue({
|
||||
mcpServers: {},
|
||||
systemConfig: {},
|
||||
});
|
||||
|
||||
expect(isLocalAuthAllowed()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when allowLocalAuth is not explicitly set', () => {
|
||||
mockLoadSettings.mockReturnValue({
|
||||
mcpServers: {},
|
||||
systemConfig: {
|
||||
oauthSSO: {
|
||||
enabled: true,
|
||||
providers: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(isLocalAuthAllowed()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when allowLocalAuth is false', () => {
|
||||
mockLoadSettings.mockReturnValue({
|
||||
mcpServers: {},
|
||||
systemConfig: {
|
||||
oauthSSO: {
|
||||
enabled: true,
|
||||
allowLocalAuth: false,
|
||||
providers: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(isLocalAuthAllowed()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when allowLocalAuth is true', () => {
|
||||
mockLoadSettings.mockReturnValue({
|
||||
mcpServers: {},
|
||||
systemConfig: {
|
||||
oauthSSO: {
|
||||
enabled: true,
|
||||
allowLocalAuth: true,
|
||||
providers: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(isLocalAuthAllowed()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEnabledProviders', () => {
|
||||
it('should return empty array when SSO is not enabled', () => {
|
||||
mockLoadSettings.mockReturnValue({
|
||||
mcpServers: {},
|
||||
systemConfig: {},
|
||||
});
|
||||
|
||||
expect(getEnabledProviders()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return only enabled providers', () => {
|
||||
mockLoadSettings.mockReturnValue({
|
||||
mcpServers: {},
|
||||
systemConfig: {
|
||||
oauthSSO: {
|
||||
enabled: true,
|
||||
providers: [
|
||||
{
|
||||
id: 'google',
|
||||
name: 'Google',
|
||||
type: 'google',
|
||||
clientId: 'test-client-id',
|
||||
clientSecret: 'test-client-secret',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'github',
|
||||
name: 'GitHub',
|
||||
type: 'github',
|
||||
clientId: 'test-client-id',
|
||||
clientSecret: 'test-client-secret',
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
id: 'microsoft',
|
||||
name: 'Microsoft',
|
||||
type: 'microsoft',
|
||||
clientId: 'test-client-id',
|
||||
clientSecret: 'test-client-secret',
|
||||
// enabled is undefined, defaults to true
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const providers = getEnabledProviders();
|
||||
expect(providers).toHaveLength(2);
|
||||
expect(providers[0]).toEqual({ id: 'google', name: 'Google', type: 'google' });
|
||||
expect(providers[1]).toEqual({ id: 'microsoft', name: 'Microsoft', type: 'microsoft' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProviderById', () => {
|
||||
it('should return undefined when provider not found', () => {
|
||||
mockLoadSettings.mockReturnValue({
|
||||
mcpServers: {},
|
||||
systemConfig: {
|
||||
oauthSSO: {
|
||||
enabled: true,
|
||||
providers: [
|
||||
{
|
||||
id: 'google',
|
||||
name: 'Google',
|
||||
type: 'google',
|
||||
clientId: 'test-client-id',
|
||||
clientSecret: 'test-client-secret',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getProviderById('github')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when provider is disabled', () => {
|
||||
mockLoadSettings.mockReturnValue({
|
||||
mcpServers: {},
|
||||
systemConfig: {
|
||||
oauthSSO: {
|
||||
enabled: true,
|
||||
providers: [
|
||||
{
|
||||
id: 'google',
|
||||
name: 'Google',
|
||||
type: 'google',
|
||||
clientId: 'test-client-id',
|
||||
clientSecret: 'test-client-secret',
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getProviderById('google')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return provider when found and enabled', () => {
|
||||
const provider = {
|
||||
id: 'google',
|
||||
name: 'Google',
|
||||
type: 'google' as const,
|
||||
clientId: 'test-client-id',
|
||||
clientSecret: 'test-client-secret',
|
||||
};
|
||||
|
||||
mockLoadSettings.mockReturnValue({
|
||||
mcpServers: {},
|
||||
systemConfig: {
|
||||
oauthSSO: {
|
||||
enabled: true,
|
||||
providers: [provider],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getProviderById('google')).toEqual(provider);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateAuthorizationUrl', () => {
|
||||
it('should return null when provider not found', () => {
|
||||
mockLoadSettings.mockReturnValue({
|
||||
mcpServers: {},
|
||||
systemConfig: {
|
||||
oauthSSO: {
|
||||
enabled: true,
|
||||
providers: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(generateAuthorizationUrl('google', 'http://localhost/callback')).toBeNull();
|
||||
});
|
||||
|
||||
it('should generate authorization URL for Google provider', () => {
|
||||
mockLoadSettings.mockReturnValue({
|
||||
mcpServers: {},
|
||||
systemConfig: {
|
||||
oauthSSO: {
|
||||
enabled: true,
|
||||
providers: [
|
||||
{
|
||||
id: 'google',
|
||||
name: 'Google',
|
||||
type: 'google',
|
||||
clientId: 'test-client-id',
|
||||
clientSecret: 'test-client-secret',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = generateAuthorizationUrl('google', 'http://localhost/callback');
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.url).toContain('https://accounts.google.com/o/oauth2/v2/auth');
|
||||
expect(result!.url).toContain('client_id=test-client-id');
|
||||
expect(result!.url).toContain('redirect_uri=http%3A%2F%2Flocalhost%2Fcallback');
|
||||
expect(result!.url).toContain('response_type=code');
|
||||
expect(result!.url).toContain('scope=openid+email+profile');
|
||||
expect(result!.url).toContain('code_challenge=');
|
||||
expect(result!.state).toBeDefined();
|
||||
});
|
||||
|
||||
it('should generate authorization URL for GitHub provider without PKCE', () => {
|
||||
mockLoadSettings.mockReturnValue({
|
||||
mcpServers: {},
|
||||
systemConfig: {
|
||||
oauthSSO: {
|
||||
enabled: true,
|
||||
providers: [
|
||||
{
|
||||
id: 'github',
|
||||
name: 'GitHub',
|
||||
type: 'github',
|
||||
clientId: 'test-client-id',
|
||||
clientSecret: 'test-client-secret',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = generateAuthorizationUrl('github', 'http://localhost/callback');
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.url).toContain('https://github.com/login/oauth/authorize');
|
||||
expect(result!.url).not.toContain('code_challenge=');
|
||||
expect(result!.state).toBeDefined();
|
||||
});
|
||||
|
||||
it('should generate authorization URL for Microsoft provider', () => {
|
||||
mockLoadSettings.mockReturnValue({
|
||||
mcpServers: {},
|
||||
systemConfig: {
|
||||
oauthSSO: {
|
||||
enabled: true,
|
||||
providers: [
|
||||
{
|
||||
id: 'microsoft',
|
||||
name: 'Microsoft',
|
||||
type: 'microsoft',
|
||||
clientId: 'test-client-id',
|
||||
clientSecret: 'test-client-secret',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = generateAuthorizationUrl('microsoft', 'http://localhost/callback');
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.url).toContain('https://login.microsoftonline.com/common/oauth2/v2.0/authorize');
|
||||
expect(result!.url).toContain('code_challenge=');
|
||||
expect(result!.state).toBeDefined();
|
||||
});
|
||||
|
||||
it('should include custom scopes when configured', () => {
|
||||
mockLoadSettings.mockReturnValue({
|
||||
mcpServers: {},
|
||||
systemConfig: {
|
||||
oauthSSO: {
|
||||
enabled: true,
|
||||
providers: [
|
||||
{
|
||||
id: 'google',
|
||||
name: 'Google',
|
||||
type: 'google',
|
||||
clientId: 'test-client-id',
|
||||
clientSecret: 'test-client-secret',
|
||||
scopes: ['custom-scope', 'another-scope'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = generateAuthorizationUrl('google', 'http://localhost/callback');
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.url).toContain('scope=custom-scope+another-scope');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user