Skip to content

Commit

Permalink
Merge pull request #755 from mcneilco/754-sso-saml-support
Browse files Browse the repository at this point in the history
754 sso saml support
  • Loading branch information
brianbolt committed Jun 3, 2021
2 parents 4303f15 + 49886b2 commit 8870bc1
Show file tree
Hide file tree
Showing 8 changed files with 292 additions and 59 deletions.
24 changes: 21 additions & 3 deletions app_template.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ startApp = ->
passport = require 'passport'
util = require 'util'
LocalStrategy = require('passport-local').Strategy
SamlStrategy = require('passport-saml').Strategy;
global.deployMode = config.all.client.deployMode

console.log "log level set to '#{console.level}'"
Expand Down Expand Up @@ -52,10 +53,27 @@ startApp = ->
passport.deserializeUser (user, done) ->
done null, user

if csUtilities.loginStrategy.length > 3
passport.use new LocalStrategy {passReqToCallback: true}, csUtilities.loginStrategy
if csUtilities.localLoginStrategy?
localStrategy = csUtilities.localLoginStrategy
else
passport.use new LocalStrategy csUtilities.loginStrategy
console.warn "Please rename CustomerSpecificServerFunction 'exports.loginStrategy' to 'exports.localLoginStrategy' In a future version of ACAS, support for 'loginStrategy' will be replaced with 'localLoginStrategy' but is currently backwards compatable."
localStrategy = csUtilities.localStrategy
if localStrategy.length > 3
passport.use new LocalStrategy {passReqToCallback: true}, localStrategy
else
passport.use new LocalStrategy localStrategy

if config.all.server.security.saml.use == true
if csUtilities.ssoLoginStrategy?
passport.use new SamlStrategy({
passReqToCallback: true
path: '/login/callback'
entryPoint: config.all.server.security.saml.entryPoint
issuer: config.all.server.security.saml.issuer
cert: config.all.server.security.saml.cert
}, csUtilities.ssoLoginStrategy)
else
console.error("NOT USING SSO configs! config.all.server.security.saml.use is set true but CustomerSpecificServerFunction 'ssoLoginStrategy' is not defined.")

loginRoutes = require './routes/loginRoutes'
sessionStore = new MemoryStore();
Expand Down
16 changes: 15 additions & 1 deletion conf/config.properties.example
Original file line number Diff line number Diff line change
Expand Up @@ -343,12 +343,26 @@ server.service.external.inventory.database.name=
server.service.external.inventory.database.username=
server.service.external.inventory.database.password=

## options for auth. strategy are: properties, database, ldap
## options for auth. strategy are: properties, database, ldap (sso is configured seperately)
server.security.authstrategy=database
#server.security.properties.path=/opt/seurat/SeuratServer/users.txt
#to make authstrategy config accessible to the client
client.security.authstrategy=${server.security.authstrategy}

## SSO Configuration
# Converts the main login page to SSO login
# Direct login using other auth strategies available at /login/Direct
# Examples configs using okta but can use any SAML login IDP

server.security.saml.use=false
server.security.saml.entryPoint=https://<organization>.okta.com/app/<appName>/<issuerID>/sso/saml
server.security.saml.issuer=http://www.okta.com/<issuerID>
# Embedded cert needs \\n in string to escape properly
server.security.saml.cert=-----BEGIN CERTIFICATE-----\\nABC\\nXYZ\\n-----END CERTIFICATE-----
server.security.saml.userNameAttribute=nameID
server.security.saml.logoutRedirectURL=https://<organization>.okta.com
server.security.saml.roles.sync=false

#Controls whether Roo syncs the AuthorRole table with authorities granted in LDAP
server.security.syncLdapAuthRoles=false
#to make syncLdapAuthRoles config accessible to the client
Expand Down
45 changes: 35 additions & 10 deletions modules/Login/src/server/routes/loginRoutes.coffee
Original file line number Diff line number Diff line change
@@ -1,15 +1,37 @@

config = require '../conf/compiled/conf.js'
csUtilities = require '../src/javascripts/ServerAPI/CustomerSpecificServerFunctions.js'

exports.setupAPIRoutes = (app) ->
app.get '/api/users/:username', exports.getUsers
app.get '/api/authors', exports.getAuthors

exports.setupRoutes = (app, passport) ->
app.get '/login', exports.loginPage
if config.all.server.security.saml.use == true
app.get '/login', ((req, res, next) ->
# If the redirect_url is set, pass it to the passport request as a relay state query
# This will then be returned as part of the body on callback
req.query.RelayState = req.query.redirect_url
next()
), passport.authenticate('saml',{failureRedirect: '/', failureFlash: true})
app.post '/login/callback', passport.authenticate('saml',
failureRedirect: '/'
failureFlash: true
), (req, res) =>
# If relay state value is set, it's because we set it to the redirect_url above
# So if it's set then redirect the user to the RelayState value
if req.body?.RelayState? && req.body.RelayState != ""
console.log "redirecting to #{req.body.RelayState}"
res.redirect(req.body.RelayState)
else
res.redirect('/');
else
app.get '/login', exports.loginPage
app.get '/login/direct', exports.loginPage
app.post '/login',
passport.authenticate('local', { failureRedirect: '/login', failureFlash: true }), exports.loginPost
app.get '/logout*', exports.logout
app.post '/api/userAuthentication', exports.authenticationService
app.get '/api/users/:username', exports.ensureAuthenticated, exports.getUsers
app.get '/passwordReset', exports.resetpage
app.post '/passwordReset',
exports.resetAuthenticationService,
Expand All @@ -21,9 +43,8 @@ exports.setupRoutes = (app, passport) ->
exports.changePost
app.post '/api/userChangeAuthentication', exports.changeAuthenticationService
app.get '/api/authors', exports.ensureAuthenticated, exports.getAuthors
app.get '/api/users/:username', exports.ensureAuthenticated, exports.getUsers

csUtilities = require '../src/javascripts/ServerAPI/CustomerSpecificServerFunctions.js'
config = require '../conf/compiled/conf.js'

exports.loginPage = (req, res) ->
user = null
Expand Down Expand Up @@ -69,11 +90,14 @@ exports.changePost = (req, res) ->

exports.logout = (req, res) ->
req.logout()
redirectMatch = req.originalUrl.match(/^\/logout\/(.*)\/?$/i)
if redirectMatch?
redirectMatch = redirectMatch[1]
else
redirectMatch = "/"
if config.all.server.security.saml.use == true && config.all.server.security.saml.logoutRedirectURL?
redirectMatch = config.all.server.security.saml.logoutRedirectURL
else
redirectMatch = req.originalUrl.match(/^\/logout\/(.*)\/?$/i)
if redirectMatch?
redirectMatch = redirectMatch[1]
else
redirectMatch = "/"
res.redirect redirectMatch

exports.ensureAuthenticated = (req, res, next) ->
Expand All @@ -82,7 +106,8 @@ exports.ensureAuthenticated = (req, res, next) ->
return next()
if req.session?
req.session.returnTo = req.url
res.redirect '/login'
redirectURL = req.protocol + '://' + req.get('host') + req.originalUrl
res.redirect '/login?redirect_url='+redirectURL

exports.ensureAuthenticatedAPI = (req, res, next) ->
console.log "checking for login for path: "+req.url
Expand Down
2 changes: 1 addition & 1 deletion modules/ServerAPI/src/client/AuthorEditor.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ class AuthorRoleController extends AbstractFormController
updateModel: =>
roleId = @authorRoleListController.getSelectedCode()
@model.set
id: roleId
id: parseInt(roleId)
@trigger 'amDirty'

clear: =>
Expand Down
128 changes: 127 additions & 1 deletion modules/ServerAPI/src/server/CustomerSpecificServerFunctions.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ ACAS_HOME="../../.."
serverUtilityFunctions = require "#{ACAS_HOME}/routes/ServerUtilityFunctions.js"
fs = require 'fs'
_ = require 'underscore'
util = require 'util'

exports.logUsage = (action, data, username) ->
# no ACAS logging service yet
Expand Down Expand Up @@ -148,7 +149,132 @@ exports.isUserAdmin = (user) ->
exports.findByUsername = (username, fn) ->
return exports.getUser username, fn

exports.loginStrategy = (req, username, password, done) ->
getSystemRolesFromSSOProfile = (profile) =>
roles = []
for group in profile.group
roleName = group.toUpperCase()
lsKind = 'ACAS'
if roleName.indexOf("CMPDREG") > -1
lsKind = 'CmpdReg'
roles.push
lsType: 'System'
lsKind: lsKind
roleName: "ROLE_#{roleName}"
return roles

exports.ssoLoginStrategy = (req, profile, callback) ->
config = require '../../../conf/compiled/conf.js'
serverUtilityFunctions = require "#{ACAS_HOME}/routes/ServerUtilityFunctions.js"
authorRoutes = require '../../../routes/AuthorRoutes.js'

userNameAttribute = config.all.server.security.saml.userNameAttribute
console.log("Incoming login #{JSON.stringify(profile)}")

# Check if author exists
[err, savedAuthor] = await serverUtilityFunctions.promisifyRequestResponseStatus(authorRoutes.getAuthorByUsernameInternal, [profile[userNameAttribute]])
if err?
console.error("Got error checking for existing author #{err} during sso login strategy")
callback err, null
return

# If the author doesn't exist then create one
# Check login ability here FIRST
if savedAuthor? && savedAuthor.length != 0
console.log "Found existing Author '#{savedAuthor.userName}'"
updateAuthor = false

if profile.email != savedAuthor.emailAddress
console.log "SSO email address '#{profile.email}' has changed from existing author email address '#{savedAuthor.emailAddress}'"
[err, unique] = await serverUtilityFunctions.promiseifyCatch(authorRoutes.checkEmailIsUnique, [profile.email])
if err
console.error(err)
callback err, null
return
if !unique == true
console.error("New email address is not unique to the sytem so it belongs to another username")
callback "Error, email address already belongs to another user", null
return
if profile.email != savedAuthor.emailAddress || profile.firstName != savedAuthor.firstName || profile.lastName != savedAuthor.lastName
updateAuthor = true
if updateAuthor == true
savedAuthor.firstName = profile.firstName
savedAuthor.lastName = profile.lastName
savedAuthor.emailAddress = profile.email
[err, updatedAuthor] = await serverUtilityFunctions.promisifyRequestResponseStatus(authorRoutes.updateAuthorInternal, [savedAuthor])
if err
err = "Got error trying to update author using SSO user profile"
console.log("#{err} Author: #{JSON.stringify(savedAuthor)}")
callback err, null
return
else
console.log "Successfully synced user profile"
savedAuthor = updatedAuthor
else
author =
firstName: profile.firstName
lastName: profile.lastName
emailAddress: profile.email
userName: profile[userNameAttribute]
version: 0
enabled: true
locked: false
password: null
recordedBy: 'acas'
recordedDate: new Date().getTime()
lsType: 'default'
lsKind: 'default'
[err, savedAuthor] = await serverUtilityFunctions.promiseifyCatch(authorRoutes.createNewAuthorInternal, [author])
if err?
console.error("Got error saving new author during sso login strategy Error #{JSON.stringify(err)}")
callback "Caught error trying to save author, please see logs", null
return
if !savedAuthor?
console.error("Got unknown error saving new author #{err} during sso login strategy Author json: #{JSON.stringify(author)}")
callback "Unknown error trying to save author, please see logs", null
return

if config.all.server.security.saml.roles.sync
console.log "Checking for roles to sync"
# Check the users ldap roles against the saved roles
ssoSystemRoles = getSystemRolesFromSSOProfile(profile)

# Get author system roles
savedAuthorSystemRoles = authorRoutes.getRolesByLsType(savedAuthor.authorRoles, "System")

# Diff system roles
diffSystemRolesWithSaved = util.promisify(authorRoutes.diffSystemRolesWithSaved)
[err, diffResult] = await exports.promiseCatch(diffSystemRolesWithSaved(savedAuthor.userName, ssoSystemRoles, savedAuthorSystemRoles, ["lsType", "lsKind", "roleName"]))
if err?
console.error("Caught error trying to diff current roles with new sso roles #{err}")
callback err, null
return

rolesToSync = false
if diffResult.rolesToAdd.length > 0
rolesToSync = true
console.log "Found #{diffResult.rolesToAdd.length} roles to add #{JSON.stringify(diffResult.rolesToAdd)}"
if diffResult.rolesToDelete.length > 0
rolesToSync = true
console.log "Found #{diffResult.rolesToDelete.length} roles to delete #{JSON.stringify(diffResult.rolesToDelete)}"

# Update author roles
if rolesToSync == true
console.log "Syncing roles"
syncRoles = util.promisify(authorRoutes.syncRoles)
[err, updatedAuthor] = await exports.promiseCatch(syncRoles(savedAuthor, diffResult.rolesToAdd, diffResult.rolesToDelete))
if err?
console.error("Caught error trying to sync roles for user #{err}")
callback err, null
return
else
console.log "No roles to sync"

# Get user doesn't format the user exactly like the updatedAuthor so we need to fetch the author one more time
# Easiest to just fetch the author fresh rather than transform
console.log "Passing callback to CustomerSpecificServerFunction getUser"
exports.getUser savedAuthor.userName, callback

exports.localLoginStrategy = (req, username, password, done) ->
exports.logUsage "login attempt", JSON.stringify(ip: req.ip, referer: req.headers['referer'], agent: req.headers['user-agent']), username
exports.authCheck username, password, (results) ->
if results.indexOf("login_error")>=0
Expand Down
Loading

0 comments on commit 8870bc1

Please sign in to comment.