Skip to content

Commit 7535a5b

Browse files
committed
fix(authentication): Implements inline authentication.
squash: Adds refresh token use when refresh token is needed on connection resuming. squash: Fix bugs and move to PKCE flow.
1 parent 7cbc638 commit 7535a5b

28 files changed

+931
-196
lines changed

config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1600,6 +1600,8 @@ var config = {
16001600
// An option to get for user info (name, picture, email) in the token outside the user context.
16011601
// Can be used with Firebase tokens.
16021602
// tokenGetUserInfoOutOfContext: false,
1603+
// An option to pass the token in the iframe API directly instead of using the redirect flow.
1604+
// tokenAuthInline: false,
16031605

16041606
// You can put an array of values to target different entity types in the invite dialog.
16051607
// Valid values are "phone", "room", "sip", "user", "videosipgw" and "email"

lang/main.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,8 @@
383383
"lockRoom": "Add meeting $t(lockRoomPassword)",
384384
"lockTitle": "Lock failed",
385385
"login": "Login",
386+
"loginFailed": "Login failed.",
387+
"loginOnResume": "Your authentication session is about to expire. You need to login again to continue the meeting.",
386388
"loginQuestion": "Are you sure you want to login and leave the conference?",
387389
"logoutQuestion": "Are you sure you want to logout and leave the conference?",
388390
"logoutTitle": "Logout",

react/features/app/actions.any.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export function maybeRedirectToTokenAuthUrl(
119119

120120
// if tokenAuthUrl check jwt if is about to expire go through the url to get new token
121121
const jwt = state['features/base/jwt'].jwt;
122+
const refreshToken = state['features/base/jwt'].refreshToken;
122123
const expirationDate = getJwtExpirationDate(jwt);
123124

124125
// if there is jwt and its expiration time is less than 3 minutes away
@@ -137,7 +138,8 @@ export function maybeRedirectToTokenAuthUrl(
137138
videoMuted
138139
},
139140
room,
140-
tenant
141+
tenant,
142+
refreshToken
141143
)
142144
.then((tokenAuthServiceUrl: string | undefined) => {
143145
if (!tokenAuthServiceUrl) {

react/features/app/getRouteToRender.native.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ const route = {
1111
* store.
1212
*
1313
* @param {any} _stateful - Used on web.
14+
* @param {any} _dispatch - Used on web.
1415
* @returns {Promise<Object>}
1516
*/
16-
export function _getRouteToRender(_stateful?: any) {
17+
export function _getRouteToRender(_stateful?: any): Promise<object> {
1718
return Promise.resolve(route);
1819
}

react/features/app/getRouteToRender.web.ts

Lines changed: 4 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
// @ts-expect-error
22
import { generateRoomWithoutSeparator } from '@jitsi/js-utils/random';
33

4-
import { getTokenAuthUrl } from '../authentication/functions.web';
54
import { IStateful } from '../base/app/types';
65
import { isRoomValid } from '../base/conference/functions';
76
import { isSupportedBrowser } from '../base/environment/environment';
8-
import { browser } from '../base/lib-jitsi-meet';
97
import { toState } from '../base/redux/functions';
10-
import { parseURIString } from '../base/util/uri';
118
import Conference from '../conference/components/web/Conference';
129
import { getDeepLinkingPage } from '../deep-linking/functions';
1310
import UnsupportedDesktopBrowser from '../unsupported-browser/components/UnsupportedDesktopBrowser';
@@ -23,9 +20,10 @@ import { IReduxState } from './types';
2320
*
2421
* @param {(Function|Object)} stateful - THe redux store, state, or
2522
* {@code getState} function.
23+
* @param {Dispatch} dispatch - The Redux dispatch function.
2624
* @returns {Promise<Object>}
2725
*/
28-
export function _getRouteToRender(stateful: IStateful) {
26+
export function _getRouteToRender(stateful: IStateful): Promise<object> {
2927
const state = toState(stateful);
3028

3129
return _getWebConferenceRoute(state) || _getWebWelcomePageRoute(state);
@@ -36,46 +34,17 @@ export function _getRouteToRender(stateful: IStateful) {
3634
* a valid conference is being joined.
3735
*
3836
* @param {Object} state - The redux state.
37+
* @param {Dispatch} dispatch - The Redux dispatch function.
3938
* @returns {Promise|undefined}
4039
*/
41-
function _getWebConferenceRoute(state: IReduxState) {
40+
function _getWebConferenceRoute(state: IReduxState): Promise<any> | undefined {
4241
const room = state['features/base/conference'].room;
4342

4443
if (!isRoomValid(room)) {
4544
return;
4645
}
4746

4847
const route = _getEmptyRoute();
49-
const config = state['features/base/config'];
50-
51-
// if we have auto redirect enabled, and we have previously logged in successfully
52-
// let's redirect to the auth url to get the token and login again
53-
if (!browser.isElectron() && config.tokenAuthUrl && config.tokenAuthUrlAutoRedirect
54-
&& state['features/authentication'].tokenAuthUrlSuccessful
55-
&& !state['features/base/jwt'].jwt && room) {
56-
const { locationURL = { href: '' } as URL } = state['features/base/connection'];
57-
const { tenant } = parseURIString(locationURL.href) || {};
58-
const { startAudioOnly } = config;
59-
60-
return getTokenAuthUrl(
61-
config,
62-
locationURL,
63-
{
64-
audioMuted: false,
65-
audioOnlyEnabled: startAudioOnly,
66-
skipPrejoin: false,
67-
videoMuted: false
68-
},
69-
room,
70-
tenant
71-
)
72-
.then((url: string | undefined) => {
73-
route.href = url;
74-
75-
return route;
76-
})
77-
.catch(() => Promise.resolve(route));
78-
}
7948

8049
// Update the location if it doesn't match. This happens when a room is
8150
// joined from the welcome page. The reason for doing this instead of using

react/features/authentication/actions.web.ts

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { maybeRedirectToWelcomePage } from '../app/actions.web';
22
import { IStore } from '../app/types';
33
import { openDialog } from '../base/dialog/actions';
4+
import { setJWT } from '../base/jwt/actions';
45
import { browser } from '../base/lib-jitsi-meet';
6+
import { showErrorNotification } from '../notifications/actions';
57

68
import { CANCEL_LOGIN } from './actionTypes';
79
import LoginQuestionDialog from './components/web/LoginQuestionDialog';
10+
import { isTokenAuthInline } from './functions.any';
11+
import logger from './logger';
812

913
export * from './actions.any';
1014

@@ -46,6 +50,134 @@ export function redirectToDefaultLocation() {
4650
return (dispatch: IStore['dispatch']) => dispatch(maybeRedirectToWelcomePage());
4751
}
4852

53+
/**
54+
* Generates a cryptographic nonce.
55+
*
56+
* @returns {string} The generated nonce.
57+
*/
58+
function generateNonce(): string {
59+
const array = new Uint8Array(32);
60+
61+
crypto.getRandomValues(array);
62+
63+
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
64+
}
65+
66+
/**
67+
* Performs login with a popup window.
68+
*
69+
* @param {string} tokenAuthServiceUrl - Authentication service URL.
70+
* @returns {Promise<any>} A promise that resolves with the authentication
71+
* result or rejects with an error.
72+
*/
73+
export function loginWithPopup(tokenAuthServiceUrl: string): Promise<any> {
74+
return new Promise<any>((resolve, reject) => {
75+
// Open popup
76+
const width = 500;
77+
const height = 600;
78+
const left = window.screen.width / 2 - width / 2;
79+
const top = window.screen.height / 2 - height / 2;
80+
81+
const nonce = generateNonce();
82+
83+
sessionStorage.setItem('oauth_nonce', nonce);
84+
85+
const popup = window.open(
86+
`${tokenAuthServiceUrl}&nonce=${nonce}`,
87+
`Auth-${Date.now()}`,
88+
`width=${width},height=${height},left=${left},top=${top}`
89+
);
90+
91+
if (!popup) {
92+
reject(new Error('Popup blocked'));
93+
94+
return;
95+
}
96+
97+
// @ts-ignore
98+
const handler = event => {
99+
// Verify origin
100+
if (event.origin !== window.location.origin) {
101+
return;
102+
}
103+
104+
if (event.data.type === 'oauth-success') {
105+
window.removeEventListener('message', handler);
106+
popup.close();
107+
108+
sessionStorage.removeItem('oauth_nonce');
109+
110+
resolve({
111+
accessToken: event.data.accessToken,
112+
idToken: event.data.idToken,
113+
refreshToken: event.data.refreshToken
114+
});
115+
} else if (event.data.type === 'oauth-error') {
116+
window.removeEventListener('message', handler);
117+
popup.close();
118+
reject(new Error(event.data.error));
119+
}
120+
};
121+
122+
// Listen for messages from the popup
123+
window.addEventListener('message', handler);
124+
125+
// Check if popup was closed
126+
const checkClosed = setInterval(() => {
127+
if (popup.closed) {
128+
clearInterval(checkClosed);
129+
window.removeEventListener('message', handler);
130+
reject(new Error('Login cancelled'));
131+
}
132+
}, 1000);
133+
});
134+
}
135+
136+
/**
137+
* Performs silent logout by loading the token authentication logout service URL in an
138+
* invisible iframe.
139+
*
140+
* @param {string} tokenAuthLogoutServiceUrl - Logout service URL.
141+
* @returns {Promise<any>} A promise that resolves when logout is complete.
142+
*/
143+
export function silentLogout(tokenAuthLogoutServiceUrl: string): any {
144+
return new Promise<void>(resolve => {
145+
const iframe = document.createElement('iframe');
146+
147+
iframe.style.display = 'none';
148+
iframe.src = tokenAuthLogoutServiceUrl;
149+
document.body.appendChild(iframe);
150+
151+
let timerId: any = undefined;
152+
153+
// Listen for logout completion
154+
// @ts-ignore
155+
const handler = event => {
156+
if (event.origin !== window.location.origin) return;
157+
158+
if (event.data.type === 'logout-success') {
159+
window.removeEventListener('message', handler);
160+
document.body.removeChild(iframe);
161+
162+
timerId && clearTimeout(timerId);
163+
164+
resolve();
165+
}
166+
};
167+
168+
window.addEventListener('message', handler);
169+
170+
// Fallback timeout
171+
timerId = setTimeout(() => {
172+
window.removeEventListener('message', handler);
173+
if (iframe.parentNode) {
174+
document.body.removeChild(iframe);
175+
}
176+
resolve(); // Assume success after timeout
177+
}, 3000);
178+
});
179+
}
180+
49181
/**
50182
* Opens token auth URL page.
51183
*
@@ -63,6 +195,42 @@ export function openTokenAuthUrl(tokenAuthServiceUrl: string): any {
63195
}
64196
};
65197

198+
if (!browser.isElectron() && isTokenAuthInline(getState()['features/base/config'])) {
199+
loginWithPopup(tokenAuthServiceUrl)
200+
.then((result: { accessToken: string; idToken: string; refreshToken?: string; }) => {
201+
// @ts-ignore
202+
const token: string = result.accessToken;
203+
const idToken: string = result.idToken;
204+
const refreshToken: string | undefined = result.refreshToken;
205+
206+
// @ts-ignore
207+
dispatch(setJWT(token, idToken, refreshToken));
208+
209+
logger.info('Reconnecting to conference with new token.');
210+
211+
const { connection } = getState()['features/base/connection'];
212+
213+
connection?.refreshToken(token).then(
214+
() => {
215+
const { membersOnly } = getState()['features/base/conference'];
216+
217+
membersOnly?.join();
218+
})
219+
.catch((err: any) => {
220+
dispatch(setJWT());
221+
logger.error(err);
222+
});
223+
})
224+
.catch(err => {
225+
dispatch(showErrorNotification({
226+
titleKey: 'dialog.loginFailed'
227+
}));
228+
logger.error(err);
229+
});
230+
231+
return;
232+
}
233+
66234
// Show warning for leaving conference only when in a conference.
67235
if (!browser.isElectron() && getState()['features/base/conference'].conference) {
68236
dispatch(openDialog('LoginQuestionDialog', LoginQuestionDialog, {

react/features/authentication/functions.any.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ import { getBackendSafeRoomName } from '../base/util/uri';
1111
export const isTokenAuthEnabled = (config: IConfig): boolean =>
1212
typeof config.tokenAuthUrl === 'string' && config.tokenAuthUrl.length > 0;
1313

14+
/**
15+
* Checks if the token authentication should be done inline.
16+
*
17+
* @param {Object} config - Configuration state object from store.
18+
* @returns {boolean}
19+
*/
20+
export const isTokenAuthInline = (config: IConfig): boolean =>
21+
config.tokenAuthInline === true;
22+
1423
/**
1524
* Returns the state that we can add as a parameter to the tokenAuthUrl.
1625
*
@@ -23,6 +32,7 @@ export const isTokenAuthEnabled = (config: IConfig): boolean =>
2332
* }.
2433
* @param {string?} roomName - The room name.
2534
* @param {string?} tenant - The tenant name if any.
35+
* @param {string?} refreshToken - The refresh token if available.
2636
*
2737
* @returns {Object} The state object.
2838
*/
@@ -35,13 +45,18 @@ export const _getTokenAuthState = (
3545
videoMuted: boolean | undefined;
3646
},
3747
roomName: string | undefined,
38-
tenant: string | undefined): object => {
39-
const state = {
48+
tenant: string | undefined,
49+
refreshToken?: string | undefined): object => {
50+
const state: any = {
4051
room: roomName,
4152
roomSafe: getBackendSafeRoomName(roomName),
4253
tenant
4354
};
4455

56+
if (refreshToken) {
57+
state.refreshToken = refreshToken;
58+
}
59+
4560
const {
4661
audioMuted = false,
4762
audioOnlyEnabled = false,

react/features/authentication/functions.web.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ function _cryptoRandom() {
4141
* }.
4242
* @param {string?} roomName - The room name.
4343
* @param {string?} tenant - The tenant name if any.
44+
* @param {string?} refreshToken - The refresh token if available.
4445
*
4546
* @returns {Promise<string|undefined>} - The URL pointing to JWT login service or
4647
* <tt>undefined</tt> if the pattern stored in config is not a string and the URL can not be
@@ -57,7 +58,9 @@ export const getTokenAuthUrl = (
5758
},
5859
roomName: string | undefined,
5960
// eslint-disable-next-line max-params
60-
tenant: string | undefined): Promise<string | undefined> => {
61+
tenant: string | undefined,
62+
// eslint-disable-next-line max-params
63+
refreshToken?: string | undefined): Promise<string | undefined> => {
6164

6265
const {
6366
audioMuted = false,
@@ -82,7 +85,8 @@ export const getTokenAuthUrl = (
8285
videoMuted
8386
},
8487
roomName,
85-
tenant
88+
tenant,
89+
refreshToken
8690
);
8791

8892
if (browser.isElectron()) {

0 commit comments

Comments
 (0)