From 6c758634726e941fdeead35bc923cba9d9748d21 Mon Sep 17 00:00:00 2001 From: John Livingston Date: Wed, 17 Apr 2024 12:09:25 +0200 Subject: [PATCH] Possibility to configure an OpenID Connect provider on the instance level WIP (#128). --- conversejs/builtin.ts | 3 +- .../custom/livechat-external-login-content.js | 5 +- .../shared/modals/livechat-external-login.js | 10 ++++ .../livechat-external-login-modal.js | 42 +++++++++++++- conversejs/loc.keys.js | 3 +- languages/en.yml | 1 + server/lib/external-auth/oidc.ts | 35 +++++++----- server/lib/routers/oidc.ts | 55 +++++++++++++------ shared/lib/types.ts | 18 +++++- 9 files changed, 135 insertions(+), 37 deletions(-) diff --git a/conversejs/builtin.ts b/conversejs/builtin.ts index a7fd05dd..2edb765b 100644 --- a/conversejs/builtin.ts +++ b/conversejs/builtin.ts @@ -1,4 +1,4 @@ -import type { InitConverseJSParams, ChatIncludeMode } from 'shared/lib/types' +import type { InitConverseJSParams, ChatIncludeMode, OIDCAuthResult } from 'shared/lib/types' import { inIframe } from './lib/utils' import { initDom } from './lib/dom' import { @@ -28,6 +28,7 @@ declare global { initConversePlugins: typeof initConversePlugins initConverse: typeof initConverse reconnectConverse?: (room: string) => void + oidcGetResult?: (data: OIDCAuthResult) => void } } diff --git a/conversejs/custom/livechat-external-login-content.js b/conversejs/custom/livechat-external-login-content.js index 481dca66..0072ec8c 100644 --- a/conversejs/custom/livechat-external-login-content.js +++ b/conversejs/custom/livechat-external-login-content.js @@ -6,6 +6,7 @@ import { __ } from 'i18n' export default class LivechatExternalLoginContentElement extends CustomElement { static get properties () { return { + external_auth_oidc_alert_message: { type: String, attribute: false }, remote_peertube_state: { type: String, attribute: false }, remote_peertube_alert_message: { type: String, attribute: false }, remote_peertube_try_anyway_url: { type: String, attribute: false } @@ -19,13 +20,14 @@ export default class LivechatExternalLoginContentElement extends CustomElement { render () { return tplExternalLoginModal(this, { + external_auth_oidc_alert_message: this.external_auth_oidc_alert_message, remote_peertube_state: this.remote_peertube_state, remote_peertube_alert_message: this.remote_peertube_alert_message, remote_peertube_try_anyway_url: this.remote_peertube_try_anyway_url }) } - onKeyUp (_ev) { + onRemotePeertubeKeyUp (_ev) { if (this.remote_peertube_state !== 'init') { this.remote_peertube_state = 'init' this.remote_peertube_alert_message = '' @@ -109,6 +111,7 @@ export default class LivechatExternalLoginContentElement extends CustomElement { } clearAlert () { + this.external_auth_oidc_alert_message = '' this.remote_peertube_alert_message = '' this.remote_peertube_try_anyway_url = '' } diff --git a/conversejs/custom/shared/modals/livechat-external-login.js b/conversejs/custom/shared/modals/livechat-external-login.js index a583e9b3..7d9d74fb 100644 --- a/conversejs/custom/shared/modals/livechat-external-login.js +++ b/conversejs/custom/shared/modals/livechat-external-login.js @@ -15,6 +15,16 @@ class ExternalLoginModal extends BaseModal { // eslint-disable-next-line no-undef return __(LOC_login_using_external_account) } + + onHide () { + super.onHide() + // kill the oidcGetResult handler if still there + try { + if (window.oidcGetResult) { window.oidcGetResult() } + } catch (err) { + console.error(err) + } + } } api.elements.define('converse-livechat-external-login', ExternalLoginModal) diff --git a/conversejs/custom/templates/livechat-external-login-modal.js b/conversejs/custom/templates/livechat-external-login-modal.js index a2f935c3..bba2570d 100644 --- a/conversejs/custom/templates/livechat-external-login-modal.js +++ b/conversejs/custom/templates/livechat-external-login-modal.js @@ -17,10 +17,48 @@ export const tplExternalLoginModal = (el, o) => {
+ ${!o.external_auth_oidc_alert_message + ? '' + : html`
${o.external_auth_oidc_alert_message}
` + }

` @@ -33,7 +71,7 @@ export const tplExternalLoginModal = (el, o) => { placeholder="${i18nRemotePeertubeUrl}" class="form-control ${o.remote_peertube_alert_message ? 'is-invalid' : ''}" name="peertube_url" - @keyup=${el.onKeyUp} + @keyup=${el.onRemotePeertubeKeyUp} ?disabled=${o.remote_peertube_state === 'loading'} /> diff --git a/conversejs/loc.keys.js b/conversejs/loc.keys.js index 730fcb2b..ac16a300 100644 --- a/conversejs/loc.keys.js +++ b/conversejs/loc.keys.js @@ -13,7 +13,8 @@ const locKeys = [ 'login_remote_peertube_no_livechat', 'login_remote_peertube_video_not_found', 'login_remote_peertube_video_not_found_try_anyway', - 'login_remote_peertube_video_not_found_try_anyway_button' + 'login_remote_peertube_video_not_found_try_anyway_button', + 'login_external_oidc_alert_message' ] module.exports = locKeys diff --git a/languages/en.yml b/languages/en.yml index a13ef52a..2f3fc312 100644 --- a/languages/en.yml +++ b/languages/en.yml @@ -421,3 +421,4 @@ login_remote_peertube_no_livechat: "The livechat plugin is not installed on this login_remote_peertube_video_not_found: "This video is not available on this Peertube instance." login_remote_peertube_video_not_found_try_anyway: "In some cases, the video can still be retrieved if you connect to the remote instance." login_remote_peertube_video_not_found_try_anyway_button: "Try anyway to open the video on the Peertube instance" +login_external_oidc_alert_message: "Authentication failed" diff --git a/server/lib/external-auth/oidc.ts b/server/lib/external-auth/oidc.ts index 54732687..6147661b 100644 --- a/server/lib/external-auth/oidc.ts +++ b/server/lib/external-auth/oidc.ts @@ -1,5 +1,5 @@ import type { RegisterServerOptions } from '@peertube/peertube-types' -import type { Request } from 'express' +import type { Request, Response, CookieOptions } from 'express' import { URL } from 'url' import { Issuer, BaseClient, generators } from 'openid-client' import { getBaseRouterRoute } from '../helpers' @@ -37,6 +37,14 @@ class ExternalAuthOIDC { outputEncoding: 'hex' as Encoding } + private readonly cookieNamePrefix: string = 'peertube-plugin-livechat-oidc-' + private readonly cookieOptions: CookieOptions = { + secure: true, + httpOnly: true, + sameSite: 'none', + maxAge: 1000 * 60 * 10 // 10 minutes + } + private ok: boolean | undefined private issuer: Issuer | undefined | null @@ -217,12 +225,11 @@ class ExternalAuthOIDC { /** * Returns everything that is needed to instanciate an OIDC authentication. + * @param req express request + * @param res express response. Will add some cookies. + * @return the url to which redirect */ - async initAuthenticationProcess (): Promise<{ - encryptedCodeVerifier: string - encryptedState: string - redirectUrl: string - }> { + async initAuthenticationProcess (req: Request, res: Response): Promise { if (!this.client) { throw new Error('External Auth OIDC not loaded yet, too soon to call oidc.initAuthentication') } @@ -242,29 +249,27 @@ class ExternalAuthOIDC { state }) - return { - encryptedCodeVerifier, - encryptedState, - redirectUrl - } + res.cookie(this.cookieNamePrefix + 'code-verifier', encryptedCodeVerifier, this.cookieOptions) + res.cookie(this.cookieNamePrefix + 'state', encryptedState, this.cookieOptions) + return redirectUrl } /** * Authentication process callback. - * @param req The ExpressJS request object. + * @param req The ExpressJS request object. Will read cookies. * @return user info */ - async validateAuthenticationProcess (req: Request, cookieNamePrefix: string): Promise { + async validateAuthenticationProcess (req: Request): Promise { if (!this.client) { throw new Error('External Auth OIDC not loaded yet, too soon to call oidc.validateAuthenticationProcess') } - const encryptedCodeVerifier = req.cookies[cookieNamePrefix + 'code-verifier'] + const encryptedCodeVerifier = req.cookies[this.cookieNamePrefix + 'code-verifier'] if (!encryptedCodeVerifier) { throw new Error('Received callback but code verifier not found in request cookies.') } - const encryptedState = req.cookies[cookieNamePrefix + 'state'] + const encryptedState = req.cookies[this.cookieNamePrefix + 'state'] if (!encryptedState) { throw new Error('Received callback but state not found in request cookies.') } diff --git a/server/lib/routers/oidc.ts b/server/lib/routers/oidc.ts index a7acfce2..934939b8 100644 --- a/server/lib/routers/oidc.ts +++ b/server/lib/routers/oidc.ts @@ -1,14 +1,32 @@ import type { RegisterServerOptions } from '@peertube/peertube-types' -import type { Router, Request, Response, NextFunction, CookieOptions } from 'express' +import type { Router, Request, Response, NextFunction } from 'express' +import type { OIDCAuthResult } from '../../../shared/lib/types' import { asyncMiddleware } from '../middlewares/async' import { ExternalAuthOIDC } from '../external-auth/oidc' -const cookieNamePrefix = 'peertube-plugin-livechat-oidc-' -const cookieOptions: CookieOptions = { - secure: true, - httpOnly: true, - sameSite: 'none', - maxAge: 1000 * 60 * 10 // 10 minutes +/** + * When using a popup for OIDC, writes the HTML/Javascript to close the popup + * and send the result to the parent window. + * @param result the result to send to the parent window + */ +function popupResultHTML (result: OIDCAuthResult): string { + return ` + + + + + ` } async function initOIDCRouter (options: RegisterServerOptions): Promise { @@ -26,10 +44,8 @@ async function initOIDCRouter (options: RegisterServerOptions): Promise throw new Error('[oidc router] External Auth OIDC not loaded yet') } - const authenticationProcess = await oidc.initAuthenticationProcess() - res.cookie(cookieNamePrefix + 'code-verifier', authenticationProcess.encryptedCodeVerifier, cookieOptions) - res.cookie(cookieNamePrefix + 'state', authenticationProcess.encryptedState, cookieOptions) - return res.redirect(authenticationProcess.redirectUrl) + const redirectUrl = await oidc.initAuthenticationProcess(req, res) + res.redirect(redirectUrl) } catch (err) { logger.error('[oidc router] Failed to process the OIDC callback: ' + (err as string)) next() @@ -38,7 +54,7 @@ async function initOIDCRouter (options: RegisterServerOptions): Promise )) router.get('/cb', asyncMiddleware( - async (req: Request, res: Response, next: NextFunction) => { + async (req: Request, res: Response, _next: NextFunction) => { logger.info('[oidc router] OIDC callback call') try { const oidc = ExternalAuthOIDC.singleton() @@ -47,13 +63,20 @@ async function initOIDCRouter (options: RegisterServerOptions): Promise throw new Error('[oidc router] External Auth OIDC not loaded yet') } - const userInfos = await oidc.validateAuthenticationProcess(req, cookieNamePrefix) - logger.info(JSON.stringify(userInfos)) // FIXME + const userInfos = await oidc.validateAuthenticationProcess(req) + logger.info(JSON.stringify(userInfos)) // FIXME (normalize data type, process, ...) - res.send('ok') + res.send(popupResultHTML({ + ok: true, + username: userInfos.username, + password: 'TODO' + })) } catch (err) { logger.error('[oidc router] Failed to process the OIDC callback: ' + (err as string)) - next() + res.sendStatus(500) + res.send(popupResultHTML({ + ok: false + })) } } )) diff --git a/shared/lib/types.ts b/shared/lib/types.ts index 6a109dbb..24dfcb4a 100644 --- a/shared/lib/types.ts +++ b/shared/lib/types.ts @@ -107,6 +107,19 @@ type ChatPeertubeIncludeMode = 'peertube-fullpage' | 'peertube-video' */ type ChatIncludeMode = 'chat-only' | ChatPeertubeIncludeMode +interface OIDCAuthResultError { + ok: true + username: string + password: string +} + +interface OIDCAuthResultOk { + ok: false + message?: string +} + +type OIDCAuthResult = OIDCAuthResultError | OIDCAuthResultOk + export type { ConverseJSTheme, InitConverseJSParams, @@ -117,5 +130,8 @@ export type { ChannelConfigurationOptions, ChannelConfiguration, ChatIncludeMode, - ChatPeertubeIncludeMode + ChatPeertubeIncludeMode, + OIDCAuthResultError, + OIDCAuthResultOk, + OIDCAuthResult }