diff --git a/docs/configuration/oauth-sso.mdx b/docs/configuration/oauth-sso.mdx index be9f97b..8488bb4 100644 --- a/docs/configuration/oauth-sso.mdx +++ b/docs/configuration/oauth-sso.mdx @@ -63,7 +63,7 @@ Add the `oauthSSO` section to your `mcp_settings.json` under `systemConfig`: 2. Create a new project or select existing one 3. Navigate to "APIs & Services" → "Credentials" 4. Create OAuth 2.0 Client ID (Web application) -5. Add authorized redirect URI: `https://your-domain/api/auth/sso/google/callback` +5. Add authorized redirect URI: `https://your-domain/auth/sso/google/callback` 6. Copy Client ID and Client Secret ```json @@ -80,7 +80,7 @@ Add the `oauthSSO` section to your `mcp_settings.json` under `systemConfig`: 1. Go to [GitHub Developer Settings](https://github.com/settings/developers) 2. Click "New OAuth App" -3. Set Authorization callback URL: `https://your-domain/api/auth/sso/github/callback` +3. Set Authorization callback URL: `https://your-domain/auth/sso/github/callback` 4. Copy Client ID and generate Client Secret ```json @@ -97,7 +97,7 @@ Add the `oauthSSO` section to your `mcp_settings.json` under `systemConfig`: 1. Go to [Azure Portal](https://portal.azure.com/) → Azure Active Directory 2. Navigate to "App registrations" → "New registration" -3. Add redirect URI: `https://your-domain/api/auth/sso/microsoft/callback` +3. Add redirect URI: `https://your-domain/auth/sso/microsoft/callback` 4. Under "Certificates & secrets", create a new client secret 5. Copy Application (client) ID and client secret value diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 28e9cce..78f759d 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -31,38 +31,59 @@ const sanitizeReturnUrl = (value: string | null): string | null => { }; // Provider icons (SVG) -const ProviderIcon: React.FC<{ type: string; className?: string }> = ({ type, className = 'w-5 h-5' }) => { +const ProviderIcon: React.FC<{ type: string; className?: string }> = ({ + type, + className = 'w-5 h-5', +}) => { switch (type) { case 'google': return ( - - - - + + + + ); case 'github': return ( - + ); case 'microsoft': return ( - - - - + + + + ); default: // Generic OAuth/OIDC icon return ( - - - + + + ); } @@ -251,37 +272,6 @@ const LoginPage: React.FC = () => { - {/* SSO Buttons */} - {ssoEnabled && ssoProviders.length > 0 && ( - - {ssoProviders.map((provider) => ( - handleSSOLogin(provider.id)} - className="sso-button group relative flex w-full items-center justify-center gap-3 rounded-md border border-gray-300/60 bg-white/80 px-4 py-2.5 text-sm font-medium text-gray-700 shadow-sm transition-all hover:bg-gray-50 hover:border-gray-400/60 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:border-gray-600/60 dark:bg-gray-800/80 dark:text-gray-200 dark:hover:bg-gray-700/80" - > - - {t('auth.continueWith', { provider: provider.name })} - - ))} - - {/* Divider - only show if local auth is also allowed */} - {allowLocalAuth && ( - - - - - - - {t('auth.orContinueWith')} - - - - )} - - )} - {/* Local auth form - only show if allowed */} {allowLocalAuth && ( @@ -338,6 +328,34 @@ const LoginPage: React.FC = () => { )} + {/* SSO Buttons */} + {ssoEnabled && ssoProviders.length > 0 && ( + + {/* Divider */} + + + + + + + {t('auth.orContinueWith')} + + + + {ssoProviders.map((provider) => ( + handleSSOLogin(provider.id)} + className="sso-button group relative flex w-full items-center justify-center gap-3 rounded-md border border-gray-300/60 bg-white/80 px-4 py-2.5 text-sm font-medium text-gray-700 shadow-sm transition-all hover:bg-gray-50 hover:border-gray-400/60 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:border-gray-600/60 dark:bg-gray-800/80 dark:text-gray-200 dark:hover:bg-gray-700/80" + > + + {t('auth.continueWith', { provider: provider.name })} + + ))} + + )} + {/* Show message if only SSO is available and no providers configured */} {!allowLocalAuth && ssoProviders.length === 0 && ( diff --git a/frontend/src/services/authService.ts b/frontend/src/services/authService.ts index 46574bc..35e2332 100644 --- a/frontend/src/services/authService.ts +++ b/frontend/src/services/authService.ts @@ -1,3 +1,4 @@ +import { getBasePath } from '@/utils/runtime'; import { AuthResponse, LoginCredentials, @@ -5,7 +6,7 @@ import { ChangePasswordCredentials, SSOConfig, } from '../types'; -import { apiPost, apiGet } from '../utils/fetchInterceptor'; +import { apiPost, apiGet, fetchWithInterceptors } from '../utils/fetchInterceptor'; import { getToken, setToken, removeToken } from '../utils/interceptors'; // Export token management functions @@ -14,9 +15,17 @@ export { getToken, setToken, removeToken }; // Get SSO configuration export const getSSOConfig = async (): Promise => { try { - const response = await apiGet<{ success: boolean; data: SSOConfig }>('/auth/sso/config'); - if (response.success && response.data) { - return response.data; + const basePath = getBasePath(); + // const response = await apiGet<{ success: boolean; data: SSOConfig }>('/auth/sso/config'); + const response = await fetchWithInterceptors(`${basePath}/auth/sso/config`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + if (response.ok) { + const data: { success: boolean; data: SSOConfig } = await response.json(); + return data.data; } return { enabled: false, providers: [], allowLocalAuth: true }; } catch (error) { @@ -28,7 +37,7 @@ export const getSSOConfig = async (): Promise => { // Initiate SSO login (redirects to provider) export const initiateSSOLogin = (providerId: string, returnUrl?: string): void => { const basePath = import.meta.env.VITE_API_BASE_PATH || ''; - let url = `${basePath}/api/auth/sso/${providerId}`; + let url = `${basePath}/auth/sso/${providerId}`; if (returnUrl) { url += `?returnUrl=${encodeURIComponent(returnUrl)}`; } diff --git a/mcp_settings.json b/mcp_settings.json index 9a2bcbd..4cc50b9 100644 --- a/mcp_settings.json +++ b/mcp_settings.json @@ -43,6 +43,34 @@ } ], "systemConfig": { + "oauthSSO": { + "enabled": true, + "allowLocalAuth": true, + "callbackBaseUrl": "https://your-mcphub-domain.com", + "providers": [ + { + "id": "google", + "name": "Google", + "type": "google", + "clientId": "your-google-client-id", + "clientSecret": "your-google-client-secret" + }, + { + "id": "github", + "name": "GitHub", + "type": "github", + "clientId": "your-github-client-id", + "clientSecret": "your-github-client-secret" + }, + { + "id": "microsoft", + "name": "Microsoft", + "type": "microsoft", + "clientId": "your-microsoft-client-id", + "clientSecret": "your-microsoft-client-secret" + } + ] + }, "oauthServer": { "enabled": true, "accessTokenLifetime": 3600, diff --git a/src/controllers/oauthSSOController.ts b/src/controllers/oauthSSOController.ts index fe8200b..23e9c2f 100644 --- a/src/controllers/oauthSSOController.ts +++ b/src/controllers/oauthSSOController.ts @@ -66,9 +66,8 @@ export const initiateSSOLogin = async (req: Request, res: Response): Promise { ); // OAuth SSO routes (no auth required - public endpoints) - router.get('/auth/sso/config', getSSOConfig); // Get SSO configuration for frontend - router.get('/auth/sso/:provider', initiateSSOLogin); // Initiate SSO login - router.get('/auth/sso/:provider/callback', handleSSOCallback); // Handle OAuth callback + app.get(`${config.basePath}/auth/sso/config`, getSSOConfig); // Get SSO configuration for frontend + app.get(`${config.basePath}/auth/sso/:provider`, initiateSSOLogin); // Initiate SSO login + app.get(`${config.basePath}/auth/sso/:provider/callback`, handleSSOCallback); // Handle OAuth callback // Runtime configuration endpoint (no auth required for frontend initialization) app.get(`${config.basePath}/config`, getRuntimeConfig);