Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

754 sso saml support #755

Merged
merged 10 commits into from
Jun 3, 2021
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