peertube-theme-nctv-dark/peertube-plugin-auth-saml2/main.js
2021-12-23 11:51:50 +01:00

338 lines
9.0 KiB
JavaScript

const saml2 = require('@peertube/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: '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: 'logout-url',
label: 'SSO logout URL (needs PeerTube >= 3.0.0)',
type: 'input',
private: true
})
registerSetting({
name: 'provider-certificate',
label: 'Identity provider certificate (PEM format)',
type: 'input-textarea',
private: true
})
registerSetting({
name: 'service-certificate',
label: 'Service certificate (PEM format)',
type: 'input-textarea',
private: true
})
registerSetting({
name: 'service-private-key',
label: 'Service private key (PEM format)',
type: 'input-textarea',
private: true
})
registerSetting({
name: 'sign-get-request',
label: 'Sign get request',
type: 'input-checkbox',
private: true,
default: false
})
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('/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',
'logout-url',
'provider-certificate',
'service-certificate',
'service-private-key'
])
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
}
logger.debug('Creating SAML service/identity instances.', { settings })
const serviceOptions = {
entity_id: settings['client-id'],
private_key: settings['service-private-key'],
certificate: settings['service-certificate'],
assert_endpoint: store.assertUrl
}
store.serviceProvider = new saml2.ServiceProvider(serviceOptions)
const identityOptions = {
sso_login_url: settings['login-url'],
sso_logout_url: settings['logout-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) => {
try {
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)
})
} catch (err) {
logger.error('Cannot create login request url.', { err })
return redirectOnError(res)
}
},
onLogout: (user, req) => {
// Return silently if logout-url is not specified
if (!settings['logout-url']) {
return
}
return new Promise(async (resolve, reject) => {
try {
const options = await storageManager.getData(`saml_session_${req.cookies.saml_session}`)
// Include nameid format so the SLO can be accepted.
// See xmlbuilder for the JS object format.
options.name_id = {
"@Format": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
"#text": options.name_id
}
store.serviceProvider.create_logout_request_url(store.identityProvider, options, (err, logoutUrl) => {
if (err) {
reject('Cannot SAML 2 logout.', { err })
return
}
resolve(logoutUrl)
})
} catch (err) {
reject('Cannot create logout request url.', { err })
}
return
})
}
})
store.userAuthenticated = result.userAuthenticated
store.storageManager = storageManager
}
function handleAssert(peertubeHelpers, settingsManager, req, res) {
const { logger } = peertubeHelpers
if (req.query.SAMLResponse) {
// This is a HTTP-redirect for a LogoutResponse and not a SamlResponse after a login request.
// So we do not want to assert it with post_assert as it will throw an error.
return res.redirect(peertubeHelpers.config.getWebserverUrl())
}
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)
// Store the nameid and session_index in the plugin database.
// Create a cookie called 'saml_session' so we can match later.
const session_id = crypto.randomBytes(10).toString("hex")
res.cookie('saml_session', session_id, { httpOnly: true, secure: true })
store.storageManager.storeData(`saml_session_${session_id}`, {
name_id: samlResponse.user.name_id,
session_index: samlResponse.user.session_index
})
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'])
}
}