Add saml2 auth support
This commit is contained in:
		
							
								
								
									
										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" | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user