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 json = require "util.json";
|
||||||
local jid_split = require"util.jid".split;
|
local jid_split = require"util.jid".split;
|
||||||
local usermanager = require "core.usermanager";
|
local usermanager = require "core.usermanager";
|
||||||
|
local st = require "util.stanza"
|
||||||
|
|
||||||
module:depends"http";
|
module:depends"http";
|
||||||
|
|
||||||
local module_host = module:get_host(); -- this module is not global
|
local module_host = module:get_host(); -- this module is not global
|
||||||
|
|
||||||
|
local vcards = module:open_store("vcard");
|
||||||
|
|
||||||
function check_auth(routes)
|
function check_auth(routes)
|
||||||
local function check_request_auth(event)
|
local function check_request_auth(event)
|
||||||
local apikey = module:get_option_string("peertubelivechat_manage_users_apikey", "")
|
local apikey = module:get_option_string("peertubelivechat_manage_users_apikey", "")
|
||||||
@ -31,6 +34,26 @@ function check_auth(routes)
|
|||||||
end
|
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 function ensure_user(event)
|
||||||
local request, response = event.request, event.response;
|
local request, response = event.request, event.response;
|
||||||
event.response.headers["Content-Type"] = "application/json";
|
event.response.headers["Content-Type"] = "application/json";
|
||||||
@ -42,29 +65,30 @@ local function ensure_user(event)
|
|||||||
});
|
});
|
||||||
end
|
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);
|
local username, host = jid_split(config.jid);
|
||||||
if module_host ~= host then
|
if module_host ~= host then
|
||||||
module:log("error", "Wrong host", host);
|
module:log("error", "Wrong host %s", host);
|
||||||
return json.encode({
|
return json.encode({
|
||||||
result = "failed";
|
result = "failed";
|
||||||
message = "Wrong host"
|
message = "Wrong host"
|
||||||
});
|
});
|
||||||
end
|
end
|
||||||
|
|
||||||
-- TODO: handle avatars.
|
|
||||||
|
|
||||||
-- if user exists, just update.
|
-- if user exists, just update.
|
||||||
if usermanager.user_exists(username, host) then
|
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
|
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({
|
return json.encode({
|
||||||
result = "failed";
|
result = "failed";
|
||||||
message = "Failed to update the password"
|
message = "Failed to update the password"
|
||||||
});
|
});
|
||||||
end
|
end
|
||||||
|
|
||||||
|
update_vcard(username, config.avatar);
|
||||||
|
|
||||||
return json.encode({
|
return json.encode({
|
||||||
result = "ok";
|
result = "ok";
|
||||||
message = "User updated"
|
message = "User updated"
|
||||||
@ -72,14 +96,17 @@ local function ensure_user(event)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- we must create the user.
|
-- 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
|
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({
|
return json.encode({
|
||||||
result = "failed";
|
result = "failed";
|
||||||
message = "Failed to create the user"
|
message = "Failed to create the user"
|
||||||
});
|
});
|
||||||
end
|
end
|
||||||
|
|
||||||
|
update_vcard(username, config.avatar);
|
||||||
|
|
||||||
return json.encode({
|
return json.encode({
|
||||||
result = "ok";
|
result = "ok";
|
||||||
message = "User created"
|
message = "User created"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { RegisterServerOptions } from '@peertube/peertube-types'
|
import type { RegisterServerOptions } from '@peertube/peertube-types'
|
||||||
import type { Request, Response, CookieOptions } from 'express'
|
import type { Request, Response, CookieOptions } from 'express'
|
||||||
import type { ExternalAccountInfos } from './types'
|
import type { ExternalAccountInfos, AcceptableAvatarMimeType } from './types'
|
||||||
import { ExternalAuthenticationError } from './error'
|
import { ExternalAuthenticationError } from './error'
|
||||||
import { getBaseRouterRoute } from '../helpers'
|
import { getBaseRouterRoute } from '../helpers'
|
||||||
import { canonicalizePluginUri } from '../uri/canonicalize'
|
import { canonicalizePluginUri } from '../uri/canonicalize'
|
||||||
@ -10,7 +10,38 @@ import { Issuer, BaseClient, generators, UnknownObject } from 'openid-client'
|
|||||||
import { JID } from '@xmpp/jid'
|
import { JID } from '@xmpp/jid'
|
||||||
import { URL } from 'url'
|
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 {
|
interface UnserializedToken {
|
||||||
jid: string
|
jid: string
|
||||||
@ -354,11 +385,14 @@ class ExternalAuthOIDC {
|
|||||||
}
|
}
|
||||||
const token = await this.encrypt(JSON.stringify(tokenContent))
|
const token = await this.encrypt(JSON.stringify(tokenContent))
|
||||||
|
|
||||||
|
const avatar = await this.readUserInfoPicture(userInfo)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
jid,
|
jid,
|
||||||
nickname,
|
nickname,
|
||||||
password,
|
password,
|
||||||
token
|
token,
|
||||||
|
avatar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -458,6 +492,7 @@ class ExternalAuthOIDC {
|
|||||||
guesses.push('given_name')
|
guesses.push('given_name')
|
||||||
break
|
break
|
||||||
case 'nickname':
|
case 'nickname':
|
||||||
|
guesses.push('preferred_username')
|
||||||
guesses.push('name')
|
guesses.push('name')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -471,6 +506,39 @@ class ExternalAuthOIDC {
|
|||||||
return undefined
|
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.
|
* Compute the JID to use for this remote account.
|
||||||
* Format will be: "username+remote.domain.tld@external.instance.tld"
|
* 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 {
|
interface ExternalAccountInfos {
|
||||||
nickname: string
|
nickname: string
|
||||||
jid: string
|
jid: string
|
||||||
password: string
|
password: string
|
||||||
token: string
|
token: string
|
||||||
// TODO: avatar
|
avatar?: {
|
||||||
|
mimetype: AcceptableAvatarMimeType
|
||||||
|
base64: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ExternalAccountInfos
|
ExternalAccountInfos,
|
||||||
|
AcceptableAvatarMimeType
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,8 @@ async function ensureUser (options: RegisterServerOptions, infos: ExternalAccoun
|
|||||||
const apiData = {
|
const apiData = {
|
||||||
jid: infos.jid,
|
jid: infos.jid,
|
||||||
nickname: infos.nickname,
|
nickname: infos.nickname,
|
||||||
password: infos.password
|
password: infos.password,
|
||||||
|
avatar: infos.avatar
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
logger.debug('Calling ensure-user API on url: ' + apiUrl)
|
logger.debug('Calling ensure-user API on url: ' + apiUrl)
|
||||||
|
@ -231,6 +231,7 @@ class ProsodyConfigContent {
|
|||||||
this.external.set('modules_enabled', [
|
this.external.set('modules_enabled', [
|
||||||
'ping',
|
'ping',
|
||||||
'http',
|
'http',
|
||||||
|
'vcard',
|
||||||
'http_peertubelivechat_manage_users'
|
'http_peertubelivechat_manage_users'
|
||||||
])
|
])
|
||||||
this.external.set('peertubelivechat_manage_users_apikey', apikey)
|
this.external.set('peertubelivechat_manage_users_apikey', apikey)
|
||||||
|
@ -72,7 +72,10 @@ 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
|
token: '**removed**', // same as password
|
||||||
|
avatar: externalAccountInfos.avatar
|
||||||
|
? `**removed** ${externalAccountInfos.avatar.mimetype} avatar`
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
))
|
))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user