Possibility to configure an OpenID Connect provider on the instance level WIP (#128).
This commit is contained in:
parent
3a5f27e751
commit
6c13d2e377
@ -151,7 +151,7 @@ async function displayConverseJS (
|
|||||||
(forceType ? '?forcetype=1' : ''),
|
(forceType ? '?forcetype=1' : ''),
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: peertubeHelpers.getAuthHeader()
|
headers: authHeader
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
@ -107,7 +107,11 @@ async function initConverse (
|
|||||||
let isAuthenticated: boolean = false
|
let isAuthenticated: boolean = false
|
||||||
let isRemoteWithNicknameSet: 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 (auth) {
|
||||||
if (!isRemoteChat) {
|
if (!isRemoteChat) {
|
||||||
localRoomAuthenticatedParams(initConverseParams, auth, params)
|
localRoomAuthenticatedParams(initConverseParams, auth, params)
|
||||||
@ -160,8 +164,11 @@ async function initConverse (
|
|||||||
|
|
||||||
// no viewer mode if authenticated.
|
// no viewer mode if authenticated.
|
||||||
params.livechat_enable_viewer_mode = autoViewerMode && !isAuthenticated && !isRemoteWithNicknameSet
|
params.livechat_enable_viewer_mode = autoViewerMode && !isAuthenticated && !isRemoteWithNicknameSet
|
||||||
|
|
||||||
|
if (tryOIDC && !isAuthenticated) {
|
||||||
params.livechat_external_auth_oidc_button_label = initConverseParams.externalAuthOIDC?.buttonLabel
|
params.livechat_external_auth_oidc_button_label = initConverseParams.externalAuthOIDC?.buttonLabel
|
||||||
params.livechat_external_auth_oidc_url = initConverseParams.externalAuthOIDC?.url
|
params.livechat_external_auth_oidc_url = initConverseParams.externalAuthOIDC?.url
|
||||||
|
}
|
||||||
|
|
||||||
if (chatIncludeMode === 'peertube-video') {
|
if (chatIncludeMode === 'peertube-video') {
|
||||||
params.livechat_mini_muc_head = true // we must replace the muc-head by the custom buttons toolbar.
|
params.livechat_mini_muc_head = true // we must replace the muc-head by the custom buttons toolbar.
|
||||||
|
@ -11,7 +11,7 @@ export const tplExternalLoginModal = (el, o) => {
|
|||||||
const externalAuthOIDCButtonLabel = api.settings.get('livechat_external_auth_oidc_button_label')
|
const externalAuthOIDCButtonLabel = api.settings.get('livechat_external_auth_oidc_button_label')
|
||||||
const externalAuthOIDCUrl = api.settings.get('livechat_external_auth_oidc_url')
|
const externalAuthOIDCUrl = api.settings.get('livechat_external_auth_oidc_url')
|
||||||
return html`<div class="modal-body livechat-external-login-modal">
|
return html`<div class="modal-body livechat-external-login-modal">
|
||||||
${!externalAuthOIDCButtonLabel || !externalAuthOIDCUrl
|
${!externalAuthOIDCButtonLabel || !externalAuthOIDCUrl || !window.sessionStorage
|
||||||
? ''
|
? ''
|
||||||
: html`
|
: html`
|
||||||
<div class="livechat-external-login-modal-external-auth-oidc">
|
<div class="livechat-external-login-modal-external-auth-oidc">
|
||||||
@ -45,9 +45,13 @@ export const tplExternalLoginModal = (el, o) => {
|
|||||||
(data.message ? ` (${data.message})` : '')
|
(data.message ? ` (${data.message})` : '')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// TODO
|
|
||||||
console.info('Got external account information', data)
|
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
|
return false
|
||||||
|
@ -8,6 +8,7 @@ interface AuthHeader { [key: string]: string }
|
|||||||
|
|
||||||
async function getLocalAuthentInfos (
|
async function getLocalAuthentInfos (
|
||||||
authenticationUrl: string,
|
authenticationUrl: string,
|
||||||
|
tryOIDC: boolean,
|
||||||
peertubeAuthHeader?: AuthHeader | null
|
peertubeAuthHeader?: AuthHeader | null
|
||||||
): Promise<false | AuthentInfos> {
|
): Promise<false | AuthentInfos> {
|
||||||
try {
|
try {
|
||||||
@ -20,11 +21,6 @@ async function getLocalAuthentInfos (
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (peertubeAuthHeader === null) {
|
|
||||||
console.info('User is not logged in.')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (peertubeAuthHeader === undefined) { // parameter not given.
|
if (peertubeAuthHeader === undefined) { // parameter not given.
|
||||||
// We must be in a page without PeertubeHelpers, so we must get authent token manualy.
|
// We must be in a page without PeertubeHelpers, so we must get authent token manualy.
|
||||||
if (!window.localStorage) {
|
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, {
|
const response = await window.fetch(authenticationUrl, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: new Headers(
|
headers: new Headers(
|
||||||
Object.assign(
|
Object.assign(
|
||||||
{},
|
{},
|
||||||
peertubeAuthHeader,
|
peertubeAuthHeader ?? {},
|
||||||
|
oidcHeaders ?? {},
|
||||||
{
|
{
|
||||||
'content-type': 'application/json;charset=UTF-8'
|
'content-type': 'application/json;charset=UTF-8'
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,13 @@ import { URL } from 'url'
|
|||||||
|
|
||||||
type UserInfoField = 'username' | 'last_name' | 'first_name' | 'nickname'
|
type UserInfoField = 'username' | 'last_name' | 'first_name' | 'nickname'
|
||||||
|
|
||||||
|
interface UnserializedToken {
|
||||||
|
jid: string
|
||||||
|
password: string
|
||||||
|
nickname: string
|
||||||
|
expire: Date
|
||||||
|
}
|
||||||
|
|
||||||
let singleton: ExternalAuthOIDC | undefined
|
let singleton: ExternalAuthOIDC | undefined
|
||||||
|
|
||||||
async function getRandomBytes (size: number): Promise<Buffer> {
|
async function getRandomBytes (size: number): Promise<Buffer> {
|
||||||
@ -328,16 +335,28 @@ class ExternalAuthOIDC {
|
|||||||
nickname ??= username
|
nickname ??= username
|
||||||
|
|
||||||
// Computing the JID (can throw Error/ExternalAuthenticationError).
|
// Computing the JID (can throw Error/ExternalAuthenticationError).
|
||||||
const jid = this.computeJID(username)
|
const jid = this.computeJID(username).toString(false)
|
||||||
|
|
||||||
// Computing a random Password
|
// Computing a random Password
|
||||||
// (16 bytes in hex => 32 chars (but only numbers and abdcef), 256^16 should be enougth).
|
// (16 bytes in hex => 32 chars (but only numbers and abdcef), 256^16 should be enougth).
|
||||||
const password = (await getRandomBytes(16)).toString('hex')
|
const password = (await getRandomBytes(16)).toString('hex')
|
||||||
|
|
||||||
return {
|
// Now we will encrypt jid + password, and return it to the browser.
|
||||||
jid: jid.toString(false),
|
// The browser will be able to use this encrypted data with the api/configuration/room API.
|
||||||
|
const tokenContent: UnserializedToken = {
|
||||||
|
jid,
|
||||||
|
password,
|
||||||
nickname,
|
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)
|
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<UnserializedToken | null> {
|
||||||
|
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.
|
* Get an attribute from the userInfos.
|
||||||
* @param userInfos userInfos returned by the remote OIDC Provider
|
* @param userInfos userInfos returned by the remote OIDC Provider
|
||||||
|
@ -2,6 +2,7 @@ interface ExternalAccountInfos {
|
|||||||
nickname: string
|
nickname: string
|
||||||
jid: string
|
jid: string
|
||||||
password: string
|
password: string
|
||||||
|
token: string
|
||||||
// TODO: avatar
|
// TODO: avatar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import { asyncMiddleware } from '../../middlewares/async'
|
|||||||
import { getProsodyDomain } from '../../prosody/config/domain'
|
import { getProsodyDomain } from '../../prosody/config/domain'
|
||||||
import { prosodyRegisterUser, prosodyCheckUserPassword, prosodyUserRegistered } from '../../prosody/auth'
|
import { prosodyRegisterUser, prosodyCheckUserPassword, prosodyUserRegistered } from '../../prosody/auth'
|
||||||
import { getUserNickname } from '../../helpers'
|
import { getUserNickname } from '../../helpers'
|
||||||
|
import { ExternalAuthOIDC } from '../../external-auth/oidc'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instanciate the authentication API.
|
* Instanciate the authentication API.
|
||||||
@ -14,6 +15,31 @@ async function initAuthApiRouter (options: RegisterServerOptions, router: Router
|
|||||||
router.get('/auth', asyncMiddleware(
|
router.get('/auth', asyncMiddleware(
|
||||||
async (req: Request, res: Response, _next: NextFunction) => {
|
async (req: Request, res: Response, _next: NextFunction) => {
|
||||||
const user = await options.peertubeHelpers.user.getAuthUser(res)
|
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) {
|
if (!user) {
|
||||||
res.sendStatus(403)
|
res.sendStatus(403)
|
||||||
return
|
return
|
||||||
|
@ -71,7 +71,8 @@ async function initOIDCRouter (options: RegisterServerOptions): Promise<Router>
|
|||||||
{},
|
{},
|
||||||
externalAccountInfos,
|
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<Router>
|
|||||||
|
|
||||||
res.send(popupResultHTML({
|
res.send(popupResultHTML({
|
||||||
ok: true,
|
ok: true,
|
||||||
jid: externalAccountInfos.jid,
|
token: externalAccountInfos.token
|
||||||
password: externalAccountInfos.password
|
|
||||||
}))
|
}))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('[oidc router] Failed to process the OIDC callback: ' + (err as string))
|
logger.error('[oidc router] Failed to process the OIDC callback: ' + (err as string))
|
||||||
|
@ -107,13 +107,12 @@ type ChatPeertubeIncludeMode = 'peertube-fullpage' | 'peertube-video'
|
|||||||
*/
|
*/
|
||||||
type ChatIncludeMode = 'chat-only' | ChatPeertubeIncludeMode
|
type ChatIncludeMode = 'chat-only' | ChatPeertubeIncludeMode
|
||||||
|
|
||||||
interface OIDCAuthResultError {
|
interface OIDCAuthResultOk {
|
||||||
ok: true
|
ok: true
|
||||||
jid: string
|
token: string
|
||||||
password: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OIDCAuthResultOk {
|
interface OIDCAuthResultError {
|
||||||
ok: false
|
ok: false
|
||||||
message?: string
|
message?: string
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user