Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/rich-parrots-lie.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion apps/meteor/client/hooks/iframe/useIframe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@ const AuthenticationProvider = ({ children }: AuthenticationProviderProps): Reac
const contextValue = useMemo(
(): ContextType<typeof AuthenticationContext> => ({
isLoggingIn,
loginWithToken: (token: string): Promise<void> =>
loginWithToken: (token: string, callback): Promise<void> =>
new Promise((resolve, reject) =>
Meteor.loginWithToken(token, (err) => {
if (err) {
console.error(err);
callback?.(err);
return reject(err);
}
resolve(undefined);
Expand Down
37 changes: 37 additions & 0 deletions apps/meteor/tests/e2e/fixtures/files/iframe-login.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Iframe Login</title>
</head>
<script>
function login() {
console.log('logging in');
window.parent.postMessage({
event: 'login-with-token',
loginToken: 'REPLACE_WITH_TOKEN',
}, '*');
}

window.addEventListener('message', (event) => {
if(event.data.event === 'login-error') {
document.getElementById('login-error').innerText = "Login failed";
}
});
</script>
<body>
<h1>Iframe Authentication Login Form</h1>
<form id="login-form">
<label for="username">Username:</label><br/>
<input type="text" id="username" name="username" placeholder="Enter username" /><br/><br/>

<label for="password">Password:</label><br/>
<input type="password" id="password" name="password" placeholder="Enter password" /><br/><br/>

<button id="submit" type="button"

onclick="login()">Login</button>
</form>
<div id="login-error"></div>
</body>
</html>
151 changes: 151 additions & 0 deletions apps/meteor/tests/e2e/iframe-authentication.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
18 changes: 17 additions & 1 deletion apps/meteor/tests/e2e/page-objects/auth.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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' });
}
}
2 changes: 1 addition & 1 deletion packages/ui-contexts/src/AuthenticationContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
loginWithToken: (user: string) => Promise<void>;
loginWithToken: (user: string, callback?: (error: Error | null | undefined) => void) => Promise<void>;
loginWithService<T extends LoginServiceConfiguration>(service: T): () => Promise<true>;
loginWithIframe: (token: string, callback?: (error: Error | null | undefined) => void) => Promise<void>;
loginWithTokenRoute: (token: string, callback?: (error: Error | null | undefined) => void) => Promise<void>;
Expand Down
Loading