-
Notifications
You must be signed in to change notification settings - Fork 7.7k
Inline authentication #16910
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Inline authentication #16910
Changes from 3 commits
7424f03
7cbc638
7a93da7
2e4961e
91ea707
1cc639a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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'; | ||
|
|
||
|
|
@@ -46,6 +50,134 @@ 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); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is sessionStorage.setItem( so we set it here. We generate it.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is now implemented.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
| } | ||
|
|
||
| // @ts-ignore | ||
| const handler = event => { | ||
| // Verify origin | ||
| if (event.origin !== window.location.origin) { | ||
hristoterezov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return; | ||
| } | ||
|
|
||
| if (event.data.type === 'oauth-success') { | ||
| window.removeEventListener('message', handler); | ||
| popup.close(); | ||
|
|
||
| sessionStorage.removeItem('oauth_nonce'); | ||
|
|
||
| resolve({ | ||
| accessToken: event.data.accessToken, | ||
| idToken: event.data.idToken, | ||
| refreshToken: event.data.refreshToken | ||
| }); | ||
| } else if (event.data.type === 'oauth-error') { | ||
| window.removeEventListener('message', handler); | ||
| popup.close(); | ||
| reject(new Error(event.data.error)); | ||
| } | ||
| }; | ||
|
|
||
| // Listen for messages from the popup | ||
| window.addEventListener('message', handler); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we should add the listener to |
||
|
|
||
| // Check if popup was closed | ||
| const checkClosed = setInterval(() => { | ||
|
||
| if (popup.closed) { | ||
| clearInterval(checkClosed); | ||
| window.removeEventListener('message', handler); | ||
| reject(new Error('Login cancelled')); | ||
| } | ||
| }, 1000); | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * 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); | ||
|
|
||
| let timerId: any = undefined; | ||
|
|
||
| // Listen for logout completion | ||
| // @ts-ignore | ||
| const handler = event => { | ||
| if (event.origin !== window.location.origin) return; | ||
hristoterezov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| if (event.data.type === 'logout-success') { | ||
| window.removeEventListener('message', handler); | ||
| document.body.removeChild(iframe); | ||
|
|
||
| timerId && clearTimeout(timerId); | ||
|
|
||
| resolve(); | ||
| } | ||
| }; | ||
|
|
||
| window.addEventListener('message', handler); | ||
|
|
||
| // Fallback timeout | ||
| timerId = setTimeout(() => { | ||
| window.removeEventListener('message', handler); | ||
| if (iframe.parentNode) { | ||
| document.body.removeChild(iframe); | ||
| } | ||
| resolve(); // Assume success after timeout | ||
| }, 3000); | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Opens token auth URL page. | ||
| * | ||
|
|
@@ -63,6 +195,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, { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 = { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why the |
||
| room: roomName, | ||
| roomSafe: getBackendSafeRoomName(roomName), | ||
| tenant | ||
| }; | ||
|
|
||
| if (refreshToken) { | ||
| state.refreshToken = refreshToken; | ||
| } | ||
|
|
||
| const { | ||
| audioMuted = false, | ||
| audioOnlyEnabled = false, | ||
|
|
||
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.