diff --git a/lib/web/auth/index.ts b/lib/web/auth/index.ts index 98dafb44a..29997c945 100644 --- a/lib/web/auth/index.ts +++ b/lib/web/auth/index.ts @@ -10,7 +10,7 @@ import { GitlabMiddleware } from './gitlab' import { DropboxMiddleware } from './dropbox' import google from './google' import ldap from './ldap' -import saml from './saml' +import { SamlMiddleware } from './saml' import oauth2 from './oauth2' import email from './email' import openid from './openid' @@ -50,7 +50,7 @@ if (config.isGitLabEnable) AuthRouter.use(GitlabMiddleware.getMiddleware()) if (config.isDropboxEnable) AuthRouter.use(DropboxMiddleware.getMiddleware()) if (config.isGoogleEnable) AuthRouter.use(google) if (config.isLDAPEnable) AuthRouter.use(ldap) -if (config.isSAMLEnable) AuthRouter.use(saml) +if (config.isSAMLEnable) AuthRouter.use(SamlMiddleware.getMiddleware()) if (config.isOAuth2Enable) AuthRouter.use(oauth2) if (config.isEmailEnable) AuthRouter.use(email) if (config.isOpenIDEnable) AuthRouter.use(openid) diff --git a/lib/web/auth/saml/index.js b/lib/web/auth/saml/index.js deleted file mode 100644 index 40a6f8b34..000000000 --- a/lib/web/auth/saml/index.js +++ /dev/null @@ -1,96 +0,0 @@ -'use strict' - -const Router = require('express').Router -const passport = require('passport') -const SamlStrategy = require('passport-saml').Strategy -const config = require('../../../config') -const models = require('../../../models') -const logger = require('../../../logger') -const { urlencodedParser } = require('../../utils') -const fs = require('fs') -const intersection = function (array1, array2) { return array1.filter((n) => array2.includes(n)) } - -let samlAuth = module.exports = Router() - -passport.use(new SamlStrategy({ - callbackUrl: config.serverURL + '/auth/saml/callback', - entryPoint: config.saml.idpSsoUrl, - issuer: config.saml.issuer || config.serverURL, - cert: fs.readFileSync(config.saml.idpCert, 'utf-8'), - identifierFormat: config.saml.identifierFormat, - disableRequestedAuthnContext: config.saml.disableRequestedAuthnContext -}, function (user, done) { - // check authorization if needed - if (config.saml.externalGroups && config.saml.groupAttribute) { - var externalGroups = intersection(config.saml.externalGroups, user[config.saml.groupAttribute]) - if (externalGroups.length > 0) { - logger.error('saml permission denied: ' + externalGroups.join(', ')) - return done('Permission denied', null) - } - } - if (config.saml.requiredGroups && config.saml.groupAttribute) { - if (intersection(config.saml.requiredGroups, user[config.saml.groupAttribute]).length === 0) { - logger.error('saml permission denied') - return done('Permission denied', null) - } - } - // user creation - var uuid = user[config.saml.attribute.id] || user.nameID - var profile = { - provider: 'saml', - id: 'SAML-' + uuid, - username: user[config.saml.attribute.username] || user.nameID, - emails: user[config.saml.attribute.email] ? [user[config.saml.attribute.email]] : [] - } - if (profile.emails.length === 0 && config.saml.identifierFormat === 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress') { - profile.emails.push(user.nameID) - } - var stringifiedProfile = JSON.stringify(profile) - models.User.findOrCreate({ - where: { - profileid: profile.id.toString() - }, - defaults: { - profile: stringifiedProfile - } - }).spread(function (user, created) { - if (user) { - var needSave = false - if (user.profile !== stringifiedProfile) { - user.profile = stringifiedProfile - needSave = true - } - if (needSave) { - user.save().then(function () { - logger.debug(`user login: ${user.id}`) - return done(null, user) - }) - } else { - logger.debug(`user login: ${user.id}`) - return done(null, user) - } - } - }).catch(function (err) { - logger.error('saml auth failed: ' + err) - return done(err, null) - }) -})) - -samlAuth.get('/auth/saml', - passport.authenticate('saml', { - successReturnToOrRedirect: config.serverURL + '/', - failureRedirect: config.serverURL + '/' - }) -) - -samlAuth.post('/auth/saml/callback', urlencodedParser, - passport.authenticate('saml', { - successReturnToOrRedirect: config.serverURL + '/', - failureRedirect: config.serverURL + '/' - }) -) - -samlAuth.get('/auth/saml/metadata', function (req, res) { - res.type('application/xml') - res.send(passport._strategy('saml').generateServiceProviderMetadata()) -}) diff --git a/lib/web/auth/saml/index.ts b/lib/web/auth/saml/index.ts new file mode 100644 index 000000000..9e11e8ff5 --- /dev/null +++ b/lib/web/auth/saml/index.ts @@ -0,0 +1,107 @@ +import { Router } from 'express' +import passport from 'passport' +import { Strategy as SamlStrategy } from 'passport-saml' +import fs from 'fs' + +import { config } from '../../../config' +import { User } from '../../../models' +import { logger } from '../../../logger' +import { urlencodedParser } from '../../utils' +import { AuthMiddleware } from '../interface' + +function intersection (array1: T[], array2: T[]): T[] { + return array1.filter((n) => array2.includes(n)) +} + +export const SamlMiddleware: AuthMiddleware = { + getMiddleware (): Router { + const SamlAuth = Router() + + const samlStrategy = new SamlStrategy({ + callbackUrl: config.serverURL + '/auth/saml/callback', + entryPoint: config.saml.idpSsoUrl, + issuer: config.saml.issuer || config.serverURL, + cert: fs.readFileSync(config.saml.idpCert, 'utf-8'), + identifierFormat: config.saml.identifierFormat, + disableRequestedAuthnContext: config.saml.disableRequestedAuthnContext + }, function (user, done) { + // check authorization if needed + if (config.saml.externalGroups && config.saml.groupAttribute) { + const externalGroups: string[] = intersection(config.saml.externalGroups, user[config.saml.groupAttribute]) + if (externalGroups.length > 0) { + logger.error('saml permission denied: ' + externalGroups.join(', ')) + return done('Permission denied', null) + } + } + if (config.saml.requiredGroups && config.saml.groupAttribute) { + if (intersection(config.saml.requiredGroups, user[config.saml.groupAttribute]).length === 0) { + logger.error('saml permission denied') + return done('Permission denied', null) + } + } + // user creation + const uuid = user[config.saml.attribute.id] || user.nameID + const profile = { + provider: 'saml', + id: 'SAML-' + uuid, + username: user[config.saml.attribute.username] || user.nameID, + emails: user[config.saml.attribute.email] ? [user[config.saml.attribute.email]] : [] + } + if (profile.emails.length === 0 && config.saml.identifierFormat === 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress') { + profile.emails.push(user.nameID) + } + const stringifiedProfile = JSON.stringify(profile) + User.findOrCreate({ + where: { + profileid: profile.id.toString() + }, + defaults: { + profile: stringifiedProfile + } + }).then(function ([user, _]) { + if (user) { + let needSave = false + if (user.profile !== stringifiedProfile) { + user.profile = stringifiedProfile + needSave = true + } + if (needSave) { + user.save().then(function () { + logger.debug(`user login: ${user.id}`) + return done(null, user) + }) + } else { + logger.debug(`user login: ${user.id}`) + return done(null, user) + } + } + }).catch(function (err) { + logger.error('saml auth failed: ' + err) + return done(err, null) + }) + }) + + passport.use(samlStrategy) + + SamlAuth.get('/auth/saml', + passport.authenticate('saml', { + successReturnToOrRedirect: config.serverURL + '/', + failureRedirect: config.serverURL + '/' + }) + ) + + SamlAuth.post('/auth/saml/callback', urlencodedParser, + passport.authenticate('saml', { + successReturnToOrRedirect: config.serverURL + '/', + failureRedirect: config.serverURL + '/' + }) + ) + + SamlAuth.get('/auth/saml/metadata', function (req, res) { + res.type('application/xml') + res.send(samlStrategy.generateServiceProviderMetadata(null)) + }) + + return SamlAuth + } +}