Verified Commit 147538fa authored by Chocobozzz's avatar Chocobozzz
Browse files

Add saml2 auth support

parent 018d8e62
# SAML2 auth plugin for PeerTube
Add SAML2 support to login form in PeerTube.
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 }
}
{
"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=="
}
}
}
{
"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"
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment