Skip to content

Commit feba290

Browse files
authored
fix: Show iframe auth when login with token fails (RocketChat#36919)
1 parent c5ee569 commit feba290

7 files changed

Lines changed: 216 additions & 4 deletions

File tree

.changeset/rich-parrots-lie.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@rocket.chat/ui-contexts': patch
3+
'@rocket.chat/meteor': patch
4+
---
5+
6+
Show iframe authentication page, when login through iframe authentication API token fails

apps/meteor/client/hooks/iframe/useIframe.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const useIframe = () => {
2222
};
2323
}
2424
if ('loginToken' in tokenData) {
25-
tokenLogin(tokenData.loginToken);
25+
tokenLogin(tokenData.loginToken, callback);
2626
}
2727
if ('token' in tokenData) {
2828
iframeLogin(tokenData.token, callback);

apps/meteor/client/providers/AuthenticationProvider/AuthenticationProvider.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@ const AuthenticationProvider = ({ children }: AuthenticationProviderProps): Reac
4141
const contextValue = useMemo(
4242
(): ContextType<typeof AuthenticationContext> => ({
4343
isLoggingIn,
44-
loginWithToken: (token: string): Promise<void> =>
44+
loginWithToken: (token: string, callback): Promise<void> =>
4545
new Promise((resolve, reject) =>
4646
Meteor.loginWithToken(token, (err) => {
4747
if (err) {
48+
console.error(err);
49+
callback?.(err);
4850
return reject(err);
4951
}
5052
resolve(undefined);
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>Iframe Login</title>
6+
</head>
7+
<script>
8+
function login() {
9+
console.log('logging in');
10+
window.parent.postMessage({
11+
event: 'login-with-token',
12+
loginToken: 'REPLACE_WITH_TOKEN',
13+
}, '*');
14+
}
15+
16+
window.addEventListener('message', (event) => {
17+
if(event.data.event === 'login-error') {
18+
document.getElementById('login-error').innerText = "Login failed";
19+
}
20+
});
21+
</script>
22+
<body>
23+
<h1>Iframe Authentication Login Form</h1>
24+
<form id="login-form">
25+
<label for="username">Username:</label><br/>
26+
<input type="text" id="username" name="username" placeholder="Enter username" /><br/><br/>
27+
28+
<label for="password">Password:</label><br/>
29+
<input type="password" id="password" name="password" placeholder="Enter password" /><br/><br/>
30+
31+
<button id="submit" type="button"
32+
33+
onclick="login()">Login</button>
34+
</form>
35+
<div id="login-error"></div>
36+
</body>
37+
</html>
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
4+
import { Users } from './fixtures/userStates';
5+
import { Utils, Registration } from './page-objects';
6+
import { test, expect } from './utils/test';
7+
8+
const IFRAME_URL = 'http://iframe.rocket.chat';
9+
const API_URL = 'http://auth.rocket.chat/api/login';
10+
11+
test.describe('iframe-authentication', () => {
12+
let poRegistration: Registration;
13+
let poUtils: Utils;
14+
15+
test.beforeAll(async ({ api }) => {
16+
await api.post('/settings/Accounts_iframe_enabled', { value: true });
17+
await api.post('/settings/Accounts_iframe_url', { value: IFRAME_URL });
18+
await api.post('/settings/Accounts_Iframe_api_url', { value: API_URL });
19+
await api.post('/settings/Accounts_Iframe_api_method', { value: 'POST' });
20+
});
21+
22+
test.afterAll(async ({ api }) => {
23+
await api.post('/settings/Accounts_iframe_enabled', { value: false });
24+
await api.post('/settings/Accounts_iframe_url', { value: '' });
25+
await api.post('/settings/Accounts_Iframe_api_url', { value: '' });
26+
await api.post('/settings/Accounts_Iframe_api_method', { value: '' });
27+
});
28+
29+
test.beforeEach(async ({ page }) => {
30+
poRegistration = new Registration(page);
31+
poUtils = new Utils(page);
32+
33+
await page.route(API_URL, async (route) => {
34+
await route.fulfill({
35+
status: 200,
36+
});
37+
});
38+
39+
const htmlContent = fs
40+
.readFileSync(path.resolve(__dirname, 'fixtures/files/iframe-login.html'), 'utf-8')
41+
.replace('REPLACE_WITH_TOKEN', Users.user1.data.loginToken);
42+
43+
await page.route(IFRAME_URL, async (route) => {
44+
await route.fulfill({
45+
status: 200,
46+
contentType: 'text/html',
47+
body: htmlContent,
48+
});
49+
});
50+
});
51+
52+
test('should render iframe instead of login page', async ({ page }) => {
53+
await page.goto('/home');
54+
55+
await expect(poRegistration.loginIframeForm).toBeVisible();
56+
});
57+
58+
test('should render iframe login page if API returns error', async ({ page }) => {
59+
await page.route(API_URL, async (route) => {
60+
await route.fulfill({
61+
status: 500,
62+
});
63+
});
64+
65+
await page.goto('/home');
66+
67+
await expect(poRegistration.loginIframeForm).toBeVisible();
68+
});
69+
70+
test('should login with token when API returns valid token', async ({ page }) => {
71+
await page.route(API_URL, async (route) => {
72+
await route.fulfill({
73+
status: 200,
74+
contentType: 'application/json',
75+
body: JSON.stringify({ loginToken: Users.user1.data.loginToken }),
76+
});
77+
});
78+
79+
await page.goto('/home');
80+
await expect(poUtils.mainContent).toBeVisible();
81+
});
82+
83+
test('should show login page when API returns invalid token', async ({ page }) => {
84+
await page.route(API_URL, async (route) => {
85+
await route.fulfill({
86+
status: 200,
87+
contentType: 'application/json',
88+
body: JSON.stringify({ loginToken: 'invalid-token' }),
89+
});
90+
});
91+
92+
await page.goto('/home');
93+
await expect(poRegistration.loginIframeForm).toBeVisible();
94+
});
95+
96+
test('should login through iframe', async ({ page }) => {
97+
await page.goto('/home');
98+
99+
await expect(poRegistration.loginIframeForm).toBeVisible();
100+
101+
await poRegistration.loginIframeSubmitButton.click();
102+
103+
await expect(poUtils.mainContent).toBeVisible();
104+
});
105+
106+
test('should return error to iframe when login fails', async ({ page }) => {
107+
const htmlContent = fs.readFileSync(path.resolve(__dirname, 'fixtures/files/iframe-login.html'), 'utf-8');
108+
109+
await page.route(IFRAME_URL, async (route) => {
110+
await route.fulfill({
111+
status: 200,
112+
contentType: 'text/html',
113+
body: htmlContent,
114+
});
115+
});
116+
117+
await page.goto('/home');
118+
119+
await expect(poRegistration.loginIframeForm).toBeVisible();
120+
121+
await poRegistration.loginIframeSubmitButton.click();
122+
123+
await expect(poRegistration.loginIframeError).toBeVisible();
124+
});
125+
126+
test.describe('incomplete settings', () => {
127+
test.beforeAll(async ({ api }) => {
128+
await api.post('/settings/Accounts_Iframe_api_url', { value: '' });
129+
});
130+
131+
test.afterAll(async ({ api }) => {
132+
await api.post('/settings/Accounts_Iframe_api_url', { value: API_URL });
133+
});
134+
135+
test('should render default login page, if settings are incomplete', async ({ page }) => {
136+
const htmlContent = fs.readFileSync(path.resolve(__dirname, 'fixtures/files/iframe-login.html'), 'utf-8');
137+
138+
await page.route(IFRAME_URL, async (route) => {
139+
await route.fulfill({
140+
status: 200,
141+
contentType: 'text/html',
142+
body: htmlContent,
143+
});
144+
});
145+
146+
await page.goto('/home');
147+
await expect(poRegistration.btnLogin).toBeVisible();
148+
await expect(poRegistration.loginIframeForm).not.toBeVisible();
149+
});
150+
});
151+
});

apps/meteor/tests/e2e/page-objects/auth.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Locator, Page } from '@playwright/test';
1+
import type { FrameLocator, Locator, Page } from '@playwright/test';
22

33
export class Registration {
44
private readonly page: Page;
@@ -78,4 +78,20 @@ export class Registration {
7878
get registrationDisabledCallout(): Locator {
7979
return this.page.locator('role=status >> text=/New user registration is currently disabled/');
8080
}
81+
82+
get loginIframe(): FrameLocator {
83+
return this.page.frameLocator('iframe[title="Login"]');
84+
}
85+
86+
get loginIframeForm(): Locator {
87+
return this.loginIframe.locator('#login-form');
88+
}
89+
90+
get loginIframeSubmitButton(): Locator {
91+
return this.loginIframe.locator('#submit');
92+
}
93+
94+
get loginIframeError(): Locator {
95+
return this.loginIframe.locator('#login-error', { hasText: 'Login failed' });
96+
}
8197
}

packages/ui-contexts/src/AuthenticationContext.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export type LoginService = LoginServiceConfiguration & {
99
export type AuthenticationContextValue = {
1010
readonly isLoggingIn: boolean;
1111
loginWithPassword: (user: string | { username: string } | { email: string } | { id: string }, password: string) => Promise<void>;
12-
loginWithToken: (user: string) => Promise<void>;
12+
loginWithToken: (user: string, callback?: (error: Error | null | undefined) => void) => Promise<void>;
1313
loginWithService<T extends LoginServiceConfiguration>(service: T): () => Promise<true>;
1414
loginWithIframe: (token: string, callback?: (error: Error | null | undefined) => void) => Promise<void>;
1515
loginWithTokenRoute: (token: string, callback?: (error: Error | null | undefined) => void) => Promise<void>;

0 commit comments

Comments
 (0)