diff --git a/apps/iframe-app/src/components/IframeWrapper.tsx b/apps/iframe-app/src/components/IframeWrapper.tsx
index 599eb724cb0..23bc39d713f 100644
--- a/apps/iframe-app/src/components/IframeWrapper.tsx
+++ b/apps/iframe-app/src/components/IframeWrapper.tsx
@@ -95,10 +95,31 @@ export function IframeWrapper({ config }: IframeWrapperProps) {
}
try {
- const { isAuthenticated, username } = await checkAuthStatus(baseUrl);
- setNeedsLogin(!isAuthenticated);
- if (isAuthenticated && username) {
- setUserName(username);
+ const { isAuthenticated, isEasyAuthConfigured, username } = await checkAuthStatus(baseUrl);
+
+ // Easy Auth is NOT configured (404 from /.auth/me)
+ // Let it fail naturally - the API calls will fail without apiKey
+ if (!isEasyAuthConfigured) {
+ setNeedsLogin(false);
+ return;
+ }
+
+ // Easy Auth IS configured
+ // Check if identity providers are configured
+ const hasIdentityProviders = props.identityProviders && Object.keys(props.identityProviders).length > 0;
+
+ if (!hasIdentityProviders) {
+ // No identity providers configured - skip login, show chat directly
+ setNeedsLogin(false);
+ } else if (isAuthenticated) {
+ // User is already authenticated
+ setNeedsLogin(false);
+ if (username) {
+ setUserName(username);
+ }
+ } else {
+ // Identity providers configured but user not authenticated - show login
+ setNeedsLogin(true);
}
} catch (error) {
console.error('[Auth] Failed to check authentication status:', error);
@@ -109,7 +130,7 @@ export function IframeWrapper({ config }: IframeWrapperProps) {
};
checkAuth();
- }, [baseUrl, inPortal, apiKey]);
+ }, [baseUrl, inPortal, apiKey, props.identityProviders]);
// Frame Blade integration
const { isReady: isFrameBladeReady } = useFrameBlade({
diff --git a/apps/iframe-app/src/components/__tests__/IframeWrapper.contextId.test.tsx b/apps/iframe-app/src/components/__tests__/IframeWrapper.contextId.test.tsx
index 4edaa677a51..30ba39ada5e 100644
--- a/apps/iframe-app/src/components/__tests__/IframeWrapper.contextId.test.tsx
+++ b/apps/iframe-app/src/components/__tests__/IframeWrapper.contextId.test.tsx
@@ -20,7 +20,7 @@ vi.mock('../MultiSessionChat/MultiSessionChat', () => ({
vi.mock('../../lib/authHandler', () => ({
createUnauthorizedHandler: vi.fn(() => vi.fn()),
getBaseUrl: vi.fn((agentCard) => `https://base.url.from/${agentCard}`),
- checkAuthStatus: vi.fn(() => Promise.resolve({ isAuthenticated: true, error: null })),
+ checkAuthStatus: vi.fn(() => Promise.resolve({ isAuthenticated: true, isEasyAuthConfigured: true, error: null })),
openLoginPopup: vi.fn(),
}));
@@ -67,7 +67,7 @@ describe('IframeWrapper - contextId support', () => {
vi.clearAllMocks();
// Reset checkAuthStatus to return authenticated
- vi.mocked(authHandler.checkAuthStatus).mockResolvedValue({ isAuthenticated: true, error: null });
+ vi.mocked(authHandler.checkAuthStatus).mockResolvedValue({ isAuthenticated: true, isEasyAuthConfigured: true, error: null });
});
afterEach(() => {
diff --git a/apps/iframe-app/src/components/__tests__/IframeWrapper.test.tsx b/apps/iframe-app/src/components/__tests__/IframeWrapper.test.tsx
index dd263b7abd5..937ecd46092 100644
--- a/apps/iframe-app/src/components/__tests__/IframeWrapper.test.tsx
+++ b/apps/iframe-app/src/components/__tests__/IframeWrapper.test.tsx
@@ -20,7 +20,7 @@ vi.mock('../MultiSessionChat/MultiSessionChat', () => ({
vi.mock('../../lib/authHandler', () => ({
createUnauthorizedHandler: vi.fn(() => vi.fn()),
getBaseUrl: vi.fn((agentCard) => `https://base.url.from/${agentCard}`),
- checkAuthStatus: vi.fn(() => Promise.resolve({ isAuthenticated: true, error: null })),
+ checkAuthStatus: vi.fn(() => Promise.resolve({ isAuthenticated: true, isEasyAuthConfigured: true, error: null })),
openLoginPopup: vi.fn(),
}));
@@ -66,8 +66,8 @@ describe('IframeWrapper', () => {
// Reset mocks - this clears call history
vi.clearAllMocks();
- // Reset checkAuthStatus to default (authenticated)
- vi.mocked(authHandler.checkAuthStatus).mockResolvedValue({ isAuthenticated: true, error: null });
+ // Reset checkAuthStatus to default (authenticated with Easy Auth)
+ vi.mocked(authHandler.checkAuthStatus).mockResolvedValue({ isAuthenticated: true, isEasyAuthConfigured: true, error: null });
// Clear localStorage
localStorage.clear();
@@ -343,8 +343,8 @@ describe('IframeWrapper', () => {
describe('Authentication', () => {
it('should show loading state during authentication check', async () => {
// Create a promise that we can control
- let resolveAuth: (value: { isAuthenticated: boolean; error: null }) => void;
- const authPromise = new Promise<{ isAuthenticated: boolean; error: null }>((resolve) => {
+ let resolveAuth: (value: { isAuthenticated: boolean; isEasyAuthConfigured: boolean; error: null }) => void;
+ const authPromise = new Promise<{ isAuthenticated: boolean; isEasyAuthConfigured: boolean; error: null }>((resolve) => {
resolveAuth = resolve;
});
@@ -358,15 +358,15 @@ describe('IframeWrapper', () => {
// Resolve auth check
await act(async () => {
- resolveAuth!({ isAuthenticated: true, error: null });
+ resolveAuth!({ isAuthenticated: true, isEasyAuthConfigured: true, error: null });
});
// Should now show the chat widget
await screen.findByTestId('chat-widget');
});
- it('should show LoginPrompt when checkAuthStatus returns false', async () => {
- vi.mocked(authHandler.checkAuthStatus).mockResolvedValue({ isAuthenticated: false, error: null });
+ it('should show LoginPrompt when checkAuthStatus returns false and Easy Auth is configured', async () => {
+ vi.mocked(authHandler.checkAuthStatus).mockResolvedValue({ isAuthenticated: false, isEasyAuthConfigured: true, error: null });
const configWithProviders: IframeConfig = {
...defaultConfig,
@@ -389,7 +389,7 @@ describe('IframeWrapper', () => {
expect(screen.getByRole('button', { name: 'Microsoft account' })).toBeInTheDocument();
});
- it('should show LoginPrompt when checkAuthStatus throws an error', async () => {
+ it('should show login when checkAuthStatus throws an error and identity providers are configured', async () => {
vi.mocked(authHandler.checkAuthStatus).mockRejectedValue(new Error('Network error'));
const configWithProviders: IframeConfig = {
@@ -407,10 +407,50 @@ describe('IframeWrapper', () => {
render();
- // Should show login prompt after error
+ // Should show login UI on error when identity providers are configured
await screen.findByText('Sign in required');
});
+ it('should skip login when Easy Auth is not configured (404)', async () => {
+ vi.mocked(authHandler.checkAuthStatus).mockResolvedValue({ isAuthenticated: false, isEasyAuthConfigured: false, error: null });
+
+ const configWithProviders: IframeConfig = {
+ ...defaultConfig,
+ props: {
+ ...defaultConfig.props,
+ identityProviders: {
+ aad: {
+ signInEndpoint: '/.auth/login/aad',
+ name: 'Microsoft',
+ },
+ },
+ },
+ };
+
+ render();
+
+ // Should go directly to chat widget (let it fail naturally without apiKey)
+ await screen.findByTestId('chat-widget');
+ });
+
+ it('should skip login when Easy Auth is configured but no identity providers', async () => {
+ vi.mocked(authHandler.checkAuthStatus).mockResolvedValue({ isAuthenticated: false, isEasyAuthConfigured: true, error: null });
+
+ // No identity providers configured
+ const configWithoutProviders: IframeConfig = {
+ ...defaultConfig,
+ props: {
+ ...defaultConfig.props,
+ identityProviders: undefined,
+ },
+ };
+
+ render();
+
+ // Should go directly to chat widget (no sign-in required when no providers)
+ await screen.findByTestId('chat-widget');
+ });
+
it('should skip auth check when in portal mode', async () => {
vi.mocked(authHandler.checkAuthStatus).mockClear();
@@ -443,7 +483,7 @@ describe('IframeWrapper', () => {
});
it('should call openLoginPopup when login button is clicked', async () => {
- vi.mocked(authHandler.checkAuthStatus).mockResolvedValue({ isAuthenticated: false, error: null });
+ vi.mocked(authHandler.checkAuthStatus).mockResolvedValue({ isAuthenticated: false, isEasyAuthConfigured: true, error: null });
const configWithProviders: IframeConfig = {
...defaultConfig,
@@ -478,7 +518,7 @@ describe('IframeWrapper', () => {
});
it('should show chat widget after successful login', async () => {
- vi.mocked(authHandler.checkAuthStatus).mockResolvedValue({ isAuthenticated: false, error: null });
+ vi.mocked(authHandler.checkAuthStatus).mockResolvedValue({ isAuthenticated: false, isEasyAuthConfigured: true, error: null });
// Capture the onSuccess callback
let onSuccessCallback: ((authInfo: authHandler.AuthInformation) => void) | undefined;
@@ -513,7 +553,7 @@ describe('IframeWrapper', () => {
// Simulate successful login callback with auth info
await act(async () => {
if (onSuccessCallback) {
- onSuccessCallback({ isAuthenticated: true, error: null, username: 'Test User' });
+ onSuccessCallback({ isAuthenticated: true, isEasyAuthConfigured: true, error: null, username: 'Test User' });
}
});
@@ -522,7 +562,7 @@ describe('IframeWrapper', () => {
});
it('should show error message when login fails', async () => {
- vi.mocked(authHandler.checkAuthStatus).mockResolvedValue({ isAuthenticated: false, error: null });
+ vi.mocked(authHandler.checkAuthStatus).mockResolvedValue({ isAuthenticated: false, isEasyAuthConfigured: true, error: null });
// Capture the onFailed callback
let onFailedCallback: ((error: Error) => void) | undefined;
diff --git a/apps/iframe-app/src/lib/__tests__/authHandler.test.ts b/apps/iframe-app/src/lib/__tests__/authHandler.test.ts
index 051b86e27ab..c99db3c2317 100644
--- a/apps/iframe-app/src/lib/__tests__/authHandler.test.ts
+++ b/apps/iframe-app/src/lib/__tests__/authHandler.test.ts
@@ -146,10 +146,11 @@ describe('authHandler', () => {
const result = await checkAuthStatus('https://example.com');
- expect(result).toEqual({ isAuthenticated: true, error: null, username: 'John Doe' });
+ expect(result).toEqual({ isAuthenticated: true, isEasyAuthConfigured: true, error: null, username: 'John Doe' });
expect(mockFetch).toHaveBeenCalledWith('https://example.com/.auth/me', {
method: 'GET',
credentials: 'include',
+ redirect: 'manual',
});
});
@@ -162,7 +163,7 @@ describe('authHandler', () => {
const result = await checkAuthStatus('https://example.com');
- expect(result).toEqual({ isAuthenticated: true, error: null, username: undefined });
+ expect(result).toEqual({ isAuthenticated: true, isEasyAuthConfigured: true, error: null, username: undefined });
});
it('should return false when user is not authenticated (empty array)', async () => {
@@ -173,7 +174,7 @@ describe('authHandler', () => {
const result = await checkAuthStatus('https://example.com');
- expect(result).toEqual({ isAuthenticated: false, error: null, username: undefined });
+ expect(result).toEqual({ isAuthenticated: false, isEasyAuthConfigured: true, error: null, username: undefined });
});
it('should handle invalid JWT token gracefully', async () => {
@@ -184,7 +185,7 @@ describe('authHandler', () => {
const result = await checkAuthStatus('https://example.com');
- expect(result).toEqual({ isAuthenticated: true, error: null, username: undefined });
+ expect(result).toEqual({ isAuthenticated: true, isEasyAuthConfigured: true, error: null, username: undefined });
});
it('should handle missing access_token gracefully', async () => {
@@ -195,20 +196,43 @@ describe('authHandler', () => {
const result = await checkAuthStatus('https://example.com');
- expect(result).toEqual({ isAuthenticated: true, error: null, username: undefined });
+ expect(result).toEqual({ isAuthenticated: true, isEasyAuthConfigured: true, error: null, username: undefined });
});
- it('should return false when response is not ok', async () => {
+ it('should return isEasyAuthConfigured true and not authenticated when 401', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 401,
+ type: 'basic',
});
const result = await checkAuthStatus('https://example.com');
- expect(result.isAuthenticated).toBe(false);
- expect(result.error).toBeInstanceOf(Error);
- expect(result.error?.message).toBe('Failed to fetch authentication status');
+ expect(result).toEqual({ isAuthenticated: false, isEasyAuthConfigured: true, error: null });
+ });
+
+ it('should return isEasyAuthConfigured true and not authenticated when 403', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ status: 403,
+ type: 'basic',
+ });
+
+ const result = await checkAuthStatus('https://example.com');
+
+ expect(result).toEqual({ isAuthenticated: false, isEasyAuthConfigured: true, error: null });
+ });
+
+ it('should return isEasyAuthConfigured true and not authenticated on opaqueredirect (302)', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ status: 0,
+ type: 'opaqueredirect',
+ });
+
+ const result = await checkAuthStatus('https://example.com');
+
+ expect(result).toEqual({ isAuthenticated: false, isEasyAuthConfigured: true, error: null });
});
it('should return false on network error', async () => {
@@ -217,7 +241,49 @@ describe('authHandler', () => {
const result = await checkAuthStatus('https://example.com');
- expect(result).toEqual({ isAuthenticated: false, error: networkError });
+ expect(result).toEqual({ isAuthenticated: false, isEasyAuthConfigured: false, error: networkError });
+ });
+
+ it('should return isEasyAuthConfigured false when /.auth/me returns 404', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ status: 404,
+ type: 'basic',
+ });
+
+ const result = await checkAuthStatus('https://example.com');
+
+ expect(result).toEqual({ isAuthenticated: false, isEasyAuthConfigured: false, error: null });
+ });
+
+ it('should return isEasyAuthConfigured true with error when 500 Internal Server Error', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ status: 500,
+ type: 'basic',
+ });
+
+ const result = await checkAuthStatus('https://example.com');
+
+ expect(result.isAuthenticated).toBe(false);
+ expect(result.isEasyAuthConfigured).toBe(true);
+ expect(result.error).toBeInstanceOf(Error);
+ expect(result.error?.message).toBe('Failed to fetch authentication status');
+ });
+
+ it('should return isEasyAuthConfigured true with error when 503 Service Unavailable', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ status: 503,
+ type: 'basic',
+ });
+
+ const result = await checkAuthStatus('https://example.com');
+
+ expect(result.isAuthenticated).toBe(false);
+ expect(result.isEasyAuthConfigured).toBe(true);
+ expect(result.error).toBeInstanceOf(Error);
+ expect(result.error?.message).toBe('Failed to fetch authentication status');
});
});
diff --git a/apps/iframe-app/src/lib/authHandler.ts b/apps/iframe-app/src/lib/authHandler.ts
index 613ea82d0e1..7fe0765edf3 100644
--- a/apps/iframe-app/src/lib/authHandler.ts
+++ b/apps/iframe-app/src/lib/authHandler.ts
@@ -18,6 +18,14 @@ interface JwtPayload {
export interface AuthInformation {
isAuthenticated: boolean;
+ /**
+ * Indicates whether Azure App Service EasyAuth is configured on the Logic App.
+ * - `true`: EasyAuth is configured (/.auth/me returns 401, 403, 302, or 200)
+ * - `false`: EasyAuth is NOT configured (/.auth/me returns 404 or network error)
+ *
+ * When false, the application should skip authentication flows entirely.
+ */
+ isEasyAuthConfigured: boolean;
error: Error | null;
username?: string;
}
@@ -139,16 +147,43 @@ export function getBaseUrl(agentCardUrl?: string): string {
/**
* Checks the current authentication status via EasyAuth /.auth/me endpoint
* Also extracts username from the JWT token if available
+ *
+ * Easy Auth can be configured to return different status codes for unauthenticated requests:
+ * - 401 Unauthorized
+ * - 403 Forbidden
+ * - 404 Not Found (this means Easy Auth is NOT configured)
+ * - 302 Redirect
+ *
+ * Any of 401, 403, or 302 indicates Easy Auth IS configured but user is not authenticated.
*/
export async function checkAuthStatus(baseUrl: string): Promise {
try {
const response = await fetch(`${baseUrl}/.auth/me`, {
method: 'GET',
credentials: 'include', // Important: include cookies for cross-origin
+ redirect: 'manual', // Don't follow redirects - we want to detect 302
});
+ const status = response.status;
+
+ // 404 means Easy Auth is not configured on this Logic App
+ if (status === 404) {
+ return { isAuthenticated: false, isEasyAuthConfigured: false, error: null };
+ }
+
+ // 401, 403 mean Easy Auth is configured but user is not authenticated
+ if (status === 401 || status === 403) {
+ return { isAuthenticated: false, isEasyAuthConfigured: true, error: null };
+ }
+
+ // 0 status with opaqueredirect type means a 302 redirect was attempted
+ // This happens when redirect: 'manual' is set and server tries to redirect
+ if (response.type === 'opaqueredirect' || status === 0) {
+ return { isAuthenticated: false, isEasyAuthConfigured: true, error: null };
+ }
+
if (!response.ok) {
- return { isAuthenticated: false, error: new Error('Failed to fetch authentication status') };
+ return { isAuthenticated: false, isEasyAuthConfigured: true, error: new Error('Failed to fetch authentication status') };
}
const data = await response.json();
@@ -156,9 +191,10 @@ export async function checkAuthStatus(baseUrl: string): Promise
const isAuthenticated = Array.isArray(data) && data.length > 0;
const username = extractUsernameFromToken(data[0]?.access_token);
- return { isAuthenticated, error: null, username };
+ return { isAuthenticated, isEasyAuthConfigured: true, error: null, username };
} catch (error) {
- return { isAuthenticated: false, error: error as Error };
+ // Network errors or other failures - assume Easy Auth is not configured
+ return { isAuthenticated: false, isEasyAuthConfigured: false, error: error as Error };
}
}
diff --git a/apps/iframe-app/src/lib/utils/__tests__/config-parser.test.ts b/apps/iframe-app/src/lib/utils/__tests__/config-parser.test.ts
index d376cf80ef7..9096042a54b 100644
--- a/apps/iframe-app/src/lib/utils/__tests__/config-parser.test.ts
+++ b/apps/iframe-app/src/lib/utils/__tests__/config-parser.test.ts
@@ -220,16 +220,13 @@ describe('parseIframeConfig', () => {
expect(config.contextId).toBe('ctx-123');
});
- it('includes default identity providers when window.IDENTITY_PROVIDERS is not set', () => {
+ it('returns undefined for identity providers when window.IDENTITY_PROVIDERS is not set', () => {
document.documentElement.dataset.agentCard = 'http://test.agent/agent-card.json';
const config = parseIframeConfig();
- expect(config.props.identityProviders).toBeDefined();
- expect(config.props.identityProviders?.microsoft).toEqual({
- signInEndpoint: '/.auth/login/aad',
- name: 'Microsoft',
- });
+ // No fallback to default providers - undefined means no sign-in required
+ expect(config.props.identityProviders).toBeUndefined();
});
it('uses window.IDENTITY_PROVIDERS when set', () => {
@@ -249,79 +246,67 @@ describe('parseIframeConfig', () => {
delete (window as any).IDENTITY_PROVIDERS;
});
- it('falls back to default identity providers when IDENTITY_PROVIDERS is invalid JSON', () => {
+ it('returns undefined for identity providers when IDENTITY_PROVIDERS is invalid JSON', () => {
document.documentElement.dataset.agentCard = 'http://test.agent/agent-card.json';
(window as any).IDENTITY_PROVIDERS = 'not valid json';
const config = parseIframeConfig();
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to parse IDENTITY_PROVIDERS:', expect.any(Error));
- expect(config.props.identityProviders).toBeDefined();
- expect(config.props.identityProviders?.microsoft).toEqual({
- signInEndpoint: '/.auth/login/aad',
- name: 'Microsoft',
- });
+ // No fallback to default providers - undefined means no sign-in required
+ expect(config.props.identityProviders).toBeUndefined();
// Clean up
delete (window as any).IDENTITY_PROVIDERS;
});
- it('falls back to default identity providers when IDENTITY_PROVIDERS is empty string', () => {
+ it('returns undefined for identity providers when IDENTITY_PROVIDERS is empty string', () => {
document.documentElement.dataset.agentCard = 'http://test.agent/agent-card.json';
(window as any).IDENTITY_PROVIDERS = '';
const config = parseIframeConfig();
- expect(config.props.identityProviders).toBeDefined();
- expect(config.props.identityProviders?.microsoft).toEqual({
- signInEndpoint: '/.auth/login/aad',
- name: 'Microsoft',
- });
+ // No fallback to default providers - undefined means no sign-in required
+ expect(config.props.identityProviders).toBeUndefined();
// Clean up
delete (window as any).IDENTITY_PROVIDERS;
});
- it('falls back to default identity providers when IDENTITY_PROVIDERS parses to null', () => {
+ it('returns undefined for identity providers when IDENTITY_PROVIDERS parses to null', () => {
document.documentElement.dataset.agentCard = 'http://test.agent/agent-card.json';
(window as any).IDENTITY_PROVIDERS = 'null';
const config = parseIframeConfig();
- expect(config.props.identityProviders).toBeDefined();
- expect(config.props.identityProviders?.microsoft).toEqual({
- signInEndpoint: '/.auth/login/aad',
- name: 'Microsoft',
- });
+ // No fallback to default providers - undefined means no sign-in required
+ expect(config.props.identityProviders).toBeUndefined();
// Clean up
delete (window as any).IDENTITY_PROVIDERS;
});
- it('falls back to default identity providers when IDENTITY_PROVIDERS parses to array', () => {
+ it('returns undefined for identity providers when IDENTITY_PROVIDERS parses to array', () => {
document.documentElement.dataset.agentCard = 'http://test.agent/agent-card.json';
(window as any).IDENTITY_PROVIDERS = '["microsoft", "google"]';
const config = parseIframeConfig();
- // Arrays are objects in JS, so this will actually be used - this tests current behavior
- expect(config.props.identityProviders).toEqual(['microsoft', 'google']);
+ // Arrays are objects but not the expected Record format
+ expect(config.props.identityProviders).toBeUndefined();
// Clean up
delete (window as any).IDENTITY_PROVIDERS;
});
- it('falls back to default identity providers when IDENTITY_PROVIDERS parses to primitive', () => {
+ it('returns undefined for identity providers when IDENTITY_PROVIDERS parses to primitive', () => {
document.documentElement.dataset.agentCard = 'http://test.agent/agent-card.json';
(window as any).IDENTITY_PROVIDERS = '"just a string"';
const config = parseIframeConfig();
- expect(config.props.identityProviders).toBeDefined();
- expect(config.props.identityProviders?.microsoft).toEqual({
- signInEndpoint: '/.auth/login/aad',
- name: 'Microsoft',
- });
+ // No fallback to default providers - undefined means no sign-in required
+ expect(config.props.identityProviders).toBeUndefined();
// Clean up
delete (window as any).IDENTITY_PROVIDERS;
@@ -476,13 +461,13 @@ describe('parseIdentityProviders', () => {
expect(result).toBeUndefined();
});
- it('returns array when JSON parses to array (arrays are objects)', () => {
+ it('returns undefined when JSON parses to array (arrays are not valid Record format)', () => {
(window as any).IDENTITY_PROVIDERS = '["microsoft", "google"]';
const result = parseIdentityProviders();
- // Note: arrays pass typeof === 'object' check, so they are returned
- expect(result).toEqual(['microsoft', 'google']);
+ // Arrays are objects but not valid Record format
+ expect(result).toBeUndefined();
});
it('returns empty object for empty JSON object', () => {
diff --git a/apps/iframe-app/src/lib/utils/config-parser.ts b/apps/iframe-app/src/lib/utils/config-parser.ts
index cbc25a11d8c..11123120f45 100644
--- a/apps/iframe-app/src/lib/utils/config-parser.ts
+++ b/apps/iframe-app/src/lib/utils/config-parser.ts
@@ -16,26 +16,6 @@ interface PortalValidationResult {
trustedParentOrigin?: string;
}
-// This is a temporary workaround for identity providers until we have a proper way to configure them through server side
-const DEFAULT_IDENTITY_PROVIDERS: Record = {
- microsoft: {
- signInEndpoint: '/.auth/login/aad',
- name: 'Microsoft',
- },
- google: {
- signInEndpoint: '/.auth/login/google',
- name: 'Google',
- },
- facebook: {
- signInEndpoint: '/.auth/login/facebook',
- name: 'Facebook',
- },
- github: {
- signInEndpoint: '/.auth/login/github',
- name: 'GitHub',
- },
-};
-
const ALLOWED_PORTAL_AUTHORITIES = [
'df.onecloud.azure-test.net',
'portal.azure.com',
@@ -228,7 +208,7 @@ export function parseIframeConfig(): IframeConfig {
welcomeMessage: brandSubtitle || dataset.welcomeMessage || params.get('welcomeMessage') || undefined,
metadata: parseMetadata(params, dataset),
apiKey: apiKey || undefined,
- identityProviders: parseIdentityProviders() || DEFAULT_IDENTITY_PROVIDERS,
+ identityProviders: parseIdentityProviders(),
oboUserToken: oboUserToken || undefined,
...fileUploadConfig,
};
@@ -276,7 +256,8 @@ export function parseIdentityProviders(): Record | und
try {
const parsed = JSON.parse(identityProviders);
- if (typeof parsed === 'object' && parsed !== null) {
+ // Arrays are objects but not valid Record format
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
return parsed as Record;
}
} catch (e) {
diff --git a/e2e/chatClient/fixtures/sse-fixtures.ts b/e2e/chatClient/fixtures/sse-fixtures.ts
index 6eddc3529e2..dbea83331ad 100644
--- a/e2e/chatClient/fixtures/sse-fixtures.ts
+++ b/e2e/chatClient/fixtures/sse-fixtures.ts
@@ -38,7 +38,34 @@ const DEFAULT_MOCK_JWT = createMockJwt({
exp: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
});
+// Default identity providers for testing
+const DEFAULT_IDENTITY_PROVIDERS = {
+ aad: {
+ name: 'Microsoft',
+ loginPath: '/.auth/login/aad',
+ },
+};
+
async function setupSSEMocking(page: Page) {
+ // Intercept and modify the HTML to inject identity providers
+ // This replaces the placeholder BEFORE the inline script runs
+ await page.route('**/*', async (route) => {
+ const request = route.request();
+ if (request.resourceType() === 'document') {
+ const response = await route.fetch();
+ let html = await response.text();
+ // Replace the placeholder with actual identity providers JSON
+ const providersJson = JSON.stringify(DEFAULT_IDENTITY_PROVIDERS).replace(/"/g, '\\"');
+ html = html.replace(
+ 'window.IDENTITY_PROVIDERS = "REPLACE_ME_WITH_IDENTITY_PROVIDERS"',
+ `window.IDENTITY_PROVIDERS = "${providersJson}"`
+ );
+ await route.fulfill({ response, body: html });
+ } else {
+ await route.continue();
+ }
+ });
+
// Log browser console messages to see what's happening
page.on('console', (msg) => {
const type = msg.type();
diff --git a/e2e/chatClient/tests/features/authentication/auth-flow.spec.ts b/e2e/chatClient/tests/features/authentication/auth-flow.spec.ts
index 5158985b686..bcd629d7855 100644
--- a/e2e/chatClient/tests/features/authentication/auth-flow.spec.ts
+++ b/e2e/chatClient/tests/features/authentication/auth-flow.spec.ts
@@ -20,8 +20,14 @@ test.describe('Authentication Flows', { tag: '@mock' }, () => {
await page.goto(`http://localhost:3001/?agentCard=${encodeURIComponent(AGENT_CARD_URL)}`);
await page.waitForLoadState('networkidle');
- // Start new chat
- await page.getByRole('button', { name: /start a new chat/i }).click();
+ // Wait for empty state to be visible first
+ await expect(page.getByText('No chats yet')).toBeVisible({ timeout: 10000 });
+
+ // Start new chat - wait for button to be visible and enabled before clicking
+ const startChatButton = page.getByRole('button', { name: /start a new chat/i });
+ await expect(startChatButton).toBeVisible({ timeout: 5000 });
+ await expect(startChatButton).toBeEnabled({ timeout: 5000 });
+ await startChatButton.click();
await expect(page.locator('textarea').first()).toBeVisible({ timeout: 5000 });
});
@@ -153,13 +159,20 @@ test.describe('Authentication Flows', { tag: '@mock' }, () => {
await expect(page.getByText(/Authentication Required/i)).toBeVisible({ timeout: 10000 });
+ // Wait a moment for the UI to stabilize
+ await page.waitForTimeout(500);
+
// Find and click cancel button
const cancelButton = page.getByRole('button', { name: /Cancel Authentication/i });
await expect(cancelButton).toBeVisible({ timeout: 5000 });
await cancelButton.click();
- // Should show canceled state
- await expect(page.getByText(/Authentication Canceled/i)).toBeVisible({ timeout: 5000 });
+ // After canceling, the authentication message should be removed from the UI
+ // (the parent component clears the auth required state)
+ await expect(page.getByText(/Authentication Required/i)).not.toBeVisible({ timeout: 5000 });
+
+ // The chat should remain functional - input should still be available
+ await expect(page.locator('textarea').first()).toBeVisible();
});
test('should disable cancel button while authenticating', async ({ page, context }) => {
@@ -218,7 +231,15 @@ test.describe('Authentication Completion Flow', { tag: '@mock' }, () => {
test.beforeEach(async ({ page }) => {
await page.goto(`http://localhost:3001/?agentCard=${encodeURIComponent(AGENT_CARD_URL)}`);
await page.waitForLoadState('networkidle');
- await page.getByRole('button', { name: /start a new chat/i }).click();
+
+ // Wait for empty state to be visible first
+ await expect(page.getByText('No chats yet')).toBeVisible({ timeout: 10000 });
+
+ // Start new chat - wait for button to be visible and enabled before clicking
+ const startChatButton = page.getByRole('button', { name: /start a new chat/i });
+ await expect(startChatButton).toBeVisible({ timeout: 5000 });
+ await expect(startChatButton).toBeEnabled({ timeout: 5000 });
+ await startChatButton.click();
await expect(page.locator('textarea').first()).toBeVisible({ timeout: 5000 });
});
@@ -320,7 +341,15 @@ test.describe('Authentication Edge Cases', { tag: '@mock' }, () => {
test.beforeEach(async ({ page }) => {
await page.goto(`http://localhost:3001/?agentCard=${encodeURIComponent(AGENT_CARD_URL)}`);
await page.waitForLoadState('networkidle');
- await page.getByRole('button', { name: /start a new chat/i }).click();
+
+ // Wait for empty state to be visible first
+ await expect(page.getByText('No chats yet')).toBeVisible({ timeout: 10000 });
+
+ // Start new chat - wait for button to be visible and enabled before clicking
+ const startChatButton = page.getByRole('button', { name: /start a new chat/i });
+ await expect(startChatButton).toBeVisible({ timeout: 5000 });
+ await expect(startChatButton).toBeEnabled({ timeout: 5000 });
+ await startChatButton.click();
await expect(page.locator('textarea').first()).toBeVisible({ timeout: 5000 });
});
diff --git a/e2e/chatClient/tests/features/authentication/login-prompt.spec.ts b/e2e/chatClient/tests/features/authentication/login-prompt.spec.ts
index cdd88878499..e1edc38881d 100644
--- a/e2e/chatClient/tests/features/authentication/login-prompt.spec.ts
+++ b/e2e/chatClient/tests/features/authentication/login-prompt.spec.ts
@@ -13,31 +13,73 @@
* - Successful authentication
*/
-import { test, expect } from '../../../fixtures/sse-fixtures';
-import { test as baseTest } from '@playwright/test';
+import { test as baseTest, expect } from '@playwright/test';
+import type { Page } from '@playwright/test';
-// Agent card URL - intercepted by our fixture
+// Agent card URL - intercepted by our mocks
const AGENT_CARD_URL = 'http://localhost:3001/api/agents/test/.well-known/agent-card.json';
-test.describe('Login Prompt Display', { tag: '@mock' }, () => {
- test('should display login prompt when agent card returns 401', async ({ page }) => {
- // Override the agent card route to return 401
- await page.route('**/api/agents/test/.well-known/agent-card.json', async (route) => {
- await route.fulfill({
- status: 401,
- contentType: 'application/json',
- body: JSON.stringify({ error: 'Unauthorized' }),
- });
+// Mock identity providers to inject into the page
+const MOCK_IDENTITY_PROVIDERS = {
+ aad: {
+ name: 'Microsoft',
+ signInEndpoint: '/.auth/login/aad',
+ },
+};
+
+// Helper to set up login prompt test with identity providers
+async function setupLoginPromptTest(page: Page) {
+ // Intercept and modify the HTML to inject identity providers
+ // This replaces the placeholder BEFORE the inline script runs
+ await page.route('**/*', async (route) => {
+ const request = route.request();
+ if (request.resourceType() === 'document') {
+ const response = await route.fetch();
+ let html = await response.text();
+ // Replace the placeholder with actual identity providers JSON
+ const providersJson = JSON.stringify(MOCK_IDENTITY_PROVIDERS).replace(/"/g, '\\"');
+ html = html.replace(
+ 'window.IDENTITY_PROVIDERS = "REPLACE_ME_WITH_IDENTITY_PROVIDERS"',
+ `window.IDENTITY_PROVIDERS = "${providersJson}"`
+ );
+ await route.fulfill({ response, body: html });
+ } else {
+ // Use fallback() to let other routes handle non-document requests
+ await route.fallback();
+ }
+ });
+
+ // Override auth/me to return unauthenticated (empty array)
+ await page.route('**/.auth/me', async (route) => {
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify([]),
});
+ });
- // Also mock the auth refresh endpoint to fail
- await page.route('**/.auth/refresh', async (route) => {
- await route.fulfill({
- status: 401,
- contentType: 'application/json',
- body: JSON.stringify({ error: 'Unauthorized' }),
- });
+ // Override the agent card route to return 401
+ await page.route('**/api/agents/test/.well-known/agent-card.json', async (route) => {
+ await route.fulfill({
+ status: 401,
+ contentType: 'application/json',
+ body: JSON.stringify({ error: 'Unauthorized' }),
});
+ });
+
+ // Also mock the auth refresh endpoint to fail
+ await page.route('**/.auth/refresh', async (route) => {
+ await route.fulfill({
+ status: 401,
+ contentType: 'application/json',
+ body: JSON.stringify({ error: 'Unauthorized' }),
+ });
+ });
+}
+
+baseTest.describe('Login Prompt Display', { tag: '@mock' }, () => {
+ baseTest('should display login prompt when agent card returns 401', async ({ page }) => {
+ await setupLoginPromptTest(page);
// Navigate to the app
await page.goto(`http://localhost:3001/?agentCard=${encodeURIComponent(AGENT_CARD_URL)}`);
@@ -49,14 +91,8 @@ test.describe('Login Prompt Display', { tag: '@mock' }, () => {
await expect(page.getByRole('button', { name: 'Microsoft account' })).toBeVisible();
});
- test('should display person icon in login prompt', async ({ page }) => {
- // Override routes to trigger login
- await page.route('**/api/agents/test/.well-known/agent-card.json', async (route) => {
- await route.fulfill({ status: 401 });
- });
- await page.route('**/.auth/refresh', async (route) => {
- await route.fulfill({ status: 401 });
- });
+ baseTest('should display person icon in login prompt', async ({ page }) => {
+ await setupLoginPromptTest(page);
await page.goto(`http://localhost:3001/?agentCard=${encodeURIComponent(AGENT_CARD_URL)}`);
@@ -68,13 +104,8 @@ test.describe('Login Prompt Display', { tag: '@mock' }, () => {
await expect(iconContainer).toBeVisible();
});
- test('should show sign in button enabled by default', async ({ page }) => {
- await page.route('**/api/agents/test/.well-known/agent-card.json', async (route) => {
- await route.fulfill({ status: 401 });
- });
- await page.route('**/.auth/refresh', async (route) => {
- await route.fulfill({ status: 401 });
- });
+ baseTest('should show sign in button enabled by default', async ({ page }) => {
+ await setupLoginPromptTest(page);
await page.goto(`http://localhost:3001/?agentCard=${encodeURIComponent(AGENT_CARD_URL)}`);
@@ -87,18 +118,12 @@ test.describe('Login Prompt Display', { tag: '@mock' }, () => {
});
});
-test.describe('Login Popup Flow', { tag: '@mock' }, () => {
- test.beforeEach(async ({ page }) => {
- // Set up 401 response for agent card
- await page.route('**/api/agents/test/.well-known/agent-card.json', async (route) => {
- await route.fulfill({ status: 401 });
- });
- await page.route('**/.auth/refresh', async (route) => {
- await route.fulfill({ status: 401 });
- });
+baseTest.describe('Login Popup Flow', { tag: '@mock' }, () => {
+ baseTest.beforeEach(async ({ page }) => {
+ await setupLoginPromptTest(page);
});
- test('should open login popup when sign in button is clicked', async ({ page, context }) => {
+ baseTest('should open login popup when sign in button is clicked', async ({ page, context }) => {
await page.goto(`http://localhost:3001/?agentCard=${encodeURIComponent(AGENT_CARD_URL)}`);
// Wait for login prompt
@@ -120,14 +145,14 @@ test.describe('Login Popup Flow', { tag: '@mock' }, () => {
await popup.close();
});
- test('should show loading state when popup is open', async ({ page, context }) => {
+ baseTest('should show loading state when popup is open', async ({ page, context }) => {
await page.goto(`http://localhost:3001/?agentCard=${encodeURIComponent(AGENT_CARD_URL)}`);
await expect(page.getByText('Sign in required')).toBeVisible({ timeout: 10000 });
// Click sign in and capture popup
const [popup] = await Promise.all([
- context.waitForEvent('page', { timeout: 5000 }),
+ context.waitForEvent('page', { timeout: 10000 }),
page.getByRole('button', { name: 'Microsoft account' }).click(),
]);
@@ -141,7 +166,7 @@ test.describe('Login Popup Flow', { tag: '@mock' }, () => {
await popup.close();
});
- test('should handle popup blocker gracefully', async ({ page }) => {
+ baseTest('should handle popup blocker gracefully', async ({ page }) => {
await page.goto(`http://localhost:3001/?agentCard=${encodeURIComponent(AGENT_CARD_URL)}`);
await expect(page.getByText('Sign in required')).toBeVisible({ timeout: 10000 });
@@ -164,7 +189,7 @@ test.describe('Login Popup Flow', { tag: '@mock' }, () => {
await expect(page.getByRole('button', { name: 'Microsoft account' })).toBeEnabled();
});
- test('should display error message when popup is blocked', async ({ page }) => {
+ baseTest('should display error message when popup is blocked', async ({ page }) => {
await page.goto(`http://localhost:3001/?agentCard=${encodeURIComponent(AGENT_CARD_URL)}`);
await expect(page.getByText('Sign in required')).toBeVisible({ timeout: 10000 });
@@ -194,10 +219,13 @@ test.describe('Login Popup Flow', { tag: '@mock' }, () => {
});
});
-test.describe('Token Refresh Flow', { tag: '@mock' }, () => {
- test('should attempt token refresh before showing login prompt', async ({ page }) => {
+baseTest.describe('Token Refresh Flow', { tag: '@mock' }, () => {
+ baseTest('should attempt token refresh before showing login prompt', async ({ page }) => {
let refreshCalled = false;
+ // Inject identity providers via HTML modification
+ await injectIdentityProviders(page, MOCK_IDENTITY_PROVIDERS);
+
// Track refresh calls
await page.route('**/.auth/refresh', async (route) => {
refreshCalled = true;
@@ -217,7 +245,10 @@ test.describe('Token Refresh Flow', { tag: '@mock' }, () => {
expect(refreshCalled).toBe(true);
});
- test('should reload page if token refresh succeeds', async ({ page }) => {
+ baseTest('should reload page if token refresh succeeds', async ({ page }) => {
+ // Inject identity providers via HTML modification
+ await injectIdentityProviders(page, MOCK_IDENTITY_PROVIDERS);
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let refreshAttempts = 0;
let agentCardAttempts = 0;
@@ -260,8 +291,11 @@ test.describe('Token Refresh Flow', { tag: '@mock' }, () => {
});
});
-test.describe('Authentication Success Flow', { tag: '@mock' }, () => {
- test('should reload page after successful authentication', async ({ page, context }) => {
+baseTest.describe('Authentication Success Flow', { tag: '@mock' }, () => {
+ baseTest('should reload page after successful authentication', async ({ page, context }) => {
+ // Inject identity providers via HTML modification
+ await injectIdentityProviders(page, MOCK_IDENTITY_PROVIDERS);
+
let authMeCallCount = 0;
// Set up 401 for agent card initially
@@ -318,7 +352,7 @@ test.describe('Authentication Success Flow', { tag: '@mock' }, () => {
// Click sign in
const [popup] = await Promise.all([
- context.waitForEvent('page', { timeout: 5000 }),
+ context.waitForEvent('page', { timeout: 10000 }),
page.getByRole('button', { name: 'Microsoft account' }).click(),
]);
@@ -330,17 +364,12 @@ test.describe('Authentication Success Flow', { tag: '@mock' }, () => {
});
});
-test.describe('Login Prompt Accessibility', { tag: '@mock' }, () => {
- test.beforeEach(async ({ page }) => {
- await page.route('**/api/agents/test/.well-known/agent-card.json', async (route) => {
- await route.fulfill({ status: 401 });
- });
- await page.route('**/.auth/refresh', async (route) => {
- await route.fulfill({ status: 401 });
- });
+baseTest.describe('Login Prompt Accessibility', { tag: '@mock' }, () => {
+ baseTest.beforeEach(async ({ page }) => {
+ await setupLoginPromptTest(page);
});
- test('should have accessible button with proper role', async ({ page }) => {
+ baseTest('should have accessible button with proper role', async ({ page }) => {
await page.goto(`http://localhost:3001/?agentCard=${encodeURIComponent(AGENT_CARD_URL)}`);
await expect(page.getByText('Sign in required')).toBeVisible({ timeout: 10000 });
@@ -351,7 +380,7 @@ test.describe('Login Prompt Accessibility', { tag: '@mock' }, () => {
await expect(signInButton).toHaveAttribute('type', 'button');
});
- test('should be keyboard navigable', async ({ page }) => {
+ baseTest('should be keyboard navigable', async ({ page }) => {
await page.goto(`http://localhost:3001/?agentCard=${encodeURIComponent(AGENT_CARD_URL)}`);
await expect(page.getByText('Sign in required')).toBeVisible({ timeout: 10000 });
@@ -466,6 +495,31 @@ baseTest.describe('Successful Login to MultiSession Chat', { tag: '@mock' }, ()
await route.fulfill({ status: 400 });
});
+ // Inject identity providers via HTML modification (LAST to have highest priority in context.route)
+ // Only modify main app pages, not auth pages
+ await context.route('**/*', async (route) => {
+ const request = route.request();
+ const url = request.url();
+ // Skip auth URLs and API URLs - they have their own handlers
+ // Use fallback() to let other routes handle these
+ if (url.includes('/.auth/') || url.includes('/api/')) {
+ await route.fallback();
+ return;
+ }
+ if (request.resourceType() === 'document') {
+ const response = await route.fetch();
+ let html = await response.text();
+ const providersJson = JSON.stringify(MOCK_IDENTITY_PROVIDERS).replace(/"/g, '\\"');
+ html = html.replace(
+ 'window.IDENTITY_PROVIDERS = "REPLACE_ME_WITH_IDENTITY_PROVIDERS"',
+ `window.IDENTITY_PROVIDERS = "${providersJson}"`
+ );
+ await route.fulfill({ response, body: html });
+ } else {
+ await route.fallback();
+ }
+ });
+
// Navigate with multiSession enabled
await page.goto(`http://localhost:3001/?agentCard=${encodeURIComponent(AGENT_CARD_URL)}&multiSession=true`);
@@ -474,7 +528,7 @@ baseTest.describe('Successful Login to MultiSession Chat', { tag: '@mock' }, ()
// Click sign in
const [popup] = await Promise.all([
- context.waitForEvent('page', { timeout: 5000 }),
+ context.waitForEvent('page', { timeout: 10000 }),
page.getByRole('button', { name: 'Microsoft account' }).click(),
]);
@@ -553,6 +607,31 @@ baseTest.describe('Successful Login to MultiSession Chat', { tag: '@mock' }, ()
});
});
+ // Inject identity providers via HTML modification (LAST to have highest priority in context.route)
+ // Only modify main app pages, not auth pages
+ await context.route('**/*', async (route) => {
+ const request = route.request();
+ const url = request.url();
+ // Skip auth URLs and API URLs - they have their own handlers
+ // Use fallback() to let other routes handle these
+ if (url.includes('/.auth/') || url.includes('/api/')) {
+ await route.fallback();
+ return;
+ }
+ if (request.resourceType() === 'document') {
+ const response = await route.fetch();
+ let html = await response.text();
+ const providersJson = JSON.stringify(MOCK_IDENTITY_PROVIDERS).replace(/"/g, '\\"');
+ html = html.replace(
+ 'window.IDENTITY_PROVIDERS = "REPLACE_ME_WITH_IDENTITY_PROVIDERS"',
+ `window.IDENTITY_PROVIDERS = "${providersJson}"`
+ );
+ await route.fulfill({ response, body: html });
+ } else {
+ await route.fallback();
+ }
+ });
+
// Navigate without multiSession (single session mode)
await page.goto(`http://localhost:3001/?agentCard=${encodeURIComponent(AGENT_CARD_URL)}`);
@@ -561,7 +640,7 @@ baseTest.describe('Successful Login to MultiSession Chat', { tag: '@mock' }, ()
// Click sign in
const [popup] = await Promise.all([
- context.waitForEvent('page', { timeout: 5000 }),
+ context.waitForEvent('page', { timeout: 10000 }),
page.getByRole('button', { name: 'Microsoft account' }).click(),
]);
@@ -624,6 +703,31 @@ baseTest.describe('Successful Login to MultiSession Chat', { tag: '@mock' }, ()
});
});
+ // Inject identity providers via HTML modification (LAST to have highest priority in context.route)
+ // Only modify main app pages, not auth pages
+ await context.route('**/*', async (route) => {
+ const request = route.request();
+ const url = request.url();
+ // Skip auth URLs and API URLs - they have their own handlers
+ // Use fallback() to let other routes handle these
+ if (url.includes('/.auth/') || url.includes('/api/')) {
+ await route.fallback();
+ return;
+ }
+ if (request.resourceType() === 'document') {
+ const response = await route.fetch();
+ let html = await response.text();
+ const providersJson = JSON.stringify(MOCK_IDENTITY_PROVIDERS).replace(/"/g, '\\"');
+ html = html.replace(
+ 'window.IDENTITY_PROVIDERS = "REPLACE_ME_WITH_IDENTITY_PROVIDERS"',
+ `window.IDENTITY_PROVIDERS = "${providersJson}"`
+ );
+ await route.fulfill({ response, body: html });
+ } else {
+ await route.fallback();
+ }
+ });
+
await page.goto(`http://localhost:3001/?agentCard=${encodeURIComponent(AGENT_CARD_URL)}`);
// Login prompt should appear
@@ -631,7 +735,7 @@ baseTest.describe('Successful Login to MultiSession Chat', { tag: '@mock' }, ()
// Sign in
const [popup] = await Promise.all([
- context.waitForEvent('page', { timeout: 5000 }),
+ context.waitForEvent('page', { timeout: 10000 }),
page.getByRole('button', { name: 'Microsoft account' }).click(),
]);
@@ -643,8 +747,37 @@ baseTest.describe('Successful Login to MultiSession Chat', { tag: '@mock' }, ()
});
});
-test.describe('Multiple Identity Providers', { tag: '@mock' }, () => {
- test('should display multiple provider buttons when multiple providers are configured', async ({ page }) => {
+// Helper to inject custom identity providers via HTML modification
+async function injectIdentityProviders(page: Page, providers: Record) {
+ await page.route('**/*', async (route) => {
+ const request = route.request();
+ if (request.resourceType() === 'document') {
+ const response = await route.fetch();
+ let html = await response.text();
+ const providersJson = JSON.stringify(providers).replace(/"/g, '\\"');
+ html = html.replace(
+ 'window.IDENTITY_PROVIDERS = "REPLACE_ME_WITH_IDENTITY_PROVIDERS"',
+ `window.IDENTITY_PROVIDERS = "${providersJson}"`
+ );
+ await route.fulfill({ response, body: html });
+ } else {
+ // Use fallback() to let other routes handle non-document requests
+ await route.fallback();
+ }
+ });
+}
+
+baseTest.describe('Multiple Identity Providers', { tag: '@mock' }, () => {
+ baseTest('should display multiple provider buttons when multiple providers are configured', async ({ page }) => {
+ const multipleProviders = {
+ aad: { signInEndpoint: '/.auth/login/aad', name: 'Microsoft' },
+ google: { signInEndpoint: '/.auth/login/google', name: 'Google' },
+ github: { signInEndpoint: '/.auth/login/github', name: 'GitHub' },
+ };
+
+ // Inject multiple identity providers
+ await injectIdentityProviders(page, multipleProviders);
+
// Override auth/me to return not authenticated
await page.route('**/.auth/me', async (route) => {
await route.fulfill({
@@ -654,10 +787,13 @@ test.describe('Multiple Identity Providers', { tag: '@mock' }, () => {
});
});
- // Set window.IDENTITY_PROVIDERS with multiple providers before page loads
- await page.addInitScript(() => {
- (window as any).IDENTITY_PROVIDERS =
- '{"aad":{"signInEndpoint":"/.auth/login/aad","name":"Microsoft"},"google":{"signInEndpoint":"/.auth/login/google","name":"Google"},"github":{"signInEndpoint":"/.auth/login/github","name":"GitHub"}}';
+ // Override agent card to return 401 (trigger login prompt)
+ await page.route('**/api/agents/test/.well-known/agent-card.json', async (route) => {
+ await route.fulfill({ status: 401 });
+ });
+
+ await page.route('**/.auth/refresh', async (route) => {
+ await route.fulfill({ status: 401 });
});
await page.goto(`http://localhost:3001/?agentCard=${encodeURIComponent(AGENT_CARD_URL)}`);
@@ -671,7 +807,15 @@ test.describe('Multiple Identity Providers', { tag: '@mock' }, () => {
await expect(page.getByRole('button', { name: 'GitHub account' })).toBeVisible();
});
- test('should use correct sign-in endpoint for each provider', async ({ page, context }) => {
+ baseTest('should use correct sign-in endpoint for each provider', async ({ page, context }) => {
+ const multipleProviders = {
+ aad: { signInEndpoint: '/.auth/login/aad', name: 'Microsoft' },
+ google: { signInEndpoint: '/.auth/login/google', name: 'Google' },
+ };
+
+ // Inject multiple identity providers
+ await injectIdentityProviders(page, multipleProviders);
+
// Override auth/me to return not authenticated
await page.route('**/.auth/me', async (route) => {
await route.fulfill({
@@ -681,10 +825,13 @@ test.describe('Multiple Identity Providers', { tag: '@mock' }, () => {
});
});
- // Set window.IDENTITY_PROVIDERS with multiple providers before page loads
- await page.addInitScript(() => {
- (window as any).IDENTITY_PROVIDERS =
- '{"aad":{"signInEndpoint":"/.auth/login/aad","name":"Microsoft"},"google":{"signInEndpoint":"/.auth/login/google","name":"Google"}}';
+ // Override agent card to return 401 (trigger login prompt)
+ await page.route('**/api/agents/test/.well-known/agent-card.json', async (route) => {
+ await route.fulfill({ status: 401 });
+ });
+
+ await page.route('**/.auth/refresh', async (route) => {
+ await route.fulfill({ status: 401 });
});
await page.goto(`http://localhost:3001/?agentCard=${encodeURIComponent(AGENT_CARD_URL)}`);
@@ -703,7 +850,15 @@ test.describe('Multiple Identity Providers', { tag: '@mock' }, () => {
await popup.close();
});
- test('should show loading state only on clicked provider button', async ({ page, context }) => {
+ baseTest('should show loading state only on clicked provider button', async ({ page, context }) => {
+ const multipleProviders = {
+ aad: { signInEndpoint: '/.auth/login/aad', name: 'Microsoft' },
+ google: { signInEndpoint: '/.auth/login/google', name: 'Google' },
+ };
+
+ // Inject multiple identity providers
+ await injectIdentityProviders(page, multipleProviders);
+
// Override auth/me to return not authenticated
await page.route('**/.auth/me', async (route) => {
await route.fulfill({
@@ -713,10 +868,13 @@ test.describe('Multiple Identity Providers', { tag: '@mock' }, () => {
});
});
- // Set window.IDENTITY_PROVIDERS with multiple providers before page loads
- await page.addInitScript(() => {
- (window as any).IDENTITY_PROVIDERS =
- '{"aad":{"signInEndpoint":"/.auth/login/aad","name":"Microsoft"},"google":{"signInEndpoint":"/.auth/login/google","name":"Google"}}';
+ // Override agent card to return 401 (trigger login prompt)
+ await page.route('**/api/agents/test/.well-known/agent-card.json', async (route) => {
+ await route.fulfill({ status: 401 });
+ });
+
+ await page.route('**/.auth/refresh', async (route) => {
+ await route.fulfill({ status: 401 });
});
await page.goto(`http://localhost:3001/?agentCard=${encodeURIComponent(AGENT_CARD_URL)}`);
@@ -726,7 +884,7 @@ test.describe('Multiple Identity Providers', { tag: '@mock' }, () => {
// Click Google button
const [popup] = await Promise.all([
- context.waitForEvent('page', { timeout: 5000 }),
+ context.waitForEvent('page', { timeout: 10000 }),
page.getByRole('button', { name: 'Google account' }).click(),
]);
@@ -742,17 +900,8 @@ test.describe('Multiple Identity Providers', { tag: '@mock' }, () => {
await popup.close();
});
- test('should show configuration message when no providers configured', async ({ page }) => {
- // Override auth/me to return not authenticated
- await page.route('**/.auth/me', async (route) => {
- await route.fulfill({
- status: 200,
- contentType: 'application/json',
- body: JSON.stringify([]),
- });
- });
-
- // Intercept the HTML page and replace the IDENTITY_PROVIDERS placeholder with empty object
+ baseTest('should show configuration message when no providers configured', async ({ page }) => {
+ // Intercept the HTML page FIRST and replace the IDENTITY_PROVIDERS placeholder with empty object
// This simulates server-side injection of empty providers config
await page.route('**/*', async (route) => {
const request = route.request();
@@ -763,10 +912,29 @@ test.describe('Multiple Identity Providers', { tag: '@mock' }, () => {
html = html.replace('window.IDENTITY_PROVIDERS = "REPLACE_ME_WITH_IDENTITY_PROVIDERS"', 'window.IDENTITY_PROVIDERS = "{}"');
await route.fulfill({ response, body: html });
} else {
- await route.continue();
+ // Use fallback() to let other routes handle non-document requests
+ await route.fallback();
}
});
+ // Override auth/me to return not authenticated
+ await page.route('**/.auth/me', async (route) => {
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify([]),
+ });
+ });
+
+ // Override agent card to return 401 (trigger login prompt)
+ await page.route('**/api/agents/test/.well-known/agent-card.json', async (route) => {
+ await route.fulfill({ status: 401 });
+ });
+
+ await page.route('**/.auth/refresh', async (route) => {
+ await route.fulfill({ status: 401 });
+ });
+
await page.goto(`http://localhost:3001/?agentCard=${encodeURIComponent(AGENT_CARD_URL)}`);
// Wait for login prompt
@@ -779,17 +947,8 @@ test.describe('Multiple Identity Providers', { tag: '@mock' }, () => {
await expect(page.getByRole('button', { name: /account/i })).not.toBeVisible();
});
- test('should handle single custom provider correctly', async ({ page, context }) => {
- // Override auth/me to return not authenticated
- await page.route('**/.auth/me', async (route) => {
- await route.fulfill({
- status: 200,
- contentType: 'application/json',
- body: JSON.stringify([]),
- });
- });
-
- // Intercept the HTML page and replace the IDENTITY_PROVIDERS placeholder with custom provider
+ baseTest('should handle single custom provider correctly', async ({ page, context }) => {
+ // Intercept the HTML page FIRST and replace the IDENTITY_PROVIDERS placeholder with custom provider
// This simulates server-side injection of custom providers config
await page.route('**/*', async (route) => {
const request = route.request();
@@ -803,10 +962,29 @@ test.describe('Multiple Identity Providers', { tag: '@mock' }, () => {
);
await route.fulfill({ response, body: html });
} else {
- await route.continue();
+ // Use fallback() to let other routes handle non-document requests
+ await route.fallback();
}
});
+ // Override auth/me to return not authenticated
+ await page.route('**/.auth/me', async (route) => {
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify([]),
+ });
+ });
+
+ // Override agent card to return 401 (trigger login prompt)
+ await page.route('**/api/agents/test/.well-known/agent-card.json', async (route) => {
+ await route.fulfill({ status: 401 });
+ });
+
+ await page.route('**/.auth/refresh', async (route) => {
+ await route.fulfill({ status: 401 });
+ });
+
await page.goto(`http://localhost:3001/?agentCard=${encodeURIComponent(AGENT_CARD_URL)}`);
// Wait for login prompt