mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-03-19 02:22:11 +01:00
SSO login fix (#5167)
Fixes bug where SSO login with custom providers caused an `InvalidClientRegistrationIdException: Invalid Client Registration with Id: oidc` errors. Root Cause: - Backend: Redirect URI was hardcoded to `/login/oauth2/code/oidc` regardless of provider registration ID - Frontend: Unknown providers were mapped back to 'oidc' instead of using actual provider ID Closes #5141 --------- Co-authored-by: Anthony Stirling <77850077+frooodle@users.noreply.github.com> Co-authored-by: Keon Chen <66115421+keonchennl@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
9fd8fd89ed
commit
82dbcfbb9b
24
frontend/src/proprietary/auth/oauthTypes.ts
Normal file
24
frontend/src/proprietary/auth/oauthTypes.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Known OAuth providers with dedicated UI support.
|
||||
* Custom providers are also supported - the backend determines availability.
|
||||
*/
|
||||
export const KNOWN_OAUTH_PROVIDERS = [
|
||||
'github',
|
||||
'google',
|
||||
'apple',
|
||||
'azure',
|
||||
'keycloak',
|
||||
'cloudron',
|
||||
'authentik',
|
||||
'oidc',
|
||||
] as const;
|
||||
|
||||
export type KnownOAuthProvider = typeof KNOWN_OAUTH_PROVIDERS[number];
|
||||
|
||||
/**
|
||||
* OAuth provider ID - can be any known provider or custom string.
|
||||
* The backend configuration determines which providers are available.
|
||||
*
|
||||
* @example 'github' | 'google' | 'mycompany' | 'authentik'
|
||||
*/
|
||||
export type OAuthProvider = KnownOAuthProvider | (string & {});
|
||||
@@ -10,6 +10,7 @@
|
||||
import apiClient from '@app/services/apiClient';
|
||||
import { AxiosError } from 'axios';
|
||||
import { BASE_PATH } from '@app/constants/app';
|
||||
import { type OAuthProvider } from '@app/auth/oauthTypes';
|
||||
|
||||
// Helper to extract error message from axios error
|
||||
function getErrorMessage(error: unknown, fallback: string): string {
|
||||
@@ -248,11 +249,14 @@ class SpringAuthClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign in with OAuth provider (GitHub, Google, etc.)
|
||||
* Sign in with OAuth provider (GitHub, Google, Authentik, etc.)
|
||||
* This redirects to the Spring OAuth2 authorization endpoint
|
||||
*
|
||||
* @param params.provider - OAuth provider ID (e.g., 'github', 'google', 'authentik', 'mycompany')
|
||||
* Can be any known provider or custom string - the backend determines available providers
|
||||
*/
|
||||
async signInWithOAuth(params: {
|
||||
provider: 'github' | 'google' | 'apple' | 'azure' | 'keycloak' | 'oidc';
|
||||
provider: OAuthProvider;
|
||||
options?: { redirectTo?: string; queryParams?: Record<string, any> };
|
||||
}): Promise<{ error: AuthError | null }> {
|
||||
try {
|
||||
|
||||
@@ -7,6 +7,7 @@ import Login from '@app/routes/Login';
|
||||
import { useAuth } from '@app/auth/UseSession';
|
||||
import { springAuth } from '@app/auth/springAuthClient';
|
||||
import { PreferencesProvider } from '@app/contexts/PreferencesContext';
|
||||
import apiClient from '@app/services/apiClient';
|
||||
|
||||
// Mock i18n to return fallback text
|
||||
vi.mock('react-i18next', () => ({
|
||||
@@ -36,8 +37,13 @@ vi.mock('@app/hooks/useDocumentMeta', () => ({
|
||||
useDocumentMeta: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock fetch for provider list
|
||||
global.fetch = vi.fn();
|
||||
// Mock apiClient for provider list
|
||||
vi.mock('@app/services/apiClient', () => ({
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockNavigate = vi.fn();
|
||||
const mockBackendProbeState = {
|
||||
@@ -89,14 +95,13 @@ describe('Login', () => {
|
||||
refreshSession: vi.fn(),
|
||||
});
|
||||
|
||||
// Mock fetch for login UI data
|
||||
vi.mocked(fetch).mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
// Mock apiClient for login UI data
|
||||
vi.mocked(apiClient.get).mockResolvedValue({
|
||||
data: {
|
||||
enableLogin: true,
|
||||
providerList: {},
|
||||
}),
|
||||
} as Response);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should render login form', async () => {
|
||||
@@ -239,6 +244,136 @@ describe('Login', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should use actual provider ID for OAuth login (authentik)', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock provider list with authentik
|
||||
vi.mocked(apiClient.get).mockResolvedValue({
|
||||
data: {
|
||||
enableLogin: true,
|
||||
providerList: {
|
||||
'/oauth2/authorization/authentik': 'Authentik',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
vi.mocked(springAuth.signInWithOAuth).mockResolvedValueOnce({
|
||||
error: null,
|
||||
});
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<BrowserRouter>
|
||||
<Login />
|
||||
</BrowserRouter>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Wait for OAuth button to appear
|
||||
await waitFor(() => {
|
||||
const button = screen.queryByText('Authentik');
|
||||
expect(button).toBeTruthy();
|
||||
}, { timeout: 3000 });
|
||||
|
||||
const oauthButton = screen.getByText('Authentik');
|
||||
await user.click(oauthButton);
|
||||
|
||||
await waitFor(() => {
|
||||
// Should use 'authentik' directly, NOT map to 'oidc'
|
||||
expect(springAuth.signInWithOAuth).toHaveBeenCalledWith({
|
||||
provider: 'authentik',
|
||||
options: { redirectTo: '/auth/callback' }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should use actual provider ID for OAuth login (custom provider)', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock provider list with custom provider 'mycompany'
|
||||
vi.mocked(apiClient.get).mockResolvedValue({
|
||||
data: {
|
||||
enableLogin: true,
|
||||
providerList: {
|
||||
'/oauth2/authorization/mycompany': 'My Company SSO',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
vi.mocked(springAuth.signInWithOAuth).mockResolvedValueOnce({
|
||||
error: null,
|
||||
});
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<BrowserRouter>
|
||||
<Login />
|
||||
</BrowserRouter>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Wait for OAuth button to appear (will show 'Mycompany' as label)
|
||||
await waitFor(() => {
|
||||
const button = screen.queryByText('Mycompany');
|
||||
expect(button).toBeTruthy();
|
||||
}, { timeout: 3000 });
|
||||
|
||||
const oauthButton = screen.getByText('Mycompany');
|
||||
await user.click(oauthButton);
|
||||
|
||||
await waitFor(() => {
|
||||
// Should use 'mycompany' directly - this is the critical fix
|
||||
// Previously it would map unknown providers to 'oidc'
|
||||
expect(springAuth.signInWithOAuth).toHaveBeenCalledWith({
|
||||
provider: 'mycompany',
|
||||
options: { redirectTo: '/auth/callback' }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should use oidc provider ID when explicitly configured', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock provider list with 'oidc'
|
||||
vi.mocked(apiClient.get).mockResolvedValue({
|
||||
data: {
|
||||
enableLogin: true,
|
||||
providerList: {
|
||||
'/oauth2/authorization/oidc': 'OIDC',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
vi.mocked(springAuth.signInWithOAuth).mockResolvedValueOnce({
|
||||
error: null,
|
||||
});
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<BrowserRouter>
|
||||
<Login />
|
||||
</BrowserRouter>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Wait for OAuth button to appear
|
||||
await waitFor(() => {
|
||||
const button = screen.queryByText('OIDC');
|
||||
expect(button).toBeTruthy();
|
||||
}, { timeout: 3000 });
|
||||
|
||||
const oauthButton = screen.getByText('OIDC');
|
||||
await user.click(oauthButton);
|
||||
|
||||
await waitFor(() => {
|
||||
// Should use 'oidc' when explicitly configured
|
||||
expect(springAuth.signInWithOAuth).toHaveBeenCalledWith({
|
||||
provider: 'oidc',
|
||||
options: { redirectTo: '/auth/callback' }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error on failed login', async () => {
|
||||
const user = userEvent.setup();
|
||||
const errorMessage = 'Invalid credentials';
|
||||
@@ -359,13 +494,12 @@ describe('Login', () => {
|
||||
it('should redirect to home when login disabled', async () => {
|
||||
mockBackendProbeState.loginDisabled = true;
|
||||
mockProbe.mockResolvedValueOnce({ status: 'up', loginDisabled: true, loading: false });
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
vi.mocked(apiClient.get).mockResolvedValueOnce({
|
||||
data: {
|
||||
enableLogin: false,
|
||||
providerList: {},
|
||||
}),
|
||||
} as Response);
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
@@ -381,15 +515,14 @@ describe('Login', () => {
|
||||
});
|
||||
|
||||
it('should handle OAuth provider click', async () => {
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
vi.mocked(apiClient.get).mockResolvedValueOnce({
|
||||
data: {
|
||||
enableLogin: true,
|
||||
providerList: {
|
||||
'/oauth2/authorization/github': 'GitHub',
|
||||
},
|
||||
}),
|
||||
} as Response);
|
||||
},
|
||||
});
|
||||
|
||||
vi.mocked(springAuth.signInWithOAuth).mockResolvedValueOnce({
|
||||
error: null,
|
||||
@@ -416,13 +549,12 @@ describe('Login', () => {
|
||||
});
|
||||
|
||||
it('should show email form by default when no SSO providers', async () => {
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
vi.mocked(apiClient.get).mockResolvedValueOnce({
|
||||
data: {
|
||||
enableLogin: true,
|
||||
providerList: {}, // No providers
|
||||
}),
|
||||
} as Response);
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
|
||||
@@ -10,6 +10,7 @@ import AuthLayout from '@app/routes/authShared/AuthLayout';
|
||||
import { useBackendProbe } from '@app/hooks/useBackendProbe';
|
||||
import apiClient from '@app/services/apiClient';
|
||||
import { BASE_PATH } from '@app/constants/app';
|
||||
import { type OAuthProvider } from '@app/auth/oauthTypes';
|
||||
|
||||
// Import login components
|
||||
import LoginHeader from '@app/routes/login/LoginHeader';
|
||||
@@ -31,7 +32,7 @@ export default function Login() {
|
||||
const [showEmailForm, setShowEmailForm] = useState(false);
|
||||
const [email, setEmail] = useState(() => searchParams.get('email') ?? '');
|
||||
const [password, setPassword] = useState('');
|
||||
const [enabledProviders, setEnabledProviders] = useState<string[]>([]);
|
||||
const [enabledProviders, setEnabledProviders] = useState<OAuthProvider[]>([]);
|
||||
const [hasSSOProviders, setHasSSOProviders] = useState(false);
|
||||
const [_enableLogin, setEnableLogin] = useState<boolean | null>(null);
|
||||
const backendProbe = useBackendProbe();
|
||||
@@ -226,25 +227,17 @@ export default function Login() {
|
||||
);
|
||||
}
|
||||
|
||||
// Known OAuth providers that have dedicated backend support
|
||||
const KNOWN_OAUTH_PROVIDERS = ['github', 'google', 'apple', 'azure', 'keycloak', 'oidc'] as const;
|
||||
type KnownOAuthProvider = typeof KNOWN_OAUTH_PROVIDERS[number];
|
||||
|
||||
const signInWithProvider = async (provider: string) => {
|
||||
const signInWithProvider = async (provider: OAuthProvider) => {
|
||||
try {
|
||||
setIsSigningIn(true);
|
||||
setError(null);
|
||||
|
||||
// Map unknown providers to 'oidc' for the backend redirect
|
||||
const backendProvider: KnownOAuthProvider = KNOWN_OAUTH_PROVIDERS.includes(provider as KnownOAuthProvider)
|
||||
? (provider as KnownOAuthProvider)
|
||||
: 'oidc';
|
||||
console.log(`[Login] Signing in with provider: ${provider}`);
|
||||
|
||||
console.log(`[Login] Signing in with ${provider} (backend: ${backendProvider})`);
|
||||
|
||||
// Redirect to Spring OAuth2 endpoint
|
||||
// Redirect to Spring OAuth2 endpoint using the actual provider ID from backend
|
||||
// The backend returns the correct registration ID (e.g., 'authentik', 'oidc', 'keycloak')
|
||||
const { error } = await springAuth.signInWithOAuth({
|
||||
provider: backendProvider,
|
||||
provider: provider,
|
||||
options: { redirectTo: `${BASE_PATH}/auth/callback` }
|
||||
});
|
||||
|
||||
|
||||
291
frontend/src/proprietary/routes/login/OAuthButtons.test.tsx
Normal file
291
frontend/src/proprietary/routes/login/OAuthButtons.test.tsx
Normal file
@@ -0,0 +1,291 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import OAuthButtons from '@app/routes/login/OAuthButtons';
|
||||
|
||||
// Mock i18n
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, fallback?: string) => fallback || key,
|
||||
}),
|
||||
}));
|
||||
|
||||
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<MantineProvider>{children}</MantineProvider>
|
||||
);
|
||||
|
||||
describe('OAuthButtons', () => {
|
||||
const mockOnProviderClick = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render known providers with correct labels', () => {
|
||||
const enabledProviders = ['google', 'github', 'authentik'];
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<OAuthButtons
|
||||
onProviderClick={mockOnProviderClick}
|
||||
isSubmitting={false}
|
||||
enabledProviders={enabledProviders}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Check that known providers are rendered with their labels
|
||||
expect(screen.getByText('Google')).toBeTruthy();
|
||||
expect(screen.getByText('GitHub')).toBeTruthy();
|
||||
expect(screen.getByText('Authentik')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render unknown provider with capitalized label and generic icon', () => {
|
||||
const enabledProviders = ['mycompany'];
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<OAuthButtons
|
||||
onProviderClick={mockOnProviderClick}
|
||||
isSubmitting={false}
|
||||
enabledProviders={enabledProviders}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Unknown provider should be capitalized
|
||||
expect(screen.getByText('Mycompany')).toBeTruthy();
|
||||
|
||||
// Check that button has generic OIDC icon
|
||||
const button = screen.getByText('Mycompany').closest('button');
|
||||
expect(button).toBeTruthy();
|
||||
const img = button?.querySelector('img');
|
||||
expect(img?.src).toContain('oidc.svg');
|
||||
});
|
||||
|
||||
it('should call onProviderClick with actual provider ID (not "oidc")', async () => {
|
||||
const user = userEvent.setup();
|
||||
const enabledProviders = ['mycompany'];
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<OAuthButtons
|
||||
onProviderClick={mockOnProviderClick}
|
||||
isSubmitting={false}
|
||||
enabledProviders={enabledProviders}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const button = screen.getByText('Mycompany');
|
||||
await user.click(button);
|
||||
|
||||
// Should use actual provider ID 'mycompany', NOT 'oidc'
|
||||
expect(mockOnProviderClick).toHaveBeenCalledWith('mycompany');
|
||||
});
|
||||
|
||||
it('should call onProviderClick with "authentik" when authentik is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const enabledProviders = ['authentik'];
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<OAuthButtons
|
||||
onProviderClick={mockOnProviderClick}
|
||||
isSubmitting={false}
|
||||
enabledProviders={enabledProviders}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const button = screen.getByText('Authentik');
|
||||
await user.click(button);
|
||||
|
||||
expect(mockOnProviderClick).toHaveBeenCalledWith('authentik');
|
||||
});
|
||||
|
||||
it('should call onProviderClick with "oidc" when OIDC is explicitly configured', async () => {
|
||||
const user = userEvent.setup();
|
||||
const enabledProviders = ['oidc'];
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<OAuthButtons
|
||||
onProviderClick={mockOnProviderClick}
|
||||
isSubmitting={false}
|
||||
enabledProviders={enabledProviders}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const button = screen.getByText('OIDC');
|
||||
await user.click(button);
|
||||
|
||||
expect(mockOnProviderClick).toHaveBeenCalledWith('oidc');
|
||||
});
|
||||
|
||||
it('should disable buttons when isSubmitting is true', () => {
|
||||
const enabledProviders = ['google', 'github'];
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<OAuthButtons
|
||||
onProviderClick={mockOnProviderClick}
|
||||
isSubmitting={true}
|
||||
enabledProviders={enabledProviders}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const googleButton = screen.getByText('Google').closest('button') as HTMLButtonElement;
|
||||
const githubButton = screen.getByText('GitHub').closest('button') as HTMLButtonElement;
|
||||
|
||||
expect(googleButton.disabled).toBe(true);
|
||||
expect(githubButton.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should render nothing when no providers are enabled', () => {
|
||||
const { container } = render(
|
||||
<TestWrapper>
|
||||
<OAuthButtons
|
||||
onProviderClick={mockOnProviderClick}
|
||||
isSubmitting={false}
|
||||
enabledProviders={[]}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Should render null/nothing (excluding Mantine's style tags)
|
||||
const hasContent = Array.from(container.children).some(
|
||||
child => child.tagName.toLowerCase() !== 'style'
|
||||
);
|
||||
expect(hasContent).toBe(false);
|
||||
});
|
||||
|
||||
it('should render multiple unknown providers with correct IDs', async () => {
|
||||
const user = userEvent.setup();
|
||||
const enabledProviders = ['company1', 'company2', 'company3'];
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<OAuthButtons
|
||||
onProviderClick={mockOnProviderClick}
|
||||
isSubmitting={false}
|
||||
enabledProviders={enabledProviders}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// All should be capitalized
|
||||
expect(screen.getByText('Company1')).toBeTruthy();
|
||||
expect(screen.getByText('Company2')).toBeTruthy();
|
||||
expect(screen.getByText('Company3')).toBeTruthy();
|
||||
|
||||
// Click each and verify correct ID is passed
|
||||
await user.click(screen.getByText('Company1'));
|
||||
expect(mockOnProviderClick).toHaveBeenCalledWith('company1');
|
||||
|
||||
await user.click(screen.getByText('Company2'));
|
||||
expect(mockOnProviderClick).toHaveBeenCalledWith('company2');
|
||||
|
||||
await user.click(screen.getByText('Company3'));
|
||||
expect(mockOnProviderClick).toHaveBeenCalledWith('company3');
|
||||
});
|
||||
|
||||
it('should use correct icon for known providers', () => {
|
||||
const enabledProviders = ['google', 'github', 'authentik', 'keycloak'];
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<OAuthButtons
|
||||
onProviderClick={mockOnProviderClick}
|
||||
isSubmitting={false}
|
||||
enabledProviders={enabledProviders}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Check that each known provider has its specific icon
|
||||
const googleButton = screen.getByText('Google').closest('button');
|
||||
expect(googleButton?.querySelector('img')?.src).toContain('google.svg');
|
||||
|
||||
const githubButton = screen.getByText('GitHub').closest('button');
|
||||
expect(githubButton?.querySelector('img')?.src).toContain('github.svg');
|
||||
|
||||
const authentikButton = screen.getByText('Authentik').closest('button');
|
||||
expect(authentikButton?.querySelector('img')?.src).toContain('authentik.svg');
|
||||
|
||||
const keycloakButton = screen.getByText('Keycloak').closest('button');
|
||||
expect(keycloakButton?.querySelector('img')?.src).toContain('keycloak.svg');
|
||||
});
|
||||
|
||||
it('should handle mixed known and unknown providers', async () => {
|
||||
const user = userEvent.setup();
|
||||
const enabledProviders = ['google', 'mycompany', 'authentik', 'custom'];
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<OAuthButtons
|
||||
onProviderClick={mockOnProviderClick}
|
||||
isSubmitting={false}
|
||||
enabledProviders={enabledProviders}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Known providers with correct labels
|
||||
expect(screen.getByText('Google')).toBeTruthy();
|
||||
expect(screen.getByText('Authentik')).toBeTruthy();
|
||||
|
||||
// Unknown providers with capitalized labels
|
||||
expect(screen.getByText('Mycompany')).toBeTruthy();
|
||||
expect(screen.getByText('Custom')).toBeTruthy();
|
||||
|
||||
// Click each and verify IDs are preserved
|
||||
await user.click(screen.getByText('Google'));
|
||||
expect(mockOnProviderClick).toHaveBeenCalledWith('google');
|
||||
|
||||
await user.click(screen.getByText('Mycompany'));
|
||||
expect(mockOnProviderClick).toHaveBeenCalledWith('mycompany');
|
||||
|
||||
await user.click(screen.getByText('Authentik'));
|
||||
expect(mockOnProviderClick).toHaveBeenCalledWith('authentik');
|
||||
|
||||
await user.click(screen.getByText('Custom'));
|
||||
expect(mockOnProviderClick).toHaveBeenCalledWith('custom');
|
||||
});
|
||||
|
||||
it('should maintain provider ID consistency - critical for OAuth redirect', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// This test ensures the fix for GitHub issue #5141
|
||||
// The provider ID used in the button click MUST match the backend registration ID
|
||||
// Previously, unknown providers were mapped to 'oidc', breaking the OAuth flow
|
||||
|
||||
const enabledProviders = ['authentik', 'okta', 'auth0'];
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<OAuthButtons
|
||||
onProviderClick={mockOnProviderClick}
|
||||
isSubmitting={false}
|
||||
enabledProviders={enabledProviders}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Each provider should use its actual ID, not 'oidc'
|
||||
await user.click(screen.getByText('Authentik'));
|
||||
expect(mockOnProviderClick).toHaveBeenLastCalledWith('authentik');
|
||||
|
||||
await user.click(screen.getByText('Okta'));
|
||||
expect(mockOnProviderClick).toHaveBeenLastCalledWith('okta');
|
||||
|
||||
await user.click(screen.getByText('Auth0'));
|
||||
expect(mockOnProviderClick).toHaveBeenLastCalledWith('auth0');
|
||||
|
||||
// Verify none were called with 'oidc' instead of their actual ID
|
||||
expect(mockOnProviderClick).not.toHaveBeenCalledWith('oidc');
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BASE_PATH } from '@app/constants/app';
|
||||
import { type OAuthProvider } from '@app/auth/oauthTypes';
|
||||
|
||||
// Debug flag to show all providers for UI testing
|
||||
// Set to true to see all SSO options regardless of backend configuration
|
||||
@@ -22,10 +23,10 @@ export const oauthProviderConfig: Record<string, { label: string; file: string }
|
||||
const GENERIC_PROVIDER_ICON = 'oidc.svg';
|
||||
|
||||
interface OAuthButtonsProps {
|
||||
onProviderClick: (provider: string) => void
|
||||
onProviderClick: (provider: OAuthProvider) => void
|
||||
isSubmitting: boolean
|
||||
layout?: 'vertical' | 'grid' | 'icons'
|
||||
enabledProviders?: string[] // List of enabled provider IDs from backend
|
||||
enabledProviders?: OAuthProvider[] // List of enabled provider IDs from backend
|
||||
}
|
||||
|
||||
export default function OAuthButtons({ onProviderClick, isSubmitting, layout = 'vertical', enabledProviders = [] }: OAuthButtonsProps) {
|
||||
|
||||
Reference in New Issue
Block a user