Add saml2 auth support
This commit is contained in:
parent
018d8e6224
commit
147538fa8e
3
peertube-plugin-auth-saml2/README.md
Normal file
3
peertube-plugin-auth-saml2/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# SAML2 auth plugin for PeerTube
|
||||
|
||||
Add SAML2 support to login form in PeerTube.
|
294
peertube-plugin-auth-saml2/main.js
Normal file
294
peertube-plugin-auth-saml2/main.js
Normal file
@ -0,0 +1,294 @@
|
||||
const saml2 = require('saml2-js')
|
||||
const crypto = require('crypto')
|
||||
|
||||
const store = {
|
||||
assertUrl: null,
|
||||
authDisplayName: 'SAML 2',
|
||||
serviceProvider: null,
|
||||
identityProvider: null
|
||||
}
|
||||
|
||||
async function register ({
|
||||
registerExternalAuth,
|
||||
unregisterExternalAuth,
|
||||
registerSetting,
|
||||
settingsManager,
|
||||
storageManager,
|
||||
peertubeHelpers,
|
||||
getRouter
|
||||
}) {
|
||||
const { logger } = peertubeHelpers
|
||||
|
||||
const metadataUrl = peertubeHelpers.config.getWebserverUrl() + '/plugins/auth-saml2/router/metadata.xml'
|
||||
|
||||
registerSetting({
|
||||
name: 'client-id',
|
||||
label: 'Client ID',
|
||||
type: 'input',
|
||||
private: true,
|
||||
default: metadataUrl
|
||||
})
|
||||
|
||||
registerSetting({
|
||||
name: 'sign-get-request',
|
||||
label: 'Sign get request',
|
||||
type: 'input-checkbox',
|
||||
private: true,
|
||||
default: false
|
||||
})
|
||||
|
||||
registerSetting({
|
||||
name: 'auth-display-name',
|
||||
label: 'Auth display name',
|
||||
type: 'input',
|
||||
private: true,
|
||||
default: 'SAML 2'
|
||||
})
|
||||
|
||||
registerSetting({
|
||||
name: 'login-url',
|
||||
label: 'SSO login URL',
|
||||
type: 'input',
|
||||
private: true
|
||||
})
|
||||
|
||||
registerSetting({
|
||||
name: 'provider-certificate',
|
||||
label: 'Identity provider certificate',
|
||||
type: 'input-textarea',
|
||||
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()
|
||||
|
||||
store.assertUrl = peertubeHelpers.config.getWebserverUrl() + '/plugins/auth-saml2/router/assert'
|
||||
router.post('/assert', (req, res) => handleAssert(peertubeHelpers, settingsManager, req, res))
|
||||
|
||||
router.get('/metadata.xml', (req, res) => {
|
||||
if (!store.serviceProvider) {
|
||||
logger.warn('Cannot get SAML 2 metadata: service provider not created.')
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
|
||||
res.type('application/xml').send(store.serviceProvider.create_metadata())
|
||||
})
|
||||
|
||||
await loadSettingsAndCreateProviders(registerExternalAuth, unregisterExternalAuth, peertubeHelpers, settingsManager, storageManager)
|
||||
store.authDisplayName = await settingsManager.getSetting('auth-display-name')
|
||||
|
||||
settingsManager.onSettingsChange(settings => {
|
||||
loadSettingsAndCreateProviders(registerExternalAuth, unregisterExternalAuth, peertubeHelpers, settingsManager, storageManager)
|
||||
.catch(err => logger.error('Cannot load settings and create client after settings changes.', { err }))
|
||||
|
||||
if (settings['auth-display-name']) store.authDisplayName = settings['auth-display-name']
|
||||
})
|
||||
}
|
||||
|
||||
async function unregister () {
|
||||
return
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
register,
|
||||
unregister
|
||||
}
|
||||
|
||||
// ############################################################################
|
||||
|
||||
async function loadSettingsAndCreateProviders (
|
||||
registerExternalAuth,
|
||||
unregisterExternalAuth,
|
||||
peertubeHelpers,
|
||||
settingsManager,
|
||||
storageManager
|
||||
) {
|
||||
const { logger } = peertubeHelpers
|
||||
|
||||
if (store.serviceProvider || store.identityProvider) {
|
||||
unregisterExternalAuth('saml2')
|
||||
}
|
||||
|
||||
store.serviceProvider = null
|
||||
store.identityProvider = null
|
||||
|
||||
const settings = await settingsManager.getSettings([
|
||||
'client-id',
|
||||
'sign-get-request',
|
||||
'login-url',
|
||||
'provider-certificate'
|
||||
])
|
||||
|
||||
if (!settings['login-url']) {
|
||||
logger.info('Do not register external saml2 auth because login URL is not set.')
|
||||
return
|
||||
}
|
||||
|
||||
if (!settings['provider-certificate']) {
|
||||
logger.info('Do not register external saml2 auth because provider certificate is not set.')
|
||||
return
|
||||
}
|
||||
|
||||
const { publicKey: servicePublicKey, privateKey: servicePrivateKey } = await lazyLoadServiceCertificates(peertubeHelpers, storageManager)
|
||||
|
||||
const serviceOptions = {
|
||||
entity_id: settings['client-id'],
|
||||
private_key: servicePrivateKey,
|
||||
certificate: servicePublicKey,
|
||||
assert_endpoint: store.assertUrl
|
||||
}
|
||||
store.serviceProvider = new saml2.ServiceProvider(serviceOptions)
|
||||
|
||||
const identityOptions = {
|
||||
sso_login_url: settings['login-url'],
|
||||
certificates: [
|
||||
settings['provider-certificate']
|
||||
],
|
||||
sign_get_request: settings['sign-get-request'],
|
||||
allow_unencrypted_assertion: true
|
||||
}
|
||||
store.identityProvider = new saml2.IdentityProvider(identityOptions)
|
||||
|
||||
const result = registerExternalAuth({
|
||||
authName: 'saml2',
|
||||
authDisplayName: () => store.authDisplayName,
|
||||
onAuthRequest: async (req, res) => {
|
||||
store.serviceProvider.create_login_request_url(store.identityProvider, {}, (err, loginUrl, requestId) => {
|
||||
if (err) {
|
||||
logger.error('Cannot SAML 2 authenticate.', { err })
|
||||
return redirectOnError(res)
|
||||
}
|
||||
|
||||
res.redirect(loginUrl)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
store.userAuthenticated = result.userAuthenticated
|
||||
}
|
||||
|
||||
function handleAssert(peertubeHelpers, settingsManager, req, res) {
|
||||
const { logger } = peertubeHelpers
|
||||
|
||||
const options = { request_body: req.body }
|
||||
|
||||
store.serviceProvider.post_assert(store.identityProvider, options, async (err, samlResponse) => {
|
||||
if (err) {
|
||||
logger.error('Error SAML 2 assert.', { err })
|
||||
return redirectOnError(res)
|
||||
}
|
||||
|
||||
logger.debug('User authenticated by SAML 2.', { samlResponse })
|
||||
|
||||
try {
|
||||
const user = await buildUser(settingsManager, samlResponse.user)
|
||||
|
||||
return store.userAuthenticated({
|
||||
req,
|
||||
res,
|
||||
...user
|
||||
})
|
||||
} catch (err) {
|
||||
logger.error('Error SAML 2 build user.', { err })
|
||||
return redirectOnError(res)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function redirectOnError (res) {
|
||||
res.redirect('/login?externalAuthError=true')
|
||||
}
|
||||
|
||||
function findInUser (samlUser, key) {
|
||||
if (!key) return undefined
|
||||
|
||||
if (samlUser[key]) return samlUser[key]
|
||||
|
||||
if (samlUser.attributes[key]) return samlUser.attributes[key][0]
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
async function buildUser (settingsManager, samlUser) {
|
||||
const settings = await settingsManager.getSettings([
|
||||
'mail-property',
|
||||
'username-property',
|
||||
'display-name-property',
|
||||
'role-property'
|
||||
])
|
||||
|
||||
let username = findInUser(samlUser, settings['username-property']) || ''
|
||||
username = username.replace(/[^a-z0-9._]/g, '_')
|
||||
|
||||
return {
|
||||
username,
|
||||
email: findInUser(samlUser, settings['mail-property']),
|
||||
displayName: findInUser(samlUser, settings['display-name-property']),
|
||||
role: findInUser(samlUser, settings['role-property'])
|
||||
}
|
||||
}
|
||||
|
||||
async function lazyLoadServiceCertificates (peertubeHelpers, storageManager) {
|
||||
const { logger } = peertubeHelpers
|
||||
|
||||
let privateKey = await storageManager.getData('service-private-key')
|
||||
let publicKey = await storageManager.getData('service-public-key')
|
||||
|
||||
if (!privateKey || !publicKey) {
|
||||
logger.info('Generating public/private keys for SAML 2.')
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
const options = {
|
||||
modulusLength: 2048,
|
||||
publicKeyEncoding: {
|
||||
type: 'spki',
|
||||
format: 'pem'
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: 'pkcs8',
|
||||
format: 'pem'
|
||||
}
|
||||
}
|
||||
|
||||
crypto.generateKeyPair('rsa', options, (err, publicKey, privateKey) => {
|
||||
if (err) return rej(err)
|
||||
|
||||
Promise.all([
|
||||
storageManager.storeData('service-private-key', privateKey),
|
||||
storageManager.storeData('service-public-key', publicKey)
|
||||
]).then(() => res({ publicKey, privateKey }))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return { privateKey, publicKey }
|
||||
}
|
141
peertube-plugin-auth-saml2/package-lock.json
generated
Normal file
141
peertube-plugin-auth-saml2/package-lock.json
generated
Normal file
@ -0,0 +1,141 @@
|
||||
{
|
||||
"name": "peertube-plugin-auth-saml2",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"async": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
||||
"requires": {
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"ejs": {
|
||||
"version": "2.7.4",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz",
|
||||
"integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA=="
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.15",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
|
||||
},
|
||||
"lodash-node": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash-node/-/lodash-node-2.4.1.tgz",
|
||||
"integrity": "sha1-6oL3sQDHM9GkKvdoAeUGEF4qgOw="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"node-forge": {
|
||||
"version": "0.7.6",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz",
|
||||
"integrity": "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw=="
|
||||
},
|
||||
"saml2-js": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/saml2-js/-/saml2-js-2.0.5.tgz",
|
||||
"integrity": "sha512-8OOZxeMxpdtEszFbvR01TcjKb8g/HsE1si/EGdTawSQY0KgwtvPTG2Ctc5xhEW45xhU+j+UrU5ttZxC72YXSUQ==",
|
||||
"requires": {
|
||||
"async": "^2.5.0",
|
||||
"debug": "^2.6.0",
|
||||
"underscore": "^1.8.0",
|
||||
"xml-crypto": "^0.10.0",
|
||||
"xml-encryption": "^0.11.0",
|
||||
"xml2js": "^0.4.0",
|
||||
"xmlbuilder": "~2.2.0",
|
||||
"xmldom": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
|
||||
},
|
||||
"underscore": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.10.2.tgz",
|
||||
"integrity": "sha512-N4P+Q/BuyuEKFJ43B9gYuOj4TQUHXX+j2FqguVOpjkssLUUrnJofCcBccJSCoeturDoZU6GorDTHSvUDlSQbTg=="
|
||||
},
|
||||
"xml-crypto": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-0.10.1.tgz",
|
||||
"integrity": "sha1-+DL3TM9W8kr8rhFjofyrRNlndKg=",
|
||||
"requires": {
|
||||
"xmldom": "=0.1.19",
|
||||
"xpath.js": ">=0.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"xmldom": {
|
||||
"version": "0.1.19",
|
||||
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.19.tgz",
|
||||
"integrity": "sha1-Yx/Ad3bv2EEYvyUXGzftTQdaCrw="
|
||||
}
|
||||
}
|
||||
},
|
||||
"xml-encryption": {
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmjs.org/xml-encryption/-/xml-encryption-0.11.2.tgz",
|
||||
"integrity": "sha512-jVvES7i5ovdO7N+NjgncA326xYKjhqeAnnvIgRnY7ROLCfFqEDLwP0Sxp/30SHG0AXQV1048T5yinOFyvwGFzg==",
|
||||
"requires": {
|
||||
"async": "^2.1.5",
|
||||
"ejs": "^2.5.6",
|
||||
"node-forge": "^0.7.0",
|
||||
"xmldom": "~0.1.15",
|
||||
"xpath": "0.0.27"
|
||||
}
|
||||
},
|
||||
"xml2js": {
|
||||
"version": "0.4.23",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
||||
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
|
||||
"requires": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~11.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"xmlbuilder": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"xmlbuilder": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-2.2.1.tgz",
|
||||
"integrity": "sha1-kyZDDxMNh0NdTECGZDqikm4QWjI=",
|
||||
"requires": {
|
||||
"lodash-node": "~2.4.1"
|
||||
}
|
||||
},
|
||||
"xmldom": {
|
||||
"version": "0.1.31",
|
||||
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz",
|
||||
"integrity": "sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ=="
|
||||
},
|
||||
"xpath": {
|
||||
"version": "0.0.27",
|
||||
"resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz",
|
||||
"integrity": "sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ=="
|
||||
},
|
||||
"xpath.js": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/xpath.js/-/xpath.js-1.1.0.tgz",
|
||||
"integrity": "sha512-jg+qkfS4K8E7965sqaUl8mRngXiKb3WZGfONgE18pr03FUQiuSV6G+Ej4tS55B+rIQSFEIw3phdVAQ4pPqNWfQ=="
|
||||
}
|
||||
}
|
||||
}
|
24
peertube-plugin-auth-saml2/package.json
Normal file
24
peertube-plugin-auth-saml2/package.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "peertube-plugin-auth-saml2",
|
||||
"version": "0.0.1",
|
||||
"description": "Add SAML 2 support to login form in PeerTube.",
|
||||
"engine": {
|
||||
"peertube": ">=2.2.0"
|
||||
},
|
||||
"keywords": [
|
||||
"peertube",
|
||||
"plugin",
|
||||
"auth"
|
||||
],
|
||||
"homepage": "https://framagit.org/framasoft/peertube/official-plugins/tree/master/peertube-plugin-auth-saml2",
|
||||
"author": "Chocobozzz",
|
||||
"bugs": "https://framagit.org/framasoft/peertube/official-plugins/issues",
|
||||
"library": "./main.js",
|
||||
"staticDirs": {},
|
||||
"css": [],
|
||||
"clientScripts": [],
|
||||
"translations": {},
|
||||
"dependencies": {
|
||||
"saml2-js": "^2.0.5"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user