Skip to content

Commit

Permalink
feat: Introduces passing state to the token authUrl.
Browse files Browse the repository at this point in the history
  • Loading branch information
damencho committed Sep 21, 2023
1 parent cd1e70b commit f90ef61
Show file tree
Hide file tree
Showing 9 changed files with 240 additions and 70 deletions.
28 changes: 25 additions & 3 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -1441,6 +1441,31 @@ var config = {
// dialInConfCodeUrl is the conference mapper converting a meeting id to a PIN used for dial-in
// or the other way around (more info in resources/cloud-api.swagger)

// You can use external service for authentication that will redirect back passing a jwt token
// You can use tokenAuthUrl config to point to a URL of such service.
// The URL for the service supports few params which will be filled in by the code.
// tokenAuthUrl:
// 'https://myservice.com/auth/{room}?code_challenge_method=S256&code_challenge={code_challenge}&state={state}'
// Supported parameters in tokenAuthUrl:
// {room} - will be replaced with the room name
// {code_challenge} - (A web only). A oauth 2.0 code challenge that will be sent to the service. See:
// https://datatracker.ietf.org/doc/html/rfc7636. The code verifier will be saved in the sessionStorage
// under key: 'code_verifier'.
// {state} - A json with the current state before redirecting. Keys that are included in the state:
// - room (The current room name as shown in the address bar)
// - roomSafe (the backend safe room name to use (lowercase), that is passed to the backend)
// - tenant (The tenant if any)
// - config.xxx (all config overrides)
// - interfaceConfig.xxx (all interfaceConfig overrides)
// - ios=true (in case ios mobile app is used)
// - android=true (in case android mobile app is used)
// - electron=true (when web is loaded in electron app)
// If there is a logout service you can specify its URL with:
// tokenLogoutUrl: 'https://myservice.com/logout'
// You can enable tokenAuthUrlAutoRedirect which will detect that you have logged in successfully before
// and will automatically redirect to the token service to get the token for the meeting.
// tokenAuthUrlAutoRedirect: false

// List of undocumented settings used in jitsi-meet
/**
_immediateReloadThreshold
Expand All @@ -1460,9 +1485,6 @@ var config = {
peopleSearchQueryTypes
peopleSearchUrl
requireDisplayName
tokenAuthUrl
tokenAuthUrlAutoRedirect
tokenLogoutUrl
*/

/**
Expand Down
14 changes: 11 additions & 3 deletions react/features/app/getRouteToRender.web.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
// @ts-expect-error
import { generateRoomWithoutSeparator } from '@jitsi/js-utils/random';

import { getTokenAuthUrl } from '../authentication/functions';
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 Down Expand Up @@ -52,9 +53,16 @@ function _getWebConferenceRoute(state: IReduxState) {
if (!browser.isElectron() && config.tokenAuthUrl && config.tokenAuthUrlAutoRedirect
&& state['features/authentication'].tokenAuthUrlSuccessful
&& !state['features/base/jwt'].jwt && room) {
route.href = getTokenAuthUrl(config, room);
const { locationURL = { href: '' } } = state['features/base/connection'];
const { tenant } = parseURIString(locationURL.href) || {};

return Promise.resolve(route);
return getTokenAuthUrl(config, 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
Expand Down
10 changes: 2 additions & 8 deletions react/features/authentication/actions.native.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { Linking, Platform } from 'react-native';
import { Linking } from 'react-native';

import { appNavigate } from '../app/actions.native';
import { IStore } from '../app/types';
import { conferenceLeft } from '../base/conference/actions';
import { connectionFailed } from '../base/connection/actions.native';
import { set } from '../base/redux/functions';
import { appendURLHashParam } from '../base/util/uri';

import { CANCEL_LOGIN } from './actionTypes';
import { stopWaitForOwner } from './actions.any';
Expand Down Expand Up @@ -85,12 +84,7 @@ export function redirectToDefaultLocation() {
* @returns {Function}
*/
export function openTokenAuthUrl(tokenAuthServiceUrl: string) {
let url = appendURLHashParam(tokenAuthServiceUrl, 'skipPrejoin', 'true');

// Append ios=true or android=true to the token URL.
url = appendURLHashParam(url, Platform.OS, 'true');

return () => {
Linking.openURL(url);
Linking.openURL(tokenAuthServiceUrl);
};
}
9 changes: 2 additions & 7 deletions react/features/authentication/actions.web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { maybeRedirectToWelcomePage } from '../app/actions.web';
import { IStore } from '../app/types';
import { openDialog } from '../base/dialog/actions';
import { browser } from '../base/lib-jitsi-meet';
import { appendURLHashParam } from '../base/util/uri';

import { CANCEL_LOGIN } from './actionTypes';
import LoginQuestionDialog from './components/web/LoginQuestionDialog';
Expand Down Expand Up @@ -57,14 +56,10 @@ export function redirectToDefaultLocation() {
export function openTokenAuthUrl(tokenAuthServiceUrl: string): any {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const redirect = () => {
// We have already shown the prejoin screen, no need to show it again after obtaining the token.
let url = appendURLHashParam(tokenAuthServiceUrl, 'skipPrejoin', 'true');

if (browser.isElectron()) {
url = appendURLHashParam(url, 'electron', 'true');
window.open(url, '_blank');
window.open(tokenAuthServiceUrl, '_blank');
} else {
window.location.href = url;
window.location.href = tokenAuthServiceUrl;
}
};

Expand Down
48 changes: 48 additions & 0 deletions react/features/authentication/functions.any.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { IConfig } from '../base/config/configType';
import { getBackendSafeRoomName } from '../base/util/uri';

/**
* Checks if the token for authentication is available.
*
* @param {Object} config - Configuration state object from store.
* @returns {boolean}
*/
export const isTokenAuthEnabled = (config: IConfig): boolean =>
typeof config.tokenAuthUrl === 'string' && config.tokenAuthUrl.length > 0;

/**
* Returns the state that we can add as a parameter to the tokenAuthUrl.
*
* @param {string?} roomName - The room name.
* @param {string?} tenant - The tenant name if any.
* @param {boolean} skipPrejoin - Whether to skip pre-join page.
* @returns {Object} The state object.
*/
export const _getTokenAuthState = (
roomName: string | undefined,
tenant: string | undefined,
skipPrejoin: boolean | undefined = false): object => {
const state = {
room: roomName,
roomSafe: getBackendSafeRoomName(roomName),
tenant
};

if (skipPrejoin) {
// We have already shown the prejoin screen, no need to show it again after obtaining the token.
// @ts-ignore
state['config.prejoinConfig.enabled'] = false;
}

const params = new URLSearchParams(window.location.search);

for (const [ key, value ] of params) {
// we allow only config and interfaceConfig overrides in the state
if (key.startsWith('config.') || key.startsWith('interfaceConfig.')) {
// @ts-ignore
state[key] = value;
}
}

return state;
};
48 changes: 48 additions & 0 deletions react/features/authentication/functions.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Platform } from 'react-native';

import { IConfig } from '../base/config/configType';

import { _getTokenAuthState } from './functions.any';

export * from './functions.any';

/**
* Creates the URL pointing to JWT token authentication service. It is
* formatted from the 'urlPattern' argument which can contain the following
* constants:
* '{room}' - name of the conference room passed as <tt>roomName</tt>
* argument to this method.
*
* @param {Object} config - Configuration state object from store. A URL pattern pointing to the login service.
* @param {string} roomName - The name of the conference room for which the user will be authenticated.
* @param {string} tenant - The name of the conference tenant.
* @param {string} skipPrejoin - The name of the conference room for which the user will be authenticated.
*
* @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
* constructed.
*/
export const getTokenAuthUrl = (
config: IConfig,
roomName: string | undefined,
tenant: string | undefined,
skipPrejoin: boolean | undefined = false): Promise<string | undefined> => {

let url = config.tokenAuthUrl;

if (!url || !roomName) {
return Promise.resolve(undefined);
}

if (url.indexOf('{state}')) {
const state = _getTokenAuthState(roomName, tenant, skipPrejoin);

// Append ios=true or android=true to the token URL.
// @ts-ignore
state[Platform.OS] = true;

url = url.replace('{state}', encodeURIComponent(JSON.stringify(state)));
}

return Promise.resolve(url.replace('{room}', roomName));
};
39 changes: 0 additions & 39 deletions react/features/authentication/functions.ts

This file was deleted.

92 changes: 92 additions & 0 deletions react/features/authentication/functions.web.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import base64js from 'base64-js';

import { IConfig } from '../base/config/configType';
import { browser } from '../base/lib-jitsi-meet';

import { _getTokenAuthState } from './functions.any';

export * from './functions.any';

/**
* Based on rfc7636 we need a random string for a code verifier.
*/
const POSSIBLE_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

/**
* Crypto random, alternative of Math.random.
*
* @returns {float} A random value.
*/
function _cryptoRandom() {
const typedArray = new Uint8Array(1);
const randomValue = crypto.getRandomValues(typedArray)[0];

return randomValue / Math.pow(2, 8);
}

/**
* Creates the URL pointing to JWT token authentication service. It is
* formatted from the 'urlPattern' argument which can contain the following
* constants:
* '{room}' - name of the conference room passed as <tt>roomName</tt>
* argument to this method.
*
* @param {Object} config - Configuration state object from store. A URL pattern pointing to the login service.
* @param {string} roomName - The name of the conference room for which the user will be authenticated.
* @param {string} tenant - The name of the conference tenant.
* @param {string} skipPrejoin - The name of the conference room for which the user will be authenticated.
*
* @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
* constructed.
*/
export const getTokenAuthUrl = (
config: IConfig,
roomName: string | undefined,
tenant: string | undefined,
skipPrejoin: boolean | undefined = false): Promise<string | undefined> => {

let url = config.tokenAuthUrl;

if (!url || !roomName) {
return Promise.resolve(undefined);
}

if (url.indexOf('{state}')) {
const state = _getTokenAuthState(roomName, tenant, skipPrejoin);

if (browser.isElectron()) {
// @ts-ignore
state.electron = true;
}

url = url.replace('{state}', encodeURIComponent(JSON.stringify(state)));
}

url = url.replace('{room}', roomName);

if (url.indexOf('{code_challenge}')) {
let codeVerifier = '';

// random string
for (let i = 0; i < 64; i++) {
codeVerifier += POSSIBLE_CHARS.charAt(Math.floor(_cryptoRandom() * POSSIBLE_CHARS.length));
}

window.sessionStorage.setItem('code_verifier', codeVerifier);

return window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier))
.then(digest => {
// prepare code challenge - base64 encoding without padding as described in:
// https://datatracker.ietf.org/doc/html/rfc7636#appendix-A
const codeChallenge = base64js.fromByteArray(new Uint8Array(digest))
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');

return url ? url.replace('{code_challenge}', codeChallenge) : undefined;
});
}

return Promise.resolve(url);
};
Loading

0 comments on commit f90ef61

Please sign in to comment.