diff --git a/config.js b/config.js index 42e009ffb583..b050cc23ec24 100644 --- a/config.js +++ b/config.js @@ -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" diff --git a/lang/main.json b/lang/main.json index 2b0d335b742e..db2daecb30a8 100644 --- a/lang/main.json +++ b/lang/main.json @@ -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", diff --git a/react/features/app/actions.any.ts b/react/features/app/actions.any.ts index 79be0088decc..99f01d9290e3 100644 --- a/react/features/app/actions.any.ts +++ b/react/features/app/actions.any.ts @@ -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 @@ -137,7 +138,8 @@ export function maybeRedirectToTokenAuthUrl( videoMuted }, room, - tenant + tenant, + refreshToken ) .then((tokenAuthServiceUrl: string | undefined) => { if (!tokenAuthServiceUrl) { diff --git a/react/features/app/getRouteToRender.native.ts b/react/features/app/getRouteToRender.native.ts index 1285940bf1e4..9090d76778da 100644 --- a/react/features/app/getRouteToRender.native.ts +++ b/react/features/app/getRouteToRender.native.ts @@ -11,8 +11,9 @@ const route = { * store. * * @param {any} _stateful - Used on web. + * @param {any} _dispatch - Used on web. * @returns {Promise} */ -export function _getRouteToRender(_stateful?: any) { +export function _getRouteToRender(_stateful?: any): Promise { return Promise.resolve(route); } diff --git a/react/features/app/getRouteToRender.web.ts b/react/features/app/getRouteToRender.web.ts index a757d2872b41..e55b0a4580bc 100644 --- a/react/features/app/getRouteToRender.web.ts +++ b/react/features/app/getRouteToRender.web.ts @@ -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'; @@ -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} */ -export function _getRouteToRender(stateful: IStateful) { +export function _getRouteToRender(stateful: IStateful): Promise { const state = toState(stateful); return _getWebConferenceRoute(state) || _getWebWelcomePageRoute(state); @@ -36,9 +34,10 @@ 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 | undefined { const room = state['features/base/conference'].room; if (!isRoomValid(room)) { @@ -46,36 +45,6 @@ function _getWebConferenceRoute(state: IReduxState) { } 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 - 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 diff --git a/react/features/authentication/actions.native.ts b/react/features/authentication/actions.native.ts index 226098591a79..2fded3235944 100644 --- a/react/features/authentication/actions.native.ts +++ b/react/features/authentication/actions.native.ts @@ -88,3 +88,13 @@ export function openTokenAuthUrl(tokenAuthServiceUrl: string) { Linking.openURL(tokenAuthServiceUrl); }; } + +/** + * Not used. + * + * @param {string} tokenAuthServiceUrl - Authentication service URL. + * @returns {Promise} Resolves. + */ +export function loginWithPopup(tokenAuthServiceUrl: string): Promise { + return Promise.resolve(tokenAuthServiceUrl); +} diff --git a/react/features/authentication/actions.web.ts b/react/features/authentication/actions.web.ts index 6f026f6ec9d2..3d4778dd5a80 100644 --- a/react/features/authentication/actions.web.ts +++ b/react/features/authentication/actions.web.ts @@ -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'; @@ -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} A promise that resolves with the authentication + * result or rejects with an error. + */ +export function loginWithPopup(tokenAuthServiceUrl: string): Promise { + return new Promise((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); + + 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); + }); +} + +/** + * Performs silent logout by loading the token authentication logout service URL in an + * invisible iframe. + * + * @param {string} tokenAuthLogoutServiceUrl - Logout service URL. + * @returns {Promise} A promise that resolves when logout is complete. + */ +export function silentLogout(tokenAuthLogoutServiceUrl: string): any { + return new Promise(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. * @@ -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, { diff --git a/react/features/authentication/functions.any.ts b/react/features/authentication/functions.any.ts index cd13c20b70f5..6e1b1e839135 100644 --- a/react/features/authentication/functions.any.ts +++ b/react/features/authentication/functions.any.ts @@ -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. * @@ -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. */ @@ -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 = { room: roomName, roomSafe: getBackendSafeRoomName(roomName), tenant }; + if (refreshToken) { + state.refreshToken = refreshToken; + } + const { audioMuted = false, audioOnlyEnabled = false, diff --git a/react/features/authentication/functions.native.ts b/react/features/authentication/functions.native.ts index 64e17d4bcd26..2018404c426d 100644 --- a/react/features/authentication/functions.native.ts +++ b/react/features/authentication/functions.native.ts @@ -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} - The URL pointing to JWT login service or * undefined if the pattern stored in config is not a string and the URL can not be @@ -39,7 +40,9 @@ export const getTokenAuthUrl = ( }, roomName: string | undefined, // eslint-disable-next-line max-params - tenant: string | undefined): Promise => { + tenant: string | undefined, + // eslint-disable-next-line max-params + refreshToken?: string | undefined): Promise => { const { audioMuted = false, @@ -64,7 +67,8 @@ export const getTokenAuthUrl = ( videoMuted }, roomName, - tenant + tenant, + refreshToken ); // Append ios=true or android=true to the token URL. diff --git a/react/features/authentication/functions.web.ts b/react/features/authentication/functions.web.ts index d94b154bde74..30900a15ffed 100644 --- a/react/features/authentication/functions.web.ts +++ b/react/features/authentication/functions.web.ts @@ -41,6 +41,7 @@ function _cryptoRandom() { * }. * @param {string?} roomName - The room name. * @param {string?} tenant - The tenant name if any. + * @param {string?} refreshToken - The refresh token if available. * * @returns {Promise} - The URL pointing to JWT login service or * undefined if the pattern stored in config is not a string and the URL can not be @@ -57,7 +58,9 @@ export const getTokenAuthUrl = ( }, roomName: string | undefined, // eslint-disable-next-line max-params - tenant: string | undefined): Promise => { + tenant: string | undefined, + // eslint-disable-next-line max-params + refreshToken?: string | undefined): Promise => { const { audioMuted = false, @@ -82,7 +85,8 @@ export const getTokenAuthUrl = ( videoMuted }, roomName, - tenant + tenant, + refreshToken ); if (browser.isElectron()) { diff --git a/react/features/authentication/middleware.ts b/react/features/authentication/middleware.ts index b4ca7f310609..660d1a99690d 100644 --- a/react/features/authentication/middleware.ts +++ b/react/features/authentication/middleware.ts @@ -1,5 +1,4 @@ import { IStore } from '../app/types'; -import { APP_WILL_NAVIGATE } from '../base/app/actionTypes'; import { CONFERENCE_FAILED, CONFERENCE_JOINED, @@ -17,6 +16,7 @@ import { MEDIA_TYPE } from '../base/media/constants'; import MiddlewareRegistry from '../base/redux/MiddlewareRegistry'; import { isLocalTrackMuted } from '../base/tracks/functions.any'; import { parseURIString } from '../base/util/uri'; +import { PREJOIN_JOINING_IN_PROGRESS } from '../prejoin/actionTypes'; import { openLogoutDialog } from '../settings/actions'; import { @@ -187,7 +187,11 @@ MiddlewareRegistry.register(store => next => action => { break; } - case APP_WILL_NAVIGATE: { + case PREJOIN_JOINING_IN_PROGRESS: { + if (!action.value) { + break; + } + const { dispatch, getState } = store; const state = getState(); const config = state['features/base/config']; @@ -288,6 +292,7 @@ function _handleLogin({ dispatch, getState }: IStore) { const { enabled: audioOnlyEnabled } = state['features/base/audio-only']; const audioMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO); const videoMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO); + const refreshToken = state['features/base/jwt'].refreshToken; if (!room) { logger.warn('Cannot handle login, room is undefined!'); @@ -311,7 +316,8 @@ function _handleLogin({ dispatch, getState }: IStore) { videoMuted }, room, - tenant + tenant, + refreshToken ) .then((tokenAuthServiceUrl: string | undefined) => { if (!tokenAuthServiceUrl) { diff --git a/react/features/base/conference/actions.any.ts b/react/features/base/conference/actions.any.ts index 077febb8f3ee..1aef889bb1a5 100644 --- a/react/features/base/conference/actions.any.ts +++ b/react/features/base/conference/actions.any.ts @@ -354,7 +354,7 @@ export function e2eRttChanged(participant: Object, rtt: number) { * authLogin: string * }} */ -export function authStatusChanged(authEnabled: boolean, authLogin: string) { +export function authStatusChanged(authEnabled: boolean, authLogin?: string) { return { type: AUTH_STATUS_CHANGED, authEnabled, diff --git a/react/features/base/conference/middleware.any.ts b/react/features/base/conference/middleware.any.ts index b9df6a6a16ce..a97ce76ae863 100644 --- a/react/features/base/conference/middleware.any.ts +++ b/react/features/base/conference/middleware.any.ts @@ -402,7 +402,8 @@ async function _connectionEstablished({ dispatch, getState }: IStore, next: Func email = getLocalParticipant(getState())?.email; } - dispatch(authStatusChanged(true, email || '')); + // it may happen to be already set (silent login) + dispatch(authStatusChanged(true, email || getState()['features/base/conference'].authLogin || '')); } // FIXME: Workaround for the web version. Currently, the creation of the diff --git a/react/features/base/config/configType.ts b/react/features/base/config/configType.ts index cb4757a4cbe0..e13197f847a8 100644 --- a/react/features/base/config/configType.ts +++ b/react/features/base/config/configType.ts @@ -616,6 +616,7 @@ export interface IConfig { disabled?: boolean; numberOfVisibleTiles?: number; }; + tokenAuthInline?: boolean; tokenAuthUrl?: string; tokenAuthUrlAutoRedirect?: string; tokenGetUserInfoOutOfContext?: boolean; diff --git a/react/features/base/connection/actionTypes.ts b/react/features/base/connection/actionTypes.ts index f11ac2d3e8ff..a0279cbcc3a4 100644 --- a/react/features/base/connection/actionTypes.ts +++ b/react/features/base/connection/actionTypes.ts @@ -51,6 +51,16 @@ export const CONNECTION_PROPERTIES_UPDATED = 'CONNECTION_PROPERTIES_UPDATED'; */ export const CONNECTION_WILL_CONNECT = 'CONNECTION_WILL_CONNECT'; +/** + * The type of (redux) action which signals that the token for a connection is expired. + * + * { + * type: CONNECTION_TOKEN_EXPIRED, + * connection: JitsiConnection + * } + */ +export const CONNECTION_TOKEN_EXPIRED = 'CONNECTION_TOKEN_EXPIRED'; + /** * The type of (redux) action which sets the location URL of the application, * connection, conference, etc. diff --git a/react/features/base/connection/actions.any.ts b/react/features/base/connection/actions.any.ts index e431be068476..669e5173e762 100644 --- a/react/features/base/connection/actions.any.ts +++ b/react/features/base/connection/actions.any.ts @@ -17,6 +17,7 @@ import { CONNECTION_ESTABLISHED, CONNECTION_FAILED, CONNECTION_PROPERTIES_UPDATED, + CONNECTION_TOKEN_EXPIRED, CONNECTION_WILL_CONNECT, SET_LOCATION_URL, SET_PREFER_VISITOR @@ -239,6 +240,9 @@ export function _connectInternal(id?: string, password?: string) { connection.addEventListener( JitsiConnectionEvents.PROPERTIES_UPDATED, _onPropertiesUpdate); + connection.addEventListener( + JitsiConnectionEvents.CONNECTION_TOKEN_EXPIRED, + _onTokenExpired); /** * Unsubscribe the connection instance from @@ -323,6 +327,16 @@ export function _connectInternal(id?: string, password?: string) { dispatch(redirect(vnode, focusJid, username)); } + /** + * Connection will resume. + * + * @private + * @returns {void} + */ + function _onTokenExpired(): void { + dispatch(_connectionTokenExpired(connection)); + } + /** * Connection properties were updated. * @@ -364,6 +378,23 @@ function _connectionWillConnect(connection: Object) { }; } +/** + * Create an action for when a connection token is expired. + * + * @param {JitsiConnection} connection - The {@code JitsiConnection} token is expired. + * @private + * @returns {{ + * type: CONNECTION_TOKEN_EXPIRED, + * connection: JitsiConnection + * }} + */ +function _connectionTokenExpired(connection: Object) { + return { + type: CONNECTION_TOKEN_EXPIRED, + connection + }; +} + /** * Create an action for when connection properties are updated. * diff --git a/react/features/base/connection/reducer.ts b/react/features/base/connection/reducer.ts index bae27ce9e16e..9d92aac87e4d 100644 --- a/react/features/base/connection/reducer.ts +++ b/react/features/base/connection/reducer.ts @@ -23,6 +23,7 @@ export interface IConnectionState { getJid: () => string; getLogs: () => Object; initJitsiConference: Function; + refreshToken: Function; removeFeature: Function; }; error?: ConnectionFailedError; diff --git a/react/features/base/jwt/actions.ts b/react/features/base/jwt/actions.ts index b38875baf914..5f0b19cd286b 100644 --- a/react/features/base/jwt/actions.ts +++ b/react/features/base/jwt/actions.ts @@ -20,15 +20,21 @@ export function setDelayedLoadOfAvatarUrl(avatarUrl?: string) { * Stores a specific JSON Web Token (JWT) into the redux store. * * @param {string} [jwt] - The JSON Web Token (JWT) to store. + * @param {string} idToken - The ID Token to store. + * @param {string} refreshToken - The Refresh Token to store. * @returns {{ * type: SET_JWT, - * jwt: (string|undefined) + * jwt: (string|undefined), + * idToken: (string|undefined), + * refreshToken: (string|undefined) * }} */ -export function setJWT(jwt?: string) { +export function setJWT(jwt?: string, idToken?: string, refreshToken?: string) { return { type: SET_JWT, - jwt + jwt, + idToken, + refreshToken }; } diff --git a/react/features/base/jwt/middleware.ts b/react/features/base/jwt/middleware.ts index 4c0c7a76d868..aed645ee5f80 100644 --- a/react/features/base/jwt/middleware.ts +++ b/react/features/base/jwt/middleware.ts @@ -3,10 +3,18 @@ import jwtDecode from 'jwt-decode'; import { AnyAction } from 'redux'; import { IStore } from '../../app/types'; +import { loginWithPopup } from '../../authentication/actions'; +import LoginQuestionDialog from '../../authentication/components/web/LoginQuestionDialog'; +import { getTokenAuthUrl, isTokenAuthInline } from '../../authentication/functions'; import { isVpaasMeeting } from '../../jaas/functions'; +import { hideNotification, showNotification } from '../../notifications/actions'; +import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../../notifications/constants'; +import { authStatusChanged } from '../conference/actions.any'; import { getCurrentConference } from '../conference/functions'; import { SET_CONFIG } from '../config/actionTypes'; -import { CONNECTION_ESTABLISHED, SET_LOCATION_URL } from '../connection/actionTypes'; +import { CONNECTION_ESTABLISHED, CONNECTION_TOKEN_EXPIRED, SET_LOCATION_URL } from '../connection/actionTypes'; +import { openDialog } from '../dialog/actions'; +import { browser } from '../lib-jitsi-meet'; import { participantUpdated } from '../participants/actions'; import { getLocalParticipant } from '../participants/functions'; import { IParticipant } from '../participants/types'; @@ -16,9 +24,12 @@ import { parseURIString } from '../util/uri'; import { SET_JWT } from './actionTypes'; import { setDelayedLoadOfAvatarUrl, setJWT, setKnownAvatarUrl } from './actions'; -import { parseJWTFromURLParams } from './functions'; +import { JWT_VALIDATION_ERRORS } from './constants'; +import { parseJWTFromURLParams, validateJwt } from './functions'; import logger from './logger'; +const PROMPT_LOGIN_NOTIFICATION_ID = 'PROMPT_LOGIN_NOTIFICATION_ID'; + /** * Set up a state change listener to perform maintenance tasks when the conference * is left or failed, e.g. Clear any delayed load avatar url. @@ -39,14 +50,93 @@ StateListenerRegistry.register( * @returns {Function} */ MiddlewareRegistry.register(store => next => action => { + const state = store.getState(); + switch (action.type) { case SET_CONFIG: case SET_LOCATION_URL: // XXX The JSON Web Token (JWT) is not the only piece of state that we // have decided to store in the feature jwt return _setConfigOrLocationURL(store, next, action); + case CONNECTION_TOKEN_EXPIRED: { + const jwt = state['features/base/jwt'].jwt; + const refreshToken = state['features/base/jwt'].refreshToken; + + if (typeof APP !== 'undefined' && jwt + && validateJwt(jwt).find((e: any) => e.key === JWT_VALIDATION_ERRORS.TOKEN_EXPIRED)) { + const { connection, locationURL = { href: '' } as URL } = state['features/base/connection']; + const { tenant } = parseURIString(locationURL.href) || {}; + const room = state['features/base/conference'].room; + const dispatch = store.dispatch; + + getTokenAuthUrl( + state['features/base/config'], + locationURL, + { + audioMuted: false, + audioOnlyEnabled: false, + skipPrejoin: false, + videoMuted: false + }, + room, + tenant, + refreshToken + ) + .then((url: string | undefined) => { + if (url) { + // only if it is inline token auth and token is about to expire + // if not expired yet use it to refresh the token + dispatch(showNotification({ + descriptionKey: 'dialog.loginOnResume', + titleKey: 'dialog.login', + uid: PROMPT_LOGIN_NOTIFICATION_ID, + customActionNameKey: [ 'dialog.login' ], + customActionHandler: [ () => { + store.dispatch(hideNotification(PROMPT_LOGIN_NOTIFICATION_ID)); + + if (isTokenAuthInline(state['features/base/config'])) { + // Use refresh token if available, otherwise fall back to silent login + loginWithPopup(url) + .then((result: { accessToken: string; idToken: string; refreshToken?: string; }) => { + // @ts-ignore + const token: string = result.accessToken; + const idToken: string = result.idToken; + const newRefreshToken: string | undefined = result.refreshToken; + + // @ts-ignore + dispatch(setJWT(token, idToken, newRefreshToken || refreshToken)); + + connection?.refreshToken(token) + .catch((err: any) => { + dispatch(setJWT()); + logger.error(err); + }); + }).catch(logger.error); + } else { + dispatch(openDialog('LoginQuestionDialog', LoginQuestionDialog, { + handler: () => { + // Give time for the dialog to close. + setTimeout(() => { + if (browser.isElectron()) { + window.open(url, '_blank'); + } else { + window.location.href = url; + } + }, 500); + } + })); + } + + } ], + appearance: NOTIFICATION_TYPE.ERROR + }, NOTIFICATION_TIMEOUT_TYPE.STICKY)); + } + }) + .catch(logger.error); + } + break; + } case CONNECTION_ESTABLISHED: { - const state = store.getState(); const delayedLoadOfAvatarUrl = state['features/base/jwt'].delayedLoadOfAvatarUrl; if (delayedLoadOfAvatarUrl) { @@ -56,6 +146,7 @@ MiddlewareRegistry.register(store => next => action => { store.dispatch(setDelayedLoadOfAvatarUrl()); store.dispatch(setKnownAvatarUrl(delayedLoadOfAvatarUrl)); } + break; } case SET_JWT: return _setJWT(store, next, action); @@ -149,7 +240,7 @@ function _setConfigOrLocationURL({ dispatch, getState }: IStore, next: Function, */ function _setJWT(store: IStore, next: Function, action: AnyAction) { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { jwt, type, ...actionPayload } = action; + const { idToken, jwt, refreshToken, type, ...actionPayload } = action; if (!Object.keys(actionPayload).length) { const state = store.getState(); @@ -210,24 +301,32 @@ function _setJWT(store: IStore, next: Function, action: AnyAction) { if (context.user && context.user.role === 'visitor') { action.preferVisitor = true; } - } else if (tokenGetUserInfoOutOfContext - && (jwtPayload.name || jwtPayload.picture || jwtPayload.email)) { - // there are some tokens (firebase) having picture and name on the main level. - _overwriteLocalParticipant(store, { - avatarURL: jwtPayload.picture, - name: jwtPayload.name, - email: jwtPayload.email - }); + } else if (jwtPayload.name || jwtPayload.picture || jwtPayload.email) { + if (tokenGetUserInfoOutOfContext) { + // there are some tokens (firebase) having picture and name on the main level. + _overwriteLocalParticipant(store, { + avatarURL: jwtPayload.picture, + name: jwtPayload.name, + email: jwtPayload.email + }); + } + + store.dispatch(authStatusChanged(true, jwtPayload.email)); } } - } else if (typeof APP === 'undefined') { - // The logic of restoring JWT overrides make sense only on mobile. - // On Web it should eventually be restored from storage, but there's - // no such use case yet. + } else { + if (typeof APP === 'undefined') { + // The logic of restoring JWT overrides make sense only on mobile. + // On Web it should eventually be restored from storage, but there's + // no such use case yet. - const { user } = state['features/base/jwt']; + const { user } = state['features/base/jwt']; + + user && _undoOverwriteLocalParticipant(store, user); + } - user && _undoOverwriteLocalParticipant(store, user); + // clears authLogin + store.dispatch(authStatusChanged(true)); } } diff --git a/react/features/base/jwt/reducer.ts b/react/features/base/jwt/reducer.ts index c27282900bc6..7b53ba072a9e 100644 --- a/react/features/base/jwt/reducer.ts +++ b/react/features/base/jwt/reducer.ts @@ -11,8 +11,10 @@ export interface IJwtState { }; delayedLoadOfAvatarUrl?: string; group?: string; + idToken?: string; jwt?: string; knownAvatarUrl?: string; + refreshToken?: string; server?: string; tenant?: string; user?: { diff --git a/react/features/prejoin/components/web/Prejoin.tsx b/react/features/prejoin/components/web/Prejoin.tsx index 544025803cc9..4099745c8933 100644 --- a/react/features/prejoin/components/web/Prejoin.tsx +++ b/react/features/prejoin/components/web/Prejoin.tsx @@ -5,9 +5,14 @@ import { connect, useDispatch } from 'react-redux'; import { makeStyles } from 'tss-react/mui'; import { IReduxState } from '../../../app/types'; +import { loginWithPopup } from '../../../authentication/actions.web'; +import { getTokenAuthUrl, isTokenAuthInline } from '../../../authentication/functions.web'; import Avatar from '../../../base/avatar/components/Avatar'; +import { IConfig } from '../../../base/config/configType'; import { isNameReadOnly } from '../../../base/config/functions.web'; import { IconArrowDown, IconArrowUp, IconPhoneRinging, IconVolumeOff } from '../../../base/icons/svg'; +import { setJWT } from '../../../base/jwt/actions'; +import { browser } from '../../../base/lib-jitsi-meet'; import { isVideoMutedByUser } from '../../../base/media/functions'; import { getLocalParticipant } from '../../../base/participants/functions'; import Popover from '../../../base/popover/components/Popover.web'; @@ -20,6 +25,7 @@ import Button from '../../../base/ui/components/web/Button'; import Input from '../../../base/ui/components/web/Input'; import { BUTTON_TYPES } from '../../../base/ui/constants.any'; import isInsecureRoomName from '../../../base/util/isInsecureRoomName'; +import { parseURIString } from '../../../base/util/uri'; import { openDisplayNamePrompt } from '../../../display-name/actions'; import { isUnsafeRoomWarningEnabled } from '../../../prejoin/functions'; import { @@ -121,6 +127,16 @@ interface IProps { */ showUnsafeRoomWarning: boolean; + /** + * The configuration for token pre-authentication, if applicable. + */ + tokenPreAuthConfig?: { + config: IConfig; + locationURL: URL; + refreshToken: string | undefined; + room: string; + }; + /** * Whether the user has approved to join a room with unsafe name. */ @@ -226,6 +242,7 @@ const Prejoin = ({ showErrorOnJoin, showRecordingWarning, showUnsafeRoomWarning, + tokenPreAuthConfig, unsafeRoomConsent, updateSettings: dispatchUpdateSettings, videoTrack @@ -259,7 +276,56 @@ const Prejoin = ({ logger.info('Prejoin join button clicked.'); - joinConference(); + // 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 + if (tokenPreAuthConfig) { + const { tenant } = parseURIString(tokenPreAuthConfig.locationURL.href) || {}; + const { startAudioOnly } = tokenPreAuthConfig.config; + const refreshToken = tokenPreAuthConfig.refreshToken; + + getTokenAuthUrl( + config, + tokenPreAuthConfig.locationURL, + { + audioMuted: false, + audioOnlyEnabled: startAudioOnly, + skipPrejoin: false, + videoMuted: false + }, + tokenPreAuthConfig.room, + tenant, + refreshToken + ) + .then((url: string | undefined) => { + if (isTokenAuthInline(config)) { + if (url) { + return loginWithPopup(url) + .then((result: { accessToken: string; idToken: string; refreshToken?: string; }) => { + // @ts-ignore + const token: string = result.accessToken; + const idToken: string = result.idToken; + const newRefreshToken: string | undefined = result.refreshToken; + + // @ts-ignore + dispatch(setJWT(token, idToken, newRefreshToken || refreshToken)); + }) + .then(() => joinConference()); + } + } else { + if (url) { + window.location.href = url; + } else { + joinConference(); + } + } + }) + .catch(err => { + logger.error('Error in silent login', err); + joinConference(); + }); + } else { + joinConference(); + } }; /** @@ -502,7 +568,12 @@ function mapStateToProps(state: IReduxState) { const { joiningInProgress } = state['features/prejoin']; const { room } = state['features/base/conference']; const { unsafeRoomConsent } = state['features/base/premeeting']; - const { showPrejoinWarning: showRecordingWarning } = state['features/base/config'].recordings ?? {}; + const config = state['features/base/config']; + const { showPrejoinWarning: showRecordingWarning } = config.recordings ?? {}; + const preTokenAuthenticate = !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']; return { deviceStatusVisible: isDeviceStatusVisible(state), @@ -518,6 +589,12 @@ function mapStateToProps(state: IReduxState) { showErrorOnJoin, showRecordingWarning: Boolean(showRecordingWarning), showUnsafeRoomWarning: isInsecureRoomName(room) && isUnsafeRoomWarningEnabled(state), + tokenPreAuthConfig: preTokenAuthenticate ? { + config, + locationURL, + refreshToken: state['features/base/jwt'].refreshToken, + room + } : undefined, unsafeRoomConsent, videoTrack: getLocalJitsiVideoTrack(state) }; diff --git a/react/features/settings/actions.web.ts b/react/features/settings/actions.web.ts index fa43fdd3ae39..c59d62dcff30 100644 --- a/react/features/settings/actions.web.ts +++ b/react/features/settings/actions.web.ts @@ -1,8 +1,8 @@ import { batch } from 'react-redux'; import { IStore } from '../app/types'; -import { setTokenAuthUrlSuccess } from '../authentication/actions.web'; -import { isTokenAuthEnabled } from '../authentication/functions'; +import { setTokenAuthUrlSuccess, silentLogout } from '../authentication/actions.web'; +import { isTokenAuthEnabled, isTokenAuthInline } from '../authentication/functions'; import { setStartMutedPolicy, setStartReactionsMuted @@ -11,6 +11,7 @@ import { getConferenceState } from '../base/conference/functions'; import { hangup } from '../base/connection/actions.web'; import { openDialog } from '../base/dialog/actions'; import i18next from '../base/i18n/i18next'; +import { setJWT } from '../base/jwt/actions'; import { browser } from '../base/lib-jitsi-meet'; import { getNormalizedDisplayName } from '../base/participants/functions'; import { updateSettings } from '../base/settings/actions'; @@ -37,6 +38,7 @@ import { getProfileTabProps, getShortcutsTabProps } from './functions.web'; +import logger from './logger'; /** * Opens {@code LogoutDialog}. @@ -51,7 +53,24 @@ export function openLogoutDialog() { const logoutUrl = config.tokenLogoutUrl; const { conference } = state['features/base/conference']; - const { jwt } = state['features/base/jwt']; + const { jwt, idToken } = state['features/base/jwt']; + + if (!browser.isElectron() && logoutUrl && isTokenAuthInline(config)) { + let url = logoutUrl; + + if (idToken) { + url += `${logoutUrl.indexOf('?') === -1 ? '?' : '&'}id_token_hint=${idToken}`; + } + + silentLogout(url) + .then(() => { + dispatch(setJWT()); + dispatch(setTokenAuthUrlSuccess(false)); + }) + .catch(() => logger.error('logout failed')); + + return; + } dispatch(openDialog('LogoutDialog', LogoutDialog, { onLogout() { diff --git a/react/features/settings/components/web/SettingsDialog.tsx b/react/features/settings/components/web/SettingsDialog.tsx index 30334fb85ccd..24f6b84b22c1 100644 --- a/react/features/settings/components/web/SettingsDialog.tsx +++ b/react/features/settings/components/web/SettingsDialog.tsx @@ -272,6 +272,13 @@ function _mapStateToProps(state: IReduxState, ownProps: any) { component: ProfileTab, labelKey: 'profile.title', props: getProfileTabProps(state), + propsUpdateFunction: (tabState: any, newProps: ReturnType) => { + return { + ...newProps, + displayName: tabState?.displayName, + email: tabState?.email + }; + }, submit: submitProfileTab, icon: IconUser }); diff --git a/resources/prosody-plugins/README.md b/resources/prosody-plugins/README.md index 5a38d07c199c..76e61cc4a3e1 100644 --- a/resources/prosody-plugins/README.md +++ b/resources/prosody-plugins/README.md @@ -89,5 +89,27 @@ - speakerStats - A table containing speaker statistics for occupants in the room. The keys are occupant JIDs and the values are objects with properties like dominantSpeakerId, faceLandmarks, and sessionId. Used by mod_speakerstats_component.lua to manage speaker statistics in the room. - visitors_destroy_timer - A timer used to destroy the room when there are no main occupants or visitors left. It is set by mod_fmuc.lua to clean up the room after a certain period of inactivity. +# session fields added by jitsi +- jitsi_meet_context_user - The context from the jwt token, added after token verify. +- jitsi_meet_context_group - The group from the jwt context, added after token verify. +- jitsi_meet_context_features - The features from the context, added after token verify. +- jitsi_meet_context_room - The room settings from the jwt context, added after token verify. +- jitsi_meet_room - The room name in jwt token, added after token verify. +- jitsi_meet_str_tenant - The tenant in the context. Added after token verify. +- jitsi_meet_domain - The domain in the jwt ('sub' claim). Added after token verify. Can be the domain if not tenant is used or the tenant itself in lowercase. +- customusername - from a query parameter to be used with combination with "pre-jitsi-authentication" event to pre-set a known jid to a session. +- jitsi_web_query_room - room name from the query. +- jitsi_web_query_prefix - the tenant from the query specified as a param named 'prefix'. +- auth_token - The token, set before verify and cleared if verification fails. +- jitsi_meet_tenant_mismatch - The tenant field from the token and the query param for tenant do not match. +- previd - Used for stream resumption. +- user_region - the region header from the http request received. +- user_agent_header - the user agent header from the http request received. +- jitsi_throttle - used by rate limit module. +- jitsi_throttle_counter - used by rate limit module. +- force_permissions_update - Indicate that on next self-presence update the permissions should be resent to the client. Used by mod_jitsi_permissions.lua to manage permissions updates for the session. +- granted_jitsi_meet_context_user_id - when affiliation was changed (grant moderation) this holds the id of the actor. +- granted_jitsi_meet_context_group_id - when affiliation was changed (grant moderation) this holds the group of the actor. + #### Notes: When modules need to store data they should do it in the room object in _data or directly. The data needs to be a simple as strings or table of strings, they should not add objects like room, sessions or occupants that cannot be serialized. Attaching data to the room object makes reloading modules safe and guarantees data will be wiped once the room is destroyed. diff --git a/resources/prosody-plugins/mod_auth_jitsi-anonymous.lua b/resources/prosody-plugins/mod_auth_jitsi-anonymous.lua index 528e3fdd027d..998bb721399b 100644 --- a/resources/prosody-plugins/mod_auth_jitsi-anonymous.lua +++ b/resources/prosody-plugins/mod_auth_jitsi-anonymous.lua @@ -7,6 +7,8 @@ local new_sasl = require "util.sasl".new; local sasl = require "util.sasl"; local sessions = prosody.full_sessions; +module:depends("jitsi_session"); + -- define auth provider local provider = {}; @@ -38,10 +40,13 @@ function provider.get_sasl_handler(session) -- Custom session matching so we can resume session even with randomly -- generated user IDs. local function get_username(self, message) + + local resuming = false; if (session.previd ~= nil) then for _, session1 in pairs(sessions) do if (session1.resumption_token == session.previd) then self.username = session1.username; + resuming = true; break; end end @@ -49,6 +54,10 @@ function provider.get_sasl_handler(session) self.username = message; end + if not resuming then + session.auth_token = nil; + end + return true; end diff --git a/resources/prosody-plugins/mod_auth_token.lua b/resources/prosody-plugins/mod_auth_token.lua index b5b09dc12bfd..54215097cbf8 100644 --- a/resources/prosody-plugins/mod_auth_token.lua +++ b/resources/prosody-plugins/mod_auth_token.lua @@ -26,41 +26,6 @@ local provider = {}; local host = module.host; --- Extract 'token' param from URL when session is created -function init_session(event) - local session, request = event.session, event.request; - local query = request.url.query; - - local token = nil; - - -- extract token from Authorization header - if request.headers["authorization"] then - -- assumes the header value starts with "Bearer " - token = request.headers["authorization"]:sub(8,#request.headers["authorization"]) - end - - -- allow override of token via query parameter - if query ~= nil then - local params = formdecode(query); - - -- The following fields are filled in the session, by extracting them - -- from the query and no validation is being done. - -- After validating auth_token will be cleaned in case of error and few - -- other fields will be extracted from the token and set in the session - - if params and params.token then - token = params.token; - end - end - - -- in either case set auth_token in the session - session.auth_token = token; - session.user_agent_header = request.headers['user_agent']; -end - -module:hook_global("bosh-session", init_session); -module:hook_global("websocket-session", init_session); - module:hook("pre-resource-unbind", function (e) local error, session = e.error, e.session; @@ -95,41 +60,60 @@ function provider.delete_user(username) return nil; end -function provider.get_sasl_handler(session) +function first_stage_auth(session) + -- retrieve custom public key from server and save it on the session + local pre_event_result = prosody.events.fire_event("pre-jitsi-authentication-fetch-key", session); + if pre_event_result ~= nil and pre_event_result.res == false then + module:log("warn", + "Error verifying token on pre authentication stage:%s, reason:%s", pre_event_result.error, pre_event_result.reason); + session.auth_token = nil; + measure_pre_fetch_fail(1); + return pre_event_result; + end - local function get_username_from_token(self, message) + local res, error, reason = token_util:process_and_verify_token(session); + if res == false then + module:log("warn", + "Error verifying token err:%s, reason:%s tenant:%s room:%s user_agent:%s", + error, reason, session.jitsi_web_query_prefix, session.jitsi_web_query_room, + session.user_agent_header); + session.auth_token = nil; + measure_verify_fail(1); + return { res = res, error = error, reason = reason }; + end - -- retrieve custom public key from server and save it on the session - local pre_event_result = prosody.events.fire_event("pre-jitsi-authentication-fetch-key", session); - if pre_event_result ~= nil and pre_event_result.res == false then - module:log("warn", - "Error verifying token on pre authentication stage:%s, reason:%s", pre_event_result.error, pre_event_result.reason); - session.auth_token = nil; - measure_pre_fetch_fail(1); - return pre_event_result.res, pre_event_result.error, pre_event_result.reason; - end + local shouldAllow = prosody.events.fire_event("jitsi-access-ban-check", session); + if shouldAllow == false then + module:log("warn", "user is banned") + measure_ban(1); + return { res = false, error = "not-allowed", reason = "user is banned" }; + end - local res, error, reason = token_util:process_and_verify_token(session); - if res == false then - module:log("warn", - "Error verifying token err:%s, reason:%s tenant:%s room:%s user_agent:%s", - error, reason, session.jitsi_web_query_prefix, session.jitsi_web_query_room, - session.user_agent_header); - session.auth_token = nil; - measure_verify_fail(1); - return res, error, reason; - end + return { verify_result = res, custom_username = prosody.events.fire_event("pre-jitsi-authentication", session) }; +end - local shouldAllow = prosody.events.fire_event("jitsi-access-ban-check", session); - if shouldAllow == false then - module:log("warn", "user is banned") - measure_ban(1); - return false, "not-allowed", "user is banned"; +function second_stage_auth(session) + local post_event_result = prosody.events.fire_event("post-jitsi-authentication", session); + if post_event_result ~= nil and post_event_result.res == false then + module:log("warn", + "Error verifying token on post authentication stage :%s, reason:%s", post_event_result.error, post_event_result.reason); + session.auth_token = nil; + measure_post_auth_fail(1); + return post_event_result; + end +end + +function provider.get_sasl_handler(session) + + local function get_username_from_token(self, message) + + local s1_result = first_stage_auth(session); + if s1_result.res == false then + return s1_result.res, s1_result.error, s1_result.reason; end - local customUsername = prosody.events.fire_event("pre-jitsi-authentication", session); - if customUsername then - self.username = customUsername; + if s1_result.custom_username then + self.username = s1_result.custom_username; elseif session.previd ~= nil then for _, session1 in pairs(sessions) do if (session1.resumption_token == session.previd) then @@ -141,17 +125,14 @@ function provider.get_sasl_handler(session) self.username = message; end - local post_event_result = prosody.events.fire_event("post-jitsi-authentication", session); - if post_event_result ~= nil and post_event_result.res == false then - module:log("warn", - "Error verifying token on post authentication stage :%s, reason:%s", post_event_result.error, post_event_result.reason); - session.auth_token = nil; - measure_post_auth_fail(1); - return post_event_result.res, post_event_result.error, post_event_result.reason; + local s2_result = second_stage_auth(session); + if s2_result and s2_result.res ~= nil then + return s2_result.res, s2_result.error, s2_result.reason; end measure_success(1); - return res; + session._jitsi_auth_done = true; + return s1_result.verify_result; end return new_sasl(host, { anonymous = get_username_from_token }); @@ -177,3 +158,47 @@ local function anonymous(self, message) end sasl.registerMechanism("ANONYMOUS", {"anonymous"}, anonymous); + +module:hook_global('c2s-session-updated', function (event) + local session, from_session = event.session, event.from_session; + + if not from_session.auth_token then + return; + end + + -- we care to handle sessions from other hosts (anonymous hosts) + if module.host ~= event.from_session.host then + -- Handle session updates (e.g., when a session is resumed on some anonymous host with a token we need to do all the checks here) + session.auth_token = event.from_session.auth_token; + + local s1_result = first_stage_auth(session); + if s1_result.res == false then + event.session:close(); + return; + end + + local s2_result = second_stage_auth(session); + if s2_result and s2_result.res == false then + event.session:close(); + return; + end + session._jitsi_auth_done = true; + end + + if not session._jitsi_auth_done then + module:log('warn', 'Impossible case hit where session did not pass auth flow'); + event.session:close(); + return; + end + + -- copy all the custom fields we set in the session + session.auth_token = from_session.auth_token; + session.jitsi_meet_context_user = from_session.jitsi_meet_context_user; + session.jitsi_meet_context_group = from_session.jitsi_meet_context_group; + session.jitsi_meet_context_features = from_session.jitsi_meet_context_features; + session.jitsi_meet_context_room = from_session.jitsi_meet_context_room; + session.jitsi_meet_room = from_session.jitsi_meet_room; + session.jitsi_meet_str_tenant = from_session.jitsi_meet_str_tenant; + session.jitsi_meet_domain = from_session.jitsi_meet_domain; + session.jitsi_meet_tenant_mismatch = from_session.jitsi_meet_tenant_mismatch; +end, 1); diff --git a/resources/prosody-plugins/mod_jitsi_session.lua b/resources/prosody-plugins/mod_jitsi_session.lua index 4ea2986ed6d5..d2537f2ba295 100644 --- a/resources/prosody-plugins/mod_jitsi_session.lua +++ b/resources/prosody-plugins/mod_jitsi_session.lua @@ -11,6 +11,14 @@ function init_session(event) local session, request = event.session, event.request; local query = request.url.query; + local token = nil; + + -- extract token from Authorization header + if request.headers["authorization"] then + -- assumes the header value starts with "Bearer " + token = request.headers["authorization"]:sub(8,#request.headers["authorization"]) + end + if query ~= nil then local params = formdecode(query); @@ -24,9 +32,23 @@ function init_session(event) -- The room name and optional prefix from the web query session.jitsi_web_query_room = params.room; session.jitsi_web_query_prefix = params.prefix or ""; + + -- The following fields are filled in the session, by extracting them + -- from the query and no validation is being done. + -- After validating auth_token will be cleaned in case of error and few + -- other fields will be extracted from the token and set in the session + + if params and params.token then + token = params.token; + end + end session.user_region = request.headers[region_header_name]; + + -- in either case set auth_token in the session + session.auth_token = token; + session.user_agent_header = request.headers['user_agent']; end module:hook_global("bosh-session", init_session, 1); diff --git a/resources/prosody-plugins/mod_muc_meeting_id.lua b/resources/prosody-plugins/mod_muc_meeting_id.lua index 514583ed9923..c10cf1aae2c1 100644 --- a/resources/prosody-plugins/mod_muc_meeting_id.lua +++ b/resources/prosody-plugins/mod_muc_meeting_id.lua @@ -16,6 +16,7 @@ local is_transcriber = util.is_transcriber; local QUEUE_MAX_SIZE = 500; module:depends("jitsi_permissions"); +module:depends("jitsi_session"); -- Common module for all logic that can be loaded under the conference muc component. -- diff --git a/resources/prosody-plugins/mod_muc_wait_for_host.lua b/resources/prosody-plugins/mod_muc_wait_for_host.lua index 461401d76d75..e37bcc880624 100644 --- a/resources/prosody-plugins/mod_muc_wait_for_host.lua +++ b/resources/prosody-plugins/mod_muc_wait_for_host.lua @@ -57,7 +57,10 @@ module:hook('muc-occupant-pre-join', function (event) local has_host = false; for _, o in room:each_occupant() do - if jid.host(o.bare_jid) == muc_domain_base then + -- the main virtual host that requires tokens + if jid.host(o.bare_jid) == muc_domain_base + -- or this is anonymous that upgraded by passing token which we validated + or prosody.full_sessions[o.jid].auth_token then room.has_host = true; end end diff --git a/static/logout.html b/static/logout.html new file mode 100644 index 000000000000..217e4adf4e99 --- /dev/null +++ b/static/logout.html @@ -0,0 +1,43 @@ + + + + + + Logged Out + + + +
+
+

You have been logged out successfully.

+
+ + + + diff --git a/static/sso.html b/static/sso.html new file mode 100644 index 000000000000..9a8eb2ff89a3 --- /dev/null +++ b/static/sso.html @@ -0,0 +1,373 @@ + + + + + + SSO Authentication + + + +
+
+ +
+ +
+
+ + + +