Possibility to configure an OpenID Connect provider on the instance level WIP (#128)
Get avatar from remote service.
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
import type { RegisterServerOptions } from '@peertube/peertube-types'
|
||||
import type { Request, Response, CookieOptions } from 'express'
|
||||
import type { ExternalAccountInfos } from './types'
|
||||
import type { ExternalAccountInfos, AcceptableAvatarMimeType } from './types'
|
||||
import { ExternalAuthenticationError } from './error'
|
||||
import { getBaseRouterRoute } from '../helpers'
|
||||
import { canonicalizePluginUri } from '../uri/canonicalize'
|
||||
@ -10,7 +10,38 @@ import { Issuer, BaseClient, generators, UnknownObject } from 'openid-client'
|
||||
import { JID } from '@xmpp/jid'
|
||||
import { URL } from 'url'
|
||||
|
||||
type UserInfoField = 'username' | 'last_name' | 'first_name' | 'nickname'
|
||||
const got = require('got')
|
||||
|
||||
function getMimeTypeFromArrayBuffer (arrayBuffer: ArrayBuffer): AcceptableAvatarMimeType | null {
|
||||
const uint8arr = new Uint8Array(arrayBuffer)
|
||||
|
||||
const len = 4
|
||||
if (uint8arr.length >= len) {
|
||||
const signatureArr = new Array(len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
signatureArr[i] = (new Uint8Array(arrayBuffer))[i].toString(16)
|
||||
}
|
||||
const signature = signatureArr.join('').toUpperCase()
|
||||
|
||||
switch (signature) {
|
||||
case '89504E47':
|
||||
return 'image/png'
|
||||
case '47494638':
|
||||
return 'image/gif'
|
||||
case 'FFD8FFDB':
|
||||
case 'FFD8FFE0':
|
||||
return 'image/jpeg'
|
||||
case '52494646':
|
||||
case '57454250':
|
||||
return 'image/webp'
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
type UserInfoField = 'username' | 'last_name' | 'first_name' | 'nickname' | 'picture'
|
||||
|
||||
interface UnserializedToken {
|
||||
jid: string
|
||||
@ -354,11 +385,14 @@ class ExternalAuthOIDC {
|
||||
}
|
||||
const token = await this.encrypt(JSON.stringify(tokenContent))
|
||||
|
||||
const avatar = await this.readUserInfoPicture(userInfo)
|
||||
|
||||
return {
|
||||
jid,
|
||||
nickname,
|
||||
password,
|
||||
token
|
||||
token,
|
||||
avatar
|
||||
}
|
||||
}
|
||||
|
||||
@ -458,6 +492,7 @@ class ExternalAuthOIDC {
|
||||
guesses.push('given_name')
|
||||
break
|
||||
case 'nickname':
|
||||
guesses.push('preferred_username')
|
||||
guesses.push('name')
|
||||
break
|
||||
}
|
||||
@ -471,6 +506,39 @@ class ExternalAuthOIDC {
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and get the avatar for the remote user (if exists).
|
||||
* @param userInfos userInfos returned by the remote OIDC Provider.
|
||||
*/
|
||||
private async readUserInfoPicture (userInfos: UnknownObject): Promise<undefined | ExternalAccountInfos['avatar']> {
|
||||
// According to "Standard Claims" section https://openid.net/specs/openid-connect-core-1_0.html,
|
||||
// there should be a `picture` field containing an URL to the avatar
|
||||
const picture = this.readUserInfoField(userInfos, 'picture')
|
||||
if (!picture) { return undefined }
|
||||
|
||||
try {
|
||||
const url = new URL(picture)
|
||||
const buf = await got(url.toString(), {
|
||||
method: 'GET',
|
||||
headers: {},
|
||||
responseType: 'buffer'
|
||||
}).buffer()
|
||||
|
||||
const mimeType = await getMimeTypeFromArrayBuffer(buf)
|
||||
if (!mimeType) {
|
||||
throw new Error('Failed to get the avatar file type')
|
||||
}
|
||||
|
||||
return {
|
||||
mimetype: mimeType,
|
||||
base64: buf.toString('base64')
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(`Failed to get the user avatar: ${err as string}`)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the JID to use for this remote account.
|
||||
* Format will be: "username+remote.domain.tld@external.instance.tld"
|
||||
|
@ -1,11 +1,17 @@
|
||||
type AcceptableAvatarMimeType = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp'
|
||||
|
||||
interface ExternalAccountInfos {
|
||||
nickname: string
|
||||
jid: string
|
||||
password: string
|
||||
token: string
|
||||
// TODO: avatar
|
||||
avatar?: {
|
||||
mimetype: AcceptableAvatarMimeType
|
||||
base64: string
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
ExternalAccountInfos
|
||||
ExternalAccountInfos,
|
||||
AcceptableAvatarMimeType
|
||||
}
|
||||
|
@ -34,7 +34,8 @@ async function ensureUser (options: RegisterServerOptions, infos: ExternalAccoun
|
||||
const apiData = {
|
||||
jid: infos.jid,
|
||||
nickname: infos.nickname,
|
||||
password: infos.password
|
||||
password: infos.password,
|
||||
avatar: infos.avatar
|
||||
}
|
||||
try {
|
||||
logger.debug('Calling ensure-user API on url: ' + apiUrl)
|
||||
|
@ -231,6 +231,7 @@ class ProsodyConfigContent {
|
||||
this.external.set('modules_enabled', [
|
||||
'ping',
|
||||
'http',
|
||||
'vcard',
|
||||
'http_peertubelivechat_manage_users'
|
||||
])
|
||||
this.external.set('peertubelivechat_manage_users_apikey', apikey)
|
||||
|
@ -72,7 +72,10 @@ async function initOIDCRouter (options: RegisterServerOptions): Promise<Router>
|
||||
externalAccountInfos,
|
||||
{
|
||||
password: '**removed**', // removing the password from logs!
|
||||
token: '**removed**' // same as password
|
||||
token: '**removed**', // same as password
|
||||
avatar: externalAccountInfos.avatar
|
||||
? `**removed** ${externalAccountInfos.avatar.mimetype} avatar`
|
||||
: undefined
|
||||
}
|
||||
)
|
||||
))
|
||||
|
Reference in New Issue
Block a user