Verified Commit abcface9 authored by Chocobozzz's avatar Chocobozzz
Browse files

Add openid connect plugin

parent 8f70a17f
# OpenID Connect auth plugin for PeerTube
Add OpenID Connect support to login form in PeerTube.
const openidModule = require('openid-client')
const crypto = require('crypto')
const store = {
client: null,
userAuthenticated: null,
secretKey: null,
redirectUrl: null
}
const encryptionOptions = {
algorithm: 'aes256',
inputEncoding: 'utf8',
outputEncoding: 'hex'
}
const cookieName = 'plugin-auth-openid-code-verifier'
async function register ({
registerExternalAuth,
unregisterExternalAuth,
registerSetting,
settingsManager,
peertubeHelpers,
getRouter
}) {
const { logger } = peertubeHelpers
registerSetting({
name: 'discover-url',
label: 'Discover URL',
type: 'input',
private: true
})
registerSetting({
name: 'client-id',
label: 'Client ID',
type: 'input',
private: true
})
registerSetting({
name: 'client-secret',
label: 'Client secret',
type: 'input',
private: true
})
registerSetting({
name: 'username-property',
label: 'Username property',
type: 'input',
private: true,
default: 'preferred_username'
})
registerSetting({
name: 'mail-property',
label: 'Email property',
type: 'input',
private: true,
default: 'email'
})
registerSetting({
name: 'display-name-property',
label: 'Display name property',
type: 'input',
private: true
})
registerSetting({
name: 'role-property',
label: 'Role property',
type: 'input',
private: true
})
const router = getRouter()
router.use('/id-token-cb', (req, res) => handleCb(peertubeHelpers, settingsManager, req, res))
store.redirectUrl = peertubeHelpers.config.getWebserverUrl() + '/plugins/auth-openid-connect/router/id-token-cb'
const secretKeyBuf = await getRandomBytes(16)
store.secretKey = secretKeyBuf.toString('hex')
await loadSettingsAndCreateClient(registerExternalAuth, unregisterExternalAuth, peertubeHelpers, settingsManager)
settingsManager.onSettingsChange(() => {
loadSettingsAndCreateClient(registerExternalAuth, unregisterExternalAuth, peertubeHelpers, settingsManager)
.catch(err => logger.error('Cannot load settings and create client after settings changes.', { err }))
})
}
async function unregister () {
return
}
module.exports = {
register,
unregister
}
// ############################################################################
async function loadSettingsAndCreateClient (registerExternalAuth, unregisterExternalAuth, peertubeHelpers, settingsManager) {
const { logger, config } = peertubeHelpers
if (store.client) {
unregisterExternalAuth('openid')
}
store.client = null
store.userAuthenticated = null
const settings = await settingsManager.getSettings([
'discover-url',
'client-id',
'client-secret'
])
if (!settings['discover-url']) {
logger.info('Do not register external openid auth because discover URL is not set.')
return
}
if (!settings['client-id']) {
logger.info('Do not register external openid auth because client ID is not set.')
return
}
const discoverUrl = settings['discover-url']
const issuer = await openidModule.Issuer.discover(discoverUrl)
logger.debug('Discovered issuer %s.', discoverUrl)
const clientOptions = {
client_id: settings['client-id'],
redirect_uris: [ store.redirectUrl ],
response_types: [ 'code' ]
}
if (settings['client-secret']) {
clientOptions.client_secret = settings['client-secret']
} else {
clientOptions.token_endpoint_auth_method = 'none'
}
store.client = new issuer.Client(clientOptions)
// We already registered this external auth
if (store.userAuthenticated) return
const webserverUrl = config.getWebserverUrl()
const result = registerExternalAuth({
authName: 'openid-connect',
authDisplayName: () => 'OpenID Connect',
onAuthRequest: async (req, res) => {
try {
const codeVerifier = openidModule.generators.codeVerifier()
const codeChallenge = openidModule.generators.codeChallenge(codeVerifier)
const redirectUrl = store.client.authorizationUrl({
scope: 'openid email profile',
response_mode: 'form_post',
code_challenge: codeChallenge,
code_challenge_method: 'S256'
})
const encryptedCodeVerifier = await encrypt(codeVerifier)
res.cookie(cookieName, encryptedCodeVerifier, {
secure: webserverUrl.startsWith('https://'),
httpOnly: true,
sameSite: 'none',
maxAge: 1000 * 60 * 10 // 10 minutes
})
return res.redirect(redirectUrl)
} catch (err) {
logger.error('Cannot handle auth request.', { err })
}
}
})
store.userAuthenticated = result.userAuthenticated
}
async function handleCb (peertubeHelpers, settingsManager, req, res) {
const { logger } = peertubeHelpers
if (!store.userAuthenticated) {
logger.info('Received callback but cannot userAuthenticated function does not exist.')
return res.sendStatus(400)
}
const encryptedCodeVerifier = req.cookies[cookieName]
if (!encryptedCodeVerifier) {
logger.error('Received callback but code verifier not found in request cookie.')
return res.sendStatus(400)
}
try {
const codeVerifier = await decrypt(encryptedCodeVerifier)
const params = store.client.callbackParams(req)
const tokenSet = await store.client.callback(store.redirectUrl, params, { code_verifier: codeVerifier })
const accessToken = tokenSet.access_token
const userInfo = await store.client.userinfo(accessToken)
const settings = await settingsManager.getSettings([
'mail-property',
'username-property',
'display-name-property',
'role-property'
])
logger.debug('Got userinfo from openid auth.', { userInfo, settings })
let role
if (settings['role-property']) {
role = parseInt('' + userInfo[settings['role-property']], 10)
}
let displayName
if (settings['display-name-property']) {
displayName = userInfo[settings['display-name-property']]
}
let username = userInfo[settings['username-property']] || ''
username = username.replace(/[^a-z0-9._]/g, '_')
store.userAuthenticated({
res,
req,
username,
email: userInfo[settings['mail-property']],
displayName,
role
})
} catch (err) {
logger.error('Error in handle callback.', { err })
res.sendStatus(400)
}
}
async function encrypt (data) {
const { algorithm, inputEncoding, outputEncoding } = encryptionOptions
const iv = await getRandomBytes(16)
const cipher = crypto.createCipheriv(algorithm, store.secretKey, iv)
let encrypted = cipher.update(data, inputEncoding, outputEncoding)
encrypted += cipher.final(outputEncoding)
return iv.toString(outputEncoding) + ':' + encrypted
}
async function decrypt (data) {
const { algorithm, inputEncoding, outputEncoding } = encryptionOptions
const encryptedArray = data.split(':')
const iv = Buffer.from(encryptedArray[0], outputEncoding)
const encrypted = Buffer.from(encryptedArray[1], outputEncoding)
const decipher = crypto.createDecipheriv(algorithm, store.secretKey, iv)
return decipher.update(encrypted, outputEncoding, inputEncoding) + decipher.final(inputEncoding)
}
function getRandomBytes (size) {
return new Promise((res, rej) => {
crypto.randomBytes(size, (err, buf) => {
if (err) return rej(err)
return res(buf)
})
})
}
{
"name": "peertube-plugin-auth-openid",
"version": "0.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@panva/asn1.js": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz",
"integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw=="
},
"@sindresorhus/is": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
"integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ=="
},
"@szmarczak/http-timer": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
"integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==",
"requires": {
"defer-to-connect": "^1.0.1"
}
},
"@types/got": {
"version": "9.6.10",
"resolved": "https://registry.npmjs.org/@types/got/-/got-9.6.10.tgz",
"integrity": "sha512-owBY1cgHUIXjObzY+vs+J9Cpw0czvfksJX+qEkgxRojFutFq7n1tKoj6Ekg57DhvXMk0vGQ7FbinvS9I/1wxcg==",
"requires": {
"@types/node": "*",
"@types/tough-cookie": "*",
"form-data": "^2.5.0"
}
},
"@types/node": {
"version": "13.13.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.4.tgz",
"integrity": "sha512-x26ur3dSXgv5AwKS0lNfbjpCakGIduWU1DU91Zz58ONRWrIKGunmZBNv4P7N+e27sJkiGDsw/3fT4AtsqQBrBA=="
},
"@types/tough-cookie": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz",
"integrity": "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A=="
},
"aggregate-error": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz",
"integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==",
"requires": {
"clean-stack": "^2.0.0",
"indent-string": "^4.0.0"
}
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"base64url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A=="
},
"cacheable-request": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz",
"integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==",
"requires": {
"clone-response": "^1.0.2",
"get-stream": "^5.1.0",
"http-cache-semantics": "^4.0.0",
"keyv": "^3.0.0",
"lowercase-keys": "^2.0.0",
"normalize-url": "^4.1.0",
"responselike": "^1.0.2"
},
"dependencies": {
"get-stream": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz",
"integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==",
"requires": {
"pump": "^3.0.0"
}
},
"lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="
}
}
},
"clean-stack": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="
},
"clone-response": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
"integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=",
"requires": {
"mimic-response": "^1.0.0"
}
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"requires": {
"delayed-stream": "~1.0.0"
}
},
"decompress-response": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
"integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
"requires": {
"mimic-response": "^1.0.0"
}
},
"defer-to-connect": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz",
"integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ=="
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
},
"duplexer3": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
},
"end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"requires": {
"once": "^1.4.0"
}
},
"form-data": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
"integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
}
},
"get-stream": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
"integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
"requires": {
"pump": "^3.0.0"
}
},
"got": {
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz",
"integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==",
"requires": {
"@sindresorhus/is": "^0.14.0",
"@szmarczak/http-timer": "^1.1.2",
"cacheable-request": "^6.0.0",
"decompress-response": "^3.3.0",
"duplexer3": "^0.1.4",
"get-stream": "^4.1.0",
"lowercase-keys": "^1.0.1",
"mimic-response": "^1.0.1",
"p-cancelable": "^1.0.0",
"to-readable-stream": "^1.0.0",
"url-parse-lax": "^3.0.0"
}
},
"http-cache-semantics": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
"integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ=="
},
"indent-string": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="
},
"jose": {
"version": "1.26.1",
"resolved": "https://registry.npmjs.org/jose/-/jose-1.26.1.tgz",
"integrity": "sha512-2WQu8SezuTI/1FqvzEJGNLiSojGyFwnZK562rwoz2yx02Ad8k0IQ37bt7UaF/oyQiyn7he4VFfQnOAB5EHHUVw==",
"requires": {
"@panva/asn1.js": "^1.0.0"
}
},
"json-buffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
"integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg="
},
"keyv": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
"integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==",
"requires": {
"json-buffer": "3.0.0"
}
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
},
"lowercase-keys": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
"integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA=="
},
"lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"requires": {
"yallist": "^3.0.2"
}
},
"make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
},
"mime-db": {
"version": "1.44.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
"integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg=="
},
"mime-types": {
"version": "2.1.27",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz",
"integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==",
"requires": {
"mime-db": "1.44.0"
}
},
"mimic-response": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="
},
"normalize-url": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz",
"integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ=="
},
"object-hash": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.0.3.tgz",
"integrity": "sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg=="
},
"oidc-token-hash": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.0.tgz",
"integrity": "sha512-8Yr4CZSv+Tn8ZkN3iN2i2w2G92mUKClp4z7EGUfdsERiYSbj7P4i/NHm72ft+aUdsiFx9UdIPSTwbyzQ6C4URg=="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"requires": {
"wrappy": "1"
}
},
"openid-client": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-3.15.0.tgz",
"integrity": "sha512-Xuvn0q1gQPcfkDYsEunrCiiMfy+ZZFjYMWxp5bkjiuj4BxhFbBo6RjlmCbhTLxdDnun0fp/qsC67bWL9AzP1BA==",
"requires": {
"@types/got": "^9.6.9",
"base64url": "^3.0.1",
"got": "^9.6.0",
"jose": "^1.25.2",
"lodash": "^4.17.15",
"lru-cache": "^5.1.1",
"make-error": "^1.3.6",
"object-hash": "^2.0.1",
"oidc-token-hash": "^5.0.0",
"p-any": "^3.0.0"
}
},
"p-any": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-any/-/p-any-3.0.0.tgz",
"integrity": "sha512-5rqbqfsRWNb0sukt0awwgJMlaep+8jV45S15SKKB34z4UuzjcofIfnriCBhWjZP2jbVtjt9yRl7buB6RlKsu9w==",
"requires": {
"p-cancelable": "^2.0.0",
"p-some": "^5.0.0"
},
"dependencies": {
"p-cancelable": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz",
"integrity": "sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg=="
}