Possibility to configure an OpenID Connect provider on the instance level WIP (#128)
Get avatar from remote service.
This commit is contained in:
parent
2334a5f861
commit
8a65f447c8
@ -1,11 +1,14 @@
|
||||
local json = require "util.json";
|
||||
local jid_split = require"util.jid".split;
|
||||
local usermanager = require "core.usermanager";
|
||||
local st = require "util.stanza"
|
||||
|
||||
module:depends"http";
|
||||
|
||||
local module_host = module:get_host(); -- this module is not global
|
||||
|
||||
local vcards = module:open_store("vcard");
|
||||
|
||||
function check_auth(routes)
|
||||
local function check_request_auth(event)
|
||||
local apikey = module:get_option_string("peertubelivechat_manage_users_apikey", "")
|
||||
@ -31,6 +34,26 @@ function check_auth(routes)
|
||||
end
|
||||
|
||||
|
||||
local function update_vcard(username, avatar)
|
||||
if not avatar then
|
||||
module:log("debug", "No avatar for user %s, deleting vcard", username);
|
||||
vcards:set(username, nil)
|
||||
return
|
||||
end
|
||||
|
||||
module:log("debug", "There is a avatar for user %s, storing the relevant vcard.", username);
|
||||
local vcard_temp = st.stanza("vCard", { xmlns = "vcard-temp" });
|
||||
vcard_temp:tag("PHOTO");
|
||||
vcard_temp:text_tag("TYPE", avatar.mimetype);
|
||||
-- avatar.base64 is already base64 encoded
|
||||
vcard_temp:text_tag("BINVAL", avatar.base64);
|
||||
vcard_temp:up();
|
||||
if not vcards:set(username, st.preserialize(vcard_temp)) then
|
||||
module:log("error", "Failed to store the vcard for user %s", username);
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function ensure_user(event)
|
||||
local request, response = event.request, event.response;
|
||||
event.response.headers["Content-Type"] = "application/json";
|
||||
@ -42,29 +65,30 @@ local function ensure_user(event)
|
||||
});
|
||||
end
|
||||
|
||||
module:log("debug", "Calling ensure_user", config.jid);
|
||||
module:log("debug", "Calling ensure_user for %s", config.jid);
|
||||
|
||||
local username, host = jid_split(config.jid);
|
||||
if module_host ~= host then
|
||||
module:log("error", "Wrong host", host);
|
||||
module:log("error", "Wrong host %s", host);
|
||||
return json.encode({
|
||||
result = "failed";
|
||||
message = "Wrong host"
|
||||
});
|
||||
end
|
||||
|
||||
-- TODO: handle avatars.
|
||||
|
||||
-- if user exists, just update.
|
||||
if usermanager.user_exists(username, host) then
|
||||
module:log("debug", "User already exists, updating", filename);
|
||||
module:log("debug", "User already exists, updating");
|
||||
if not usermanager.set_password(username, config.password, host, nil) then
|
||||
module:log("error", "Failed to update the password", host);
|
||||
module:log("error", "Failed to update the password");
|
||||
return json.encode({
|
||||
result = "failed";
|
||||
message = "Failed to update the password"
|
||||
});
|
||||
end
|
||||
|
||||
update_vcard(username, config.avatar);
|
||||
|
||||
return json.encode({
|
||||
result = "ok";
|
||||
message = "User updated"
|
||||
@ -72,14 +96,17 @@ local function ensure_user(event)
|
||||
end
|
||||
|
||||
-- we must create the user.
|
||||
module:log("debug", "User does not exists, creating", filename);
|
||||
module:log("debug", "User does not exists, creating");
|
||||
if (not usermanager.create_user(username, config.password, host)) then
|
||||
module:log("error", "Failed to create the user", host);
|
||||
module:log("error", "Failed to create the user");
|
||||
return json.encode({
|
||||
result = "failed";
|
||||
message = "Failed to create the user"
|
||||
});
|
||||
end
|
||||
|
||||
update_vcard(username, config.avatar);
|
||||
|
||||
return json.encode({
|
||||
result = "ok";
|
||||
message = "User created"
|
||||
|
@ -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
|
||||
}
|
||||
)
|
||||
))
|
||||
|
Loading…
x
Reference in New Issue
Block a user