Skip to content
Open
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
2 changes: 2 additions & 0 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -1600,6 +1600,8 @@ var config = {
// An option to get for user info (name, picture, email) in the token outside the user context.
// Can be used with Firebase tokens.
// tokenGetUserInfoOutOfContext: false,
// An option to pass the token in the iframe API directly instead of using the redirect flow.
// tokenAuthInline: false,

// You can put an array of values to target different entity types in the invite dialog.
// Valid values are "phone", "room", "sip", "user", "videosipgw" and "email"
Expand Down
2 changes: 2 additions & 0 deletions lang/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,8 @@
"lockRoom": "Add meeting $t(lockRoomPassword)",
"lockTitle": "Lock failed",
"login": "Login",
"loginFailed": "Login failed.",
"loginOnResume": "Your authentication session has expired. You need to login again to continue the meeting.",
"loginQuestion": "Are you sure you want to login and leave the conference?",
"logoutQuestion": "Are you sure you want to logout and leave the conference?",
"logoutTitle": "Logout",
Expand Down
4 changes: 3 additions & 1 deletion react/features/app/actions.any.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export function maybeRedirectToTokenAuthUrl(

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

// if there is jwt and its expiration time is less than 3 minutes away
Expand All @@ -137,7 +138,8 @@ export function maybeRedirectToTokenAuthUrl(
videoMuted
},
room,
tenant
tenant,
refreshToken
)
.then((tokenAuthServiceUrl: string | undefined) => {
if (!tokenAuthServiceUrl) {
Expand Down
3 changes: 2 additions & 1 deletion react/features/app/getRouteToRender.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ const route = {
* store.
*
* @param {any} _stateful - Used on web.
* @param {any} _dispatch - Used on web.
* @returns {Promise<Object>}
*/
export function _getRouteToRender(_stateful?: any) {
export function _getRouteToRender(_stateful?: any): Promise<object> {
return Promise.resolve(route);
}
39 changes: 4 additions & 35 deletions react/features/app/getRouteToRender.web.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
// @ts-expect-error
import { generateRoomWithoutSeparator } from '@jitsi/js-utils/random';

import { getTokenAuthUrl } from '../authentication/functions.web';
import { IStateful } from '../base/app/types';
import { isRoomValid } from '../base/conference/functions';
import { isSupportedBrowser } from '../base/environment/environment';
import { browser } from '../base/lib-jitsi-meet';
import { toState } from '../base/redux/functions';
import { parseURIString } from '../base/util/uri';
import Conference from '../conference/components/web/Conference';
import { getDeepLinkingPage } from '../deep-linking/functions';
import UnsupportedDesktopBrowser from '../unsupported-browser/components/UnsupportedDesktopBrowser';
Expand All @@ -23,9 +20,10 @@ import { IReduxState } from './types';
*
* @param {(Function|Object)} stateful - THe redux store, state, or
* {@code getState} function.
* @param {Dispatch} dispatch - The Redux dispatch function.
* @returns {Promise<Object>}
*/
export function _getRouteToRender(stateful: IStateful) {
export function _getRouteToRender(stateful: IStateful): Promise<object> {
const state = toState(stateful);

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

if (!isRoomValid(room)) {
return;
}

const route = _getEmptyRoute();
const config = state['features/base/config'];

// if we have auto redirect enabled, and we have previously logged in successfully
// let's redirect to the auth url to get the token and login again
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need this anymore? Why?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well this was setting the jwt on prejoin without clicking join. But this means it is invoked without user interaction and that does not work with popups, the browser blocks them. So I moved this to happen before we join, after clicking the join button.

if (!browser.isElectron() && config.tokenAuthUrl && config.tokenAuthUrlAutoRedirect
&& state['features/authentication'].tokenAuthUrlSuccessful
&& !state['features/base/jwt'].jwt && room) {
const { locationURL = { href: '' } as URL } = state['features/base/connection'];
const { tenant } = parseURIString(locationURL.href) || {};
const { startAudioOnly } = config;

return getTokenAuthUrl(
config,
locationURL,
{
audioMuted: false,
audioOnlyEnabled: startAudioOnly,
skipPrejoin: false,
videoMuted: false
},
room,
tenant
)
.then((url: string | undefined) => {
route.href = url;

return route;
})
.catch(() => Promise.resolve(route));
}

// Update the location if it doesn't match. This happens when a room is
// joined from the welcome page. The reason for doing this instead of using
Expand Down
10 changes: 10 additions & 0 deletions react/features/authentication/actions.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,13 @@ export function openTokenAuthUrl(tokenAuthServiceUrl: string) {
Linking.openURL(tokenAuthServiceUrl);
};
}

/**
* Not used.
*
* @param {string} tokenAuthServiceUrl - Authentication service URL.
* @returns {Promise<any>} Resolves.
*/
export function loginWithPopup(tokenAuthServiceUrl: string): Promise<any> {
return Promise.resolve(tokenAuthServiceUrl);
}
149 changes: 149 additions & 0 deletions react/features/authentication/actions.web.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { maybeRedirectToWelcomePage } from '../app/actions.web';
import { IStore } from '../app/types';
import { openDialog } from '../base/dialog/actions';
import { setJWT } from '../base/jwt/actions';
import { browser } from '../base/lib-jitsi-meet';
import { showErrorNotification } from '../notifications/actions';

import { CANCEL_LOGIN } from './actionTypes';
import LoginQuestionDialog from './components/web/LoginQuestionDialog';
import { isTokenAuthInline } from './functions.any';
import logger from './logger';

export * from './actions.any';

Expand Down Expand Up @@ -46,6 +50,115 @@ 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.
* @returns {Promise<any>} A promise that resolves with the authentication
* result or rejects with an error.
*/
export function loginWithPopup(tokenAuthServiceUrl: string): Promise<any> {
return new Promise<any>((resolve, reject) => {
// Open popup
const width = 500;
const height = 600;
const left = window.screen.width / 2 - width / 2;
const top = window.screen.height / 2 - height / 2;

const nonce = generateNonce();

sessionStorage.setItem('oauth_nonce', nonce);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this? where do we use it? I see that we remove it but I don't see where we get it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is sessionStorage.setItem( so we set it here. We generate it.
Hum, I think it was used for verifying the response in sso.html. Probably we do not do it at the moment, but we can add it. I will double check when I do the changes for the other PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is now implemented.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should check if the session storage is enabled. If it is disabled trough the browser settings this will throw an exception.


const popup = window.open(
`${tokenAuthServiceUrl}&nonce=${nonce}`,
`Auth-${Date.now()}`,
`width=${width},height=${height},left=${left},top=${top}`
);

if (!popup) {
reject(new Error('Popup blocked'));

return;
}

const cleanup = (handler: any) => {
window.removeEventListener('message', handler);
popup.close();

sessionStorage.removeItem('oauth_nonce');
};

// @ts-ignore
const handler = event => {
// 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should add the listener to popup. this way if the user closes the popup the listener will be auto removed! otherwise in this use case we will be leaking listeners.

});
}

/**
* 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);
});
}

/**
* Opens token auth URL page.
*
Expand All @@ -63,6 +176,42 @@ export function openTokenAuthUrl(tokenAuthServiceUrl: string): any {
}
};

if (!browser.isElectron() && isTokenAuthInline(getState()['features/base/config'])) {
loginWithPopup(tokenAuthServiceUrl)
.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'];

connection?.refreshToken(token).then(
() => {
const { membersOnly } = getState()['features/base/conference'];

membersOnly?.join();
})
.catch((err: any) => {
dispatch(setJWT());
logger.error(err);
});
})
.catch(err => {
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, {
Expand Down
19 changes: 17 additions & 2 deletions react/features/authentication/functions.any.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ import { getBackendSafeRoomName } from '../base/util/uri';
export const isTokenAuthEnabled = (config: IConfig): boolean =>
typeof config.tokenAuthUrl === 'string' && config.tokenAuthUrl.length > 0;

/**
* Checks if the token authentication should be done inline.
*
* @param {Object} config - Configuration state object from store.
* @returns {boolean}
*/
export const isTokenAuthInline = (config: IConfig): boolean =>
config.tokenAuthInline === true;

/**
* Returns the state that we can add as a parameter to the tokenAuthUrl.
*
Expand All @@ -23,6 +32,7 @@ export const isTokenAuthEnabled = (config: IConfig): boolean =>
* }.
* @param {string?} roomName - The room name.
* @param {string?} tenant - The tenant name if any.
* @param {string?} refreshToken - The refresh token if available.
*
* @returns {Object} The state object.
*/
Expand All @@ -35,13 +45,18 @@ export const _getTokenAuthState = (
videoMuted: boolean | undefined;
},
roomName: string | undefined,
tenant: string | undefined): object => {
const state = {
tenant: string | undefined,
refreshToken?: string | undefined): object => {
const state: any = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the any? If we need to specify a type lets define a real one and use it. It might be useful to have a type for this state anyway...

room: roomName,
roomSafe: getBackendSafeRoomName(roomName),
tenant
};

if (refreshToken) {
state.refreshToken = refreshToken;
}

const {
audioMuted = false,
audioOnlyEnabled = false,
Expand Down
8 changes: 6 additions & 2 deletions react/features/authentication/functions.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export * from './functions.any';
* }.
* @param {string?} roomName - The room name.
* @param {string?} tenant - The tenant name if any.
* @param {string?} refreshToken - The refreshToken if any.
*
* @returns {Promise<string|undefined>} - The URL pointing to JWT login service or
* <tt>undefined</tt> if the pattern stored in config is not a string and the URL can not be
Expand All @@ -39,7 +40,9 @@ export const getTokenAuthUrl = (
},
roomName: string | undefined,
// eslint-disable-next-line max-params
tenant: string | undefined): Promise<string | undefined> => {
tenant: string | undefined,
// eslint-disable-next-line max-params
refreshToken?: string | undefined): Promise<string | undefined> => {

const {
audioMuted = false,
Expand All @@ -64,7 +67,8 @@ export const getTokenAuthUrl = (
videoMuted
},
roomName,
tenant
tenant,
refreshToken
);

// Append ios=true or android=true to the token URL.
Expand Down
Loading
Loading