diff --git a/client/utils/conversejs.ts b/client/utils/conversejs.ts
index b6588e55..fc7ad6c1 100644
--- a/client/utils/conversejs.ts
+++ b/client/utils/conversejs.ts
@@ -151,7 +151,7 @@ async function displayConverseJS (
(forceType ? '?forcetype=1' : ''),
{
method: 'GET',
- headers: peertubeHelpers.getAuthHeader()
+ headers: authHeader
}
)
if (!response.ok) {
diff --git a/conversejs/builtin.ts b/conversejs/builtin.ts
index 2edb765b..57a1a495 100644
--- a/conversejs/builtin.ts
+++ b/conversejs/builtin.ts
@@ -107,7 +107,11 @@ async function initConverse (
let isAuthenticated: boolean = false
let isRemoteWithNicknameSet: boolean = false
- const auth = await getLocalAuthentInfos(authenticationUrl, peertubeAuthHeader)
+ // OIDC (OpenID Connect):
+ const tryOIDC = !!initConverseParams.externalAuthOIDC
+
+ const auth = await getLocalAuthentInfos(authenticationUrl, tryOIDC, peertubeAuthHeader)
+
if (auth) {
if (!isRemoteChat) {
localRoomAuthenticatedParams(initConverseParams, auth, params)
@@ -160,8 +164,11 @@ async function initConverse (
// no viewer mode if authenticated.
params.livechat_enable_viewer_mode = autoViewerMode && !isAuthenticated && !isRemoteWithNicknameSet
- params.livechat_external_auth_oidc_button_label = initConverseParams.externalAuthOIDC?.buttonLabel
- params.livechat_external_auth_oidc_url = initConverseParams.externalAuthOIDC?.url
+
+ if (tryOIDC && !isAuthenticated) {
+ params.livechat_external_auth_oidc_button_label = initConverseParams.externalAuthOIDC?.buttonLabel
+ params.livechat_external_auth_oidc_url = initConverseParams.externalAuthOIDC?.url
+ }
if (chatIncludeMode === 'peertube-video') {
params.livechat_mini_muc_head = true // we must replace the muc-head by the custom buttons toolbar.
diff --git a/conversejs/custom/templates/livechat-external-login-modal.js b/conversejs/custom/templates/livechat-external-login-modal.js
index e6065b8b..b7211539 100644
--- a/conversejs/custom/templates/livechat-external-login-modal.js
+++ b/conversejs/custom/templates/livechat-external-login-modal.js
@@ -11,7 +11,7 @@ export const tplExternalLoginModal = (el, o) => {
const externalAuthOIDCButtonLabel = api.settings.get('livechat_external_auth_oidc_button_label')
const externalAuthOIDCUrl = api.settings.get('livechat_external_auth_oidc_url')
return html`
- ${!externalAuthOIDCButtonLabel || !externalAuthOIDCUrl
+ ${!externalAuthOIDCButtonLabel || !externalAuthOIDCUrl || !window.sessionStorage
? ''
: html`
@@ -45,9 +45,13 @@ export const tplExternalLoginModal = (el, o) => {
(data.message ? ` (${data.message})` : '')
return
}
- // TODO
+
console.info('Got external account information', data)
- console.error('not implemented yet')
+ // Storing the token in sessionStorage.
+ window.sessionStorage.setItem('peertube-plugin-livechat-oidc-token', data.token)
+
+ // FIXME: do better.
+ window.location.reload()
}
return false
diff --git a/conversejs/lib/auth.ts b/conversejs/lib/auth.ts
index 8f3d0ed8..b3e467d5 100644
--- a/conversejs/lib/auth.ts
+++ b/conversejs/lib/auth.ts
@@ -8,6 +8,7 @@ interface AuthHeader { [key: string]: string }
async function getLocalAuthentInfos (
authenticationUrl: string,
+ tryOIDC: boolean,
peertubeAuthHeader?: AuthHeader | null
): Promise {
try {
@@ -20,11 +21,6 @@ async function getLocalAuthentInfos (
return false
}
- if (peertubeAuthHeader === null) {
- console.info('User is not logged in.')
- return false
- }
-
if (peertubeAuthHeader === undefined) { // parameter not given.
// We must be in a page without PeertubeHelpers, so we must get authent token manualy.
if (!window.localStorage) {
@@ -45,12 +41,27 @@ async function getLocalAuthentInfos (
}
}
+ let oidcHeaders: any
+ // When user has used the External OIDC mechanisme to create an account, we got a token in sessionStorage.
+ if (tryOIDC && !peertubeAuthHeader && window.sessionStorage) {
+ const token = window.sessionStorage.getItem('peertube-plugin-livechat-oidc-token')
+ if (token && (typeof token === 'string')) {
+ oidcHeaders = { 'X-Peertube-Plugin-Livechat-OIDC-Token': token }
+ }
+ }
+
+ if (peertubeAuthHeader === null && oidcHeaders === undefined) {
+ console.info('User is not logged in.')
+ return false
+ }
+
const response = await window.fetch(authenticationUrl, {
method: 'GET',
headers: new Headers(
Object.assign(
{},
- peertubeAuthHeader,
+ peertubeAuthHeader ?? {},
+ oidcHeaders ?? {},
{
'content-type': 'application/json;charset=UTF-8'
}
diff --git a/server/lib/external-auth/oidc.ts b/server/lib/external-auth/oidc.ts
index b07dc4f8..2db73b22 100644
--- a/server/lib/external-auth/oidc.ts
+++ b/server/lib/external-auth/oidc.ts
@@ -12,6 +12,13 @@ import { URL } from 'url'
type UserInfoField = 'username' | 'last_name' | 'first_name' | 'nickname'
+interface UnserializedToken {
+ jid: string
+ password: string
+ nickname: string
+ expire: Date
+}
+
let singleton: ExternalAuthOIDC | undefined
async function getRandomBytes (size: number): Promise {
@@ -328,16 +335,28 @@ class ExternalAuthOIDC {
nickname ??= username
// Computing the JID (can throw Error/ExternalAuthenticationError).
- const jid = this.computeJID(username)
+ const jid = this.computeJID(username).toString(false)
// Computing a random Password
// (16 bytes in hex => 32 chars (but only numbers and abdcef), 256^16 should be enougth).
const password = (await getRandomBytes(16)).toString('hex')
- return {
- jid: jid.toString(false),
+ // Now we will encrypt jid + password, and return it to the browser.
+ // The browser will be able to use this encrypted data with the api/configuration/room API.
+ const tokenContent: UnserializedToken = {
+ jid,
+ password,
nickname,
- password
+ // expires in 12 hours (user will just have to do the whole process again).
+ expire: (new Date(Date.now() + 12 * 3600 * 1000))
+ }
+ const token = await this.encrypt(JSON.stringify(tokenContent))
+
+ return {
+ jid,
+ nickname,
+ password,
+ token
}
}
@@ -365,6 +384,54 @@ class ExternalAuthOIDC {
return decipher.update(encrypted as any, outputEncoding, inputEncoding) + decipher.final(inputEncoding)
}
+ /**
+ * Decrypt and unserialize a token associated to a previous authentication.
+ * @param token the token stored by the browser.
+ * @return authentication informations, or null if:
+ * if the token is expired, if the token is invalid.
+ * Can also fail (and return null) when server was restarted, or settings saved, as the secret key may have changed
+ * (this is not an issue, users just have to start the process again).
+ */
+ public async unserializeToken (token: string): Promise {
+ try {
+ const decrypted = await this.decrypt(token)
+ const o = JSON.parse(decrypted) // can fail
+
+ if (typeof o !== 'object') {
+ throw new Error('Invalid encrypted data')
+ }
+ if (typeof o.jid !== 'string' || o.jid === '') {
+ throw new Error('No jid')
+ }
+ if (typeof o.password !== 'string' || o.password === '') {
+ throw new Error('No password')
+ }
+ if (typeof o.nickname !== 'string' || o.nickname === '') {
+ throw new Error('No nickname')
+ }
+
+ const expire = new Date(Date.parse(o.expire))
+ if (!(expire instanceof Date) || isNaN(expire.getTime())) {
+ throw new Error('Invalid expire date')
+ }
+
+ if (expire <= new Date()) {
+ throw new Error('Token expired')
+ }
+
+ return {
+ jid: o.jid,
+ password: o.password,
+ nickname: o.nickname,
+ expire
+ }
+ } catch (err) {
+ // This is not an error, as there are many legitimate cases (token expired, ...)
+ this.logger.info('Cant unserialize the token: ' + (err as string))
+ return null
+ }
+ }
+
/**
* Get an attribute from the userInfos.
* @param userInfos userInfos returned by the remote OIDC Provider
diff --git a/server/lib/external-auth/types.ts b/server/lib/external-auth/types.ts
index 4e5f5476..449d3885 100644
--- a/server/lib/external-auth/types.ts
+++ b/server/lib/external-auth/types.ts
@@ -2,6 +2,7 @@ interface ExternalAccountInfos {
nickname: string
jid: string
password: string
+ token: string
// TODO: avatar
}
diff --git a/server/lib/routers/api/auth.ts b/server/lib/routers/api/auth.ts
index 3e17d179..99bca1b4 100644
--- a/server/lib/routers/api/auth.ts
+++ b/server/lib/routers/api/auth.ts
@@ -4,6 +4,7 @@ import { asyncMiddleware } from '../../middlewares/async'
import { getProsodyDomain } from '../../prosody/config/domain'
import { prosodyRegisterUser, prosodyCheckUserPassword, prosodyUserRegistered } from '../../prosody/auth'
import { getUserNickname } from '../../helpers'
+import { ExternalAuthOIDC } from '../../external-auth/oidc'
/**
* Instanciate the authentication API.
@@ -14,6 +15,31 @@ async function initAuthApiRouter (options: RegisterServerOptions, router: Router
router.get('/auth', asyncMiddleware(
async (req: Request, res: Response, _next: NextFunction) => {
const user = await options.peertubeHelpers.user.getAuthUser(res)
+
+ if (!user) {
+ // No Peertube user, but perhaps an external authentication?
+ const token = req.header('X-Peertube-Plugin-Livechat-OIDC-Token')
+ if (token) {
+ try {
+ const oidc = ExternalAuthOIDC.singleton()
+ if (await oidc.isOk()) {
+ const unserializedToken = await oidc.unserializeToken(token)
+ if (unserializedToken) {
+ res.status(200).json({
+ jid: unserializedToken.jid,
+ password: unserializedToken.password,
+ nickname: unserializedToken.nickname
+ })
+ return
+ }
+ }
+ } catch (err) {
+ options.peertubeHelpers.logger.error(err)
+ // Just continue with the normal flow.
+ }
+ }
+ }
+
if (!user) {
res.sendStatus(403)
return
diff --git a/server/lib/routers/oidc.ts b/server/lib/routers/oidc.ts
index e23d15f4..7f575a57 100644
--- a/server/lib/routers/oidc.ts
+++ b/server/lib/routers/oidc.ts
@@ -71,7 +71,8 @@ async function initOIDCRouter (options: RegisterServerOptions): Promise
{},
externalAccountInfos,
{
- password: '**removed**' // removing the password from logs!
+ password: '**removed**', // removing the password from logs!
+ token: '**removed**' // same as password
}
)
))
@@ -85,8 +86,7 @@ async function initOIDCRouter (options: RegisterServerOptions): Promise
res.send(popupResultHTML({
ok: true,
- jid: externalAccountInfos.jid,
- password: externalAccountInfos.password
+ token: externalAccountInfos.token
}))
} catch (err) {
logger.error('[oidc router] Failed to process the OIDC callback: ' + (err as string))
diff --git a/shared/lib/types.ts b/shared/lib/types.ts
index 1ff3da5d..ff36537d 100644
--- a/shared/lib/types.ts
+++ b/shared/lib/types.ts
@@ -107,13 +107,12 @@ type ChatPeertubeIncludeMode = 'peertube-fullpage' | 'peertube-video'
*/
type ChatIncludeMode = 'chat-only' | ChatPeertubeIncludeMode
-interface OIDCAuthResultError {
+interface OIDCAuthResultOk {
ok: true
- jid: string
- password: string
+ token: string
}
-interface OIDCAuthResultOk {
+interface OIDCAuthResultError {
ok: false
message?: string
}