diff --git a/.changeset/rich-parrots-lie.md b/.changeset/rich-parrots-lie.md new file mode 100644 index 00000000000..788d358fa7b --- /dev/null +++ b/.changeset/rich-parrots-lie.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/ui-contexts': patch +'@rocket.chat/meteor': patch +--- + +Show iframe authentication page, when login through iframe authentication API token fails diff --git a/apps/meteor/client/hooks/iframe/useIframe.ts b/apps/meteor/client/hooks/iframe/useIframe.ts index 095bffda917..8f0e2056241 100644 --- a/apps/meteor/client/hooks/iframe/useIframe.ts +++ b/apps/meteor/client/hooks/iframe/useIframe.ts @@ -22,7 +22,7 @@ export const useIframe = () => { }; } if ('loginToken' in tokenData) { - tokenLogin(tokenData.loginToken); + tokenLogin(tokenData.loginToken, callback); } if ('token' in tokenData) { iframeLogin(tokenData.token, callback); diff --git a/apps/meteor/client/providers/AuthenticationProvider/AuthenticationProvider.tsx b/apps/meteor/client/providers/AuthenticationProvider/AuthenticationProvider.tsx index ca5edfdc599..8b5d8d90460 100644 --- a/apps/meteor/client/providers/AuthenticationProvider/AuthenticationProvider.tsx +++ b/apps/meteor/client/providers/AuthenticationProvider/AuthenticationProvider.tsx @@ -41,10 +41,12 @@ const AuthenticationProvider = ({ children }: AuthenticationProviderProps): Reac const contextValue = useMemo( (): ContextType => ({ isLoggingIn, - loginWithToken: (token: string): Promise => + loginWithToken: (token: string, callback): Promise => new Promise((resolve, reject) => Meteor.loginWithToken(token, (err) => { if (err) { + console.error(err); + callback?.(err); return reject(err); } resolve(undefined); diff --git a/apps/meteor/tests/e2e/fixtures/files/iframe-login.html b/apps/meteor/tests/e2e/fixtures/files/iframe-login.html new file mode 100644 index 00000000000..59d0c5f13f3 --- /dev/null +++ b/apps/meteor/tests/e2e/fixtures/files/iframe-login.html @@ -0,0 +1,37 @@ + + + + + Iframe Login + + + +

Iframe Authentication Login Form

+
+
+

+ +
+

+ + +
+
+ + diff --git a/apps/meteor/tests/e2e/iframe-authentication.spec.ts b/apps/meteor/tests/e2e/iframe-authentication.spec.ts new file mode 100644 index 00000000000..0b5e1cb3887 --- /dev/null +++ b/apps/meteor/tests/e2e/iframe-authentication.spec.ts @@ -0,0 +1,151 @@ +import fs from 'fs'; +import path from 'path'; + +import { Users } from './fixtures/userStates'; +import { Utils, Registration } from './page-objects'; +import { test, expect } from './utils/test'; + +const IFRAME_URL = 'http://iframe.rocket.chat'; +const API_URL = 'http://auth.rocket.chat/api/login'; + +test.describe('iframe-authentication', () => { + let poRegistration: Registration; + let poUtils: Utils; + + test.beforeAll(async ({ api }) => { + await api.post('/settings/Accounts_iframe_enabled', { value: true }); + await api.post('/settings/Accounts_iframe_url', { value: IFRAME_URL }); + await api.post('/settings/Accounts_Iframe_api_url', { value: API_URL }); + await api.post('/settings/Accounts_Iframe_api_method', { value: 'POST' }); + }); + + test.afterAll(async ({ api }) => { + await api.post('/settings/Accounts_iframe_enabled', { value: false }); + await api.post('/settings/Accounts_iframe_url', { value: '' }); + await api.post('/settings/Accounts_Iframe_api_url', { value: '' }); + await api.post('/settings/Accounts_Iframe_api_method', { value: '' }); + }); + + test.beforeEach(async ({ page }) => { + poRegistration = new Registration(page); + poUtils = new Utils(page); + + await page.route(API_URL, async (route) => { + await route.fulfill({ + status: 200, + }); + }); + + const htmlContent = fs + .readFileSync(path.resolve(__dirname, 'fixtures/files/iframe-login.html'), 'utf-8') + .replace('REPLACE_WITH_TOKEN', Users.user1.data.loginToken); + + await page.route(IFRAME_URL, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'text/html', + body: htmlContent, + }); + }); + }); + + test('should render iframe instead of login page', async ({ page }) => { + await page.goto('/home'); + + await expect(poRegistration.loginIframeForm).toBeVisible(); + }); + + test('should render iframe login page if API returns error', async ({ page }) => { + await page.route(API_URL, async (route) => { + await route.fulfill({ + status: 500, + }); + }); + + await page.goto('/home'); + + await expect(poRegistration.loginIframeForm).toBeVisible(); + }); + + test('should login with token when API returns valid token', async ({ page }) => { + await page.route(API_URL, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ loginToken: Users.user1.data.loginToken }), + }); + }); + + await page.goto('/home'); + await expect(poUtils.mainContent).toBeVisible(); + }); + + test('should show login page when API returns invalid token', async ({ page }) => { + await page.route(API_URL, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ loginToken: 'invalid-token' }), + }); + }); + + await page.goto('/home'); + await expect(poRegistration.loginIframeForm).toBeVisible(); + }); + + test('should login through iframe', async ({ page }) => { + await page.goto('/home'); + + await expect(poRegistration.loginIframeForm).toBeVisible(); + + await poRegistration.loginIframeSubmitButton.click(); + + await expect(poUtils.mainContent).toBeVisible(); + }); + + test('should return error to iframe when login fails', async ({ page }) => { + const htmlContent = fs.readFileSync(path.resolve(__dirname, 'fixtures/files/iframe-login.html'), 'utf-8'); + + await page.route(IFRAME_URL, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'text/html', + body: htmlContent, + }); + }); + + await page.goto('/home'); + + await expect(poRegistration.loginIframeForm).toBeVisible(); + + await poRegistration.loginIframeSubmitButton.click(); + + await expect(poRegistration.loginIframeError).toBeVisible(); + }); + + test.describe('incomplete settings', () => { + test.beforeAll(async ({ api }) => { + await api.post('/settings/Accounts_Iframe_api_url', { value: '' }); + }); + + test.afterAll(async ({ api }) => { + await api.post('/settings/Accounts_Iframe_api_url', { value: API_URL }); + }); + + test('should render default login page, if settings are incomplete', async ({ page }) => { + const htmlContent = fs.readFileSync(path.resolve(__dirname, 'fixtures/files/iframe-login.html'), 'utf-8'); + + await page.route(IFRAME_URL, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'text/html', + body: htmlContent, + }); + }); + + await page.goto('/home'); + await expect(poRegistration.btnLogin).toBeVisible(); + await expect(poRegistration.loginIframeForm).not.toBeVisible(); + }); + }); +}); diff --git a/apps/meteor/tests/e2e/page-objects/auth.ts b/apps/meteor/tests/e2e/page-objects/auth.ts index cd80137743d..4d63e8b6982 100644 --- a/apps/meteor/tests/e2e/page-objects/auth.ts +++ b/apps/meteor/tests/e2e/page-objects/auth.ts @@ -1,4 +1,4 @@ -import type { Locator, Page } from '@playwright/test'; +import type { FrameLocator, Locator, Page } from '@playwright/test'; export class Registration { private readonly page: Page; @@ -78,4 +78,20 @@ export class Registration { get registrationDisabledCallout(): Locator { return this.page.locator('role=status >> text=/New user registration is currently disabled/'); } + + get loginIframe(): FrameLocator { + return this.page.frameLocator('iframe[title="Login"]'); + } + + get loginIframeForm(): Locator { + return this.loginIframe.locator('#login-form'); + } + + get loginIframeSubmitButton(): Locator { + return this.loginIframe.locator('#submit'); + } + + get loginIframeError(): Locator { + return this.loginIframe.locator('#login-error', { hasText: 'Login failed' }); + } } diff --git a/packages/ui-contexts/src/AuthenticationContext.ts b/packages/ui-contexts/src/AuthenticationContext.ts index 93c6f9c479f..83c430ec807 100644 --- a/packages/ui-contexts/src/AuthenticationContext.ts +++ b/packages/ui-contexts/src/AuthenticationContext.ts @@ -9,7 +9,7 @@ export type LoginService = LoginServiceConfiguration & { export type AuthenticationContextValue = { readonly isLoggingIn: boolean; loginWithPassword: (user: string | { username: string } | { email: string } | { id: string }, password: string) => Promise; - loginWithToken: (user: string) => Promise; + loginWithToken: (user: string, callback?: (error: Error | null | undefined) => void) => Promise; loginWithService(service: T): () => Promise; loginWithIframe: (token: string, callback?: (error: Error | null | undefined) => void) => Promise; loginWithTokenRoute: (token: string, callback?: (error: Error | null | undefined) => void) => Promise;