snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
This commit is contained in:
359
react/features/authentication/actions.web.ts
Normal file
359
react/features/authentication/actions.web.ts
Normal file
@@ -0,0 +1,359 @@
|
||||
import { maybeRedirectToWelcomePage } from '../app/actions.web';
|
||||
import { IStore } from '../app/types';
|
||||
import { connect } from '../base/connection/actions';
|
||||
import { openDialog } from '../base/dialog/actions';
|
||||
import { setJWT } from '../base/jwt/actions';
|
||||
import { browser } from '../base/lib-jitsi-meet';
|
||||
import { hideNotification, showErrorNotification, showNotification } from '../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../notifications/constants';
|
||||
|
||||
import { CANCEL_LOGIN } from './actionTypes';
|
||||
import LoginQuestionDialog from './components/web/LoginQuestionDialog';
|
||||
import { isTokenAuthInline } from './functions.any';
|
||||
import logger from './logger';
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
const PROMPT_POPUP_NOTIFICATION_ID = 'PROMPT_POPUP_NOTIFICATION_ID';
|
||||
|
||||
/**
|
||||
* Custom error that includes a recovery callback.
|
||||
*/
|
||||
class PopupBlockedError extends Error {
|
||||
constructor(
|
||||
message: string = 'Popup was blocked by browser',
|
||||
public readonly retry?: () => Window | null
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'PopupBlockedError';
|
||||
Error.captureStackTrace(this, PopupBlockedError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom error thrown when a pending inline login popup is closed
|
||||
* programmatically (e.g. the participant is admitted to the conference while
|
||||
* the popup is still open), as opposed to failing to authenticate.
|
||||
*/
|
||||
class LoginCancelledError extends Error {
|
||||
constructor(message: string = 'Login cancelled') {
|
||||
super(message);
|
||||
this.name = 'LoginCancelledError';
|
||||
Error.captureStackTrace(this, LoginCancelledError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds a function that closes the currently open inline login popup (if any)
|
||||
* and settles its pending promise. It is set while a popup is open and cleared
|
||||
* once the popup is closed, so it can be used to dismiss the popup when it is no
|
||||
* longer needed (e.g. when the participant is admitted to the conference).
|
||||
*/
|
||||
let closeActiveLoginPopup: (() => void) | undefined;
|
||||
|
||||
/**
|
||||
* Cancels {@ink LoginDialog}.
|
||||
*
|
||||
* @returns {{
|
||||
* type: CANCEL_LOGIN
|
||||
* }}
|
||||
*/
|
||||
export function cancelLogin() {
|
||||
return {
|
||||
type: CANCEL_LOGIN
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels authentication, closes {@link WaitForOwnerDialog}
|
||||
* and navigates back to the welcome page only in the case of authentication required error.
|
||||
* We can be showing the dialog while lobby is enabled and participant is still waiting there and hiding this dialog
|
||||
* should do nothing.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function cancelWaitForOwner() {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const { authRequired } = getState()['features/base/conference'];
|
||||
|
||||
authRequired && dispatch(maybeRedirectToWelcomePage());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the default location (e.g. Welcome page).
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function redirectToDefaultLocation() {
|
||||
return (dispatch: IStore['dispatch']) => dispatch(maybeRedirectToWelcomePage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a cryptographic nonce.
|
||||
*
|
||||
* @returns {string} The generated nonce.
|
||||
*/
|
||||
function generateNonce(): string {
|
||||
const array = new Uint8Array(32);
|
||||
|
||||
crypto.getRandomValues(array);
|
||||
|
||||
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs login with a popup window.
|
||||
*
|
||||
* @param {string} tokenAuthServiceUrl - Authentication service URL.
|
||||
* @param {Window|undefined} popup - Authentication service URL.
|
||||
* @returns {Promise<any>} A promise that resolves with the authentication
|
||||
* result or rejects with an error.
|
||||
*/
|
||||
export function loginWithPopup(tokenAuthServiceUrl: string, popup?: Window | null): Promise<any> {
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
let nonceParam = '';
|
||||
|
||||
try {
|
||||
const nonce = generateNonce();
|
||||
|
||||
sessionStorage.setItem('oauth_nonce', nonce);
|
||||
|
||||
nonceParam = `&nonce=${nonce}`;
|
||||
} catch (e) {
|
||||
if (e instanceof DOMException && e.name === 'SecurityError') {
|
||||
logger.warn(
|
||||
'sessionStorage access denied (cross-origin or restricted context) enable it to improve security',
|
||||
e);
|
||||
} else {
|
||||
logger.error('Unable to save nonce in session storage', e);
|
||||
}
|
||||
}
|
||||
|
||||
const openPopup = () => {
|
||||
// Open popup
|
||||
const width = 500;
|
||||
const height = 600;
|
||||
const left = window.screen.width / 2 - width / 2;
|
||||
const top = window.screen.height / 2 - height / 2;
|
||||
|
||||
return window.open(
|
||||
`${tokenAuthServiceUrl}${nonceParam}`,
|
||||
`Auth-${Date.now()}`,
|
||||
`width=${width},height=${height},left=${left},top=${top}`
|
||||
);
|
||||
};
|
||||
|
||||
if (!popup) {
|
||||
popup = openPopup();
|
||||
}
|
||||
|
||||
if (!popup) {
|
||||
// adds a callback to the error that can be used to directly retry where window.open will be executed
|
||||
// on the user click without any promises(dispatches) incoved
|
||||
reject(new PopupBlockedError('Popup blocked', () => {
|
||||
return openPopup();
|
||||
}));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let closedPollInterval: ReturnType<typeof setInterval> | undefined = undefined;
|
||||
const cleanup = (handler: (event: MessageEvent) => void) => {
|
||||
window.removeEventListener('message', handler);
|
||||
clearInterval(closedPollInterval);
|
||||
popup?.close();
|
||||
closeActiveLoginPopup = undefined;
|
||||
|
||||
try {
|
||||
sessionStorage.removeItem('oauth_nonce');
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
sessionStorage.removeItem('code_verifier');
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const handler = (event: MessageEvent) => {
|
||||
// Verify origin
|
||||
if (event.origin !== window.location.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data.type === 'oauth-success') {
|
||||
cleanup(handler);
|
||||
|
||||
resolve({
|
||||
accessToken: event.data.accessToken,
|
||||
idToken: event.data.idToken,
|
||||
refreshToken: event.data.refreshToken
|
||||
});
|
||||
} else if (event.data.type === 'oauth-error') {
|
||||
cleanup(handler);
|
||||
|
||||
reject(new Error(event.data.error));
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for messages from the popup
|
||||
window.addEventListener('message', handler);
|
||||
|
||||
// Expose a way to close this popup externally, so it can be dismissed
|
||||
// when it is no longer needed (e.g. the participant was admitted to the
|
||||
// conference while the popup was still open).
|
||||
closeActiveLoginPopup = () => {
|
||||
cleanup(handler);
|
||||
reject(new LoginCancelledError());
|
||||
};
|
||||
|
||||
// Detect manual popup close before authentication completes
|
||||
closedPollInterval = setInterval(() => {
|
||||
if (popup?.closed) {
|
||||
cleanup(handler);
|
||||
reject(new Error('Login cancelled'));
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs silent logout by loading the token authentication logout service URL in an
|
||||
* invisible iframe.
|
||||
*
|
||||
* @param {string} tokenAuthLogoutServiceUrl - Logout service URL.
|
||||
* @returns {Promise<any>} A promise that resolves when logout is complete.
|
||||
*/
|
||||
export function silentLogout(tokenAuthLogoutServiceUrl: string): any {
|
||||
return new Promise<void>(resolve => {
|
||||
const iframe = document.createElement('iframe');
|
||||
|
||||
iframe.style.display = 'none';
|
||||
iframe.src = tokenAuthLogoutServiceUrl;
|
||||
document.body.appendChild(iframe);
|
||||
|
||||
// Listen for logout completion
|
||||
const handler = (event: any) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
|
||||
if (event.data.type === 'logout-success') {
|
||||
window.removeEventListener('message', handler);
|
||||
document.body.removeChild(iframe);
|
||||
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', handler);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the currently open inline login popup (if any) and settles its pending
|
||||
* promise. Used to dismiss the popup when it is no longer needed, e.g. when the
|
||||
* participant is admitted to the conference while the popup is still open.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
export function closeLoginPopup() {
|
||||
closeActiveLoginPopup?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens token auth URL page.
|
||||
*
|
||||
* @param {string} tokenAuthServiceUrl - Authentication service URL.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function openTokenAuthUrl(tokenAuthServiceUrl: string): any {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const redirect = () => {
|
||||
if (browser.isElectron()) {
|
||||
window.open(tokenAuthServiceUrl, '_blank');
|
||||
} else {
|
||||
window.location.href = tokenAuthServiceUrl;
|
||||
}
|
||||
};
|
||||
|
||||
if (!browser.isElectron() && isTokenAuthInline(getState()['features/base/config'])) {
|
||||
const login = (popup?: Window | null) => {
|
||||
return loginWithPopup(tokenAuthServiceUrl, popup)
|
||||
.then((result: { accessToken: string; idToken: string; refreshToken?: string; }) => {
|
||||
// @ts-ignore
|
||||
const token: string = result.accessToken;
|
||||
const idToken: string = result.idToken;
|
||||
const refreshToken: string | undefined = result.refreshToken;
|
||||
|
||||
// @ts-ignore
|
||||
dispatch(setJWT(token, idToken, refreshToken));
|
||||
|
||||
logger.info('Reconnecting to conference with new token.');
|
||||
|
||||
const { connection } = getState()['features/base/connection'];
|
||||
|
||||
if (connection) {
|
||||
connection.refreshToken(token).then(
|
||||
() => {
|
||||
const { membersOnly } = getState()['features/base/conference'];
|
||||
|
||||
membersOnly?.join();
|
||||
})
|
||||
.catch((err: any) => {
|
||||
dispatch(setJWT());
|
||||
logger.error(err);
|
||||
});
|
||||
} else {
|
||||
dispatch(connect());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
login().catch(err => {
|
||||
if (err instanceof PopupBlockedError) {
|
||||
dispatch(showNotification({
|
||||
titleKey: 'dialog.loginPopupBlocked',
|
||||
uid: PROMPT_POPUP_NOTIFICATION_ID,
|
||||
customActionNameKey: [ 'dialog.retry' ],
|
||||
customActionHandler: [ () => {
|
||||
dispatch(hideNotification(PROMPT_POPUP_NOTIFICATION_ID));
|
||||
|
||||
// the window.open will be executed directly from the onClick handler of the notification action button
|
||||
login(err.retry ? err.retry() : undefined)
|
||||
.catch(logger.error);
|
||||
} ],
|
||||
appearance: NOTIFICATION_TYPE.ERROR
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
logger.error(err);
|
||||
} else if (err instanceof LoginCancelledError) {
|
||||
// The popup was closed programmatically (e.g. we were
|
||||
// admitted to the conference while it was still open); this
|
||||
// is not an authentication failure, so nothing to report.
|
||||
logger.info('Inline login popup closed:', err.message);
|
||||
} else {
|
||||
// let's add expand that will show the error message in the notification
|
||||
dispatch(showErrorNotification({
|
||||
titleKey: 'dialog.loginFailed'
|
||||
}));
|
||||
logger.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Show warning for leaving conference only when in a conference.
|
||||
if (!browser.isElectron() && getState()['features/base/conference'].conference) {
|
||||
dispatch(openDialog('LoginQuestionDialog', LoginQuestionDialog, {
|
||||
handler: () => {
|
||||
// Give time for the dialog to close.
|
||||
setTimeout(() => redirect(), 500);
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
redirect();
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user