mirror of
https://github.com/sasjs/server.git
synced 2026-01-10 07:50:05 +00:00
fix: using passport for azure and local authentication
This commit is contained in:
1278
package-lock.json
generated
1278
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -22,15 +22,25 @@
|
|||||||
"author": "Analytium Ltd",
|
"author": "Analytium Ltd",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/utils": "^2.23.3",
|
"@sasjs/utils": "^2.23.3",
|
||||||
|
"connect-ensure-login": "^0.1.1",
|
||||||
|
"ejs": "^3.1.6",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-session": "^1.17.1",
|
"express-session": "^1.17.1",
|
||||||
"msal-express-wrapper": "git+https://github.com/Azure-Samples/msal-express-wrapper.git#1.0.0-beta.3"
|
"jsonwebtoken": "^8.5.1",
|
||||||
|
"passport": "^0.4.0",
|
||||||
|
"passport-azure-ad-oauth2": "0.0.4",
|
||||||
|
"passport-local": "^1.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/connect-ensure-login": "^0.1.6",
|
||||||
|
"@types/ejs": "^3.1.0",
|
||||||
"@types/express": "^4.17.12",
|
"@types/express": "^4.17.12",
|
||||||
"@types/express-session": "^1.17.4",
|
"@types/express-session": "^1.17.4",
|
||||||
"@types/jest": "^26.0.24",
|
"@types/jest": "^26.0.24",
|
||||||
|
"@types/jsonwebtoken": "^8.5.5",
|
||||||
"@types/node": "^15.12.2",
|
"@types/node": "^15.12.2",
|
||||||
|
"@types/passport": "^1.0.7",
|
||||||
|
"@types/passport-local": "^1.0.34",
|
||||||
"@types/supertest": "^2.0.11",
|
"@types/supertest": "^2.0.11",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"jest": "^27.0.6",
|
"jest": "^27.0.6",
|
||||||
|
|||||||
29
src/app.ts
29
src/app.ts
@@ -1,26 +1,27 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
|
import { renderFile } from 'ejs'
|
||||||
|
|
||||||
import indexRouter from './routes'
|
import { Routes } from './routes'
|
||||||
import { AuthMechanism } from './types'
|
import { passportMiddleware } from './middleware'
|
||||||
import { getAzureSubApp } from './authMechanisms'
|
import { getAuthMechanisms } from './utils'
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
|
app.use(express.urlencoded({ extended: false }))
|
||||||
|
app.engine('html', renderFile)
|
||||||
|
app.set('view engine', 'html')
|
||||||
|
app.set('views', path.join(__dirname, './views'))
|
||||||
|
|
||||||
app.use(express.json({ limit: '50mb' }))
|
app.use(express.json({ limit: '50mb' }))
|
||||||
app.use(express.static(path.join(__dirname, '..', 'public')))
|
app.use(express.static(path.join(__dirname, '..', 'public')))
|
||||||
|
|
||||||
require('dotenv').config()
|
app.use(passportMiddleware())
|
||||||
|
|
||||||
const authMechanisms = process.env.AUTH?.split(' ') ?? [
|
|
||||||
AuthMechanism.NoSecurity
|
|
||||||
]
|
|
||||||
|
|
||||||
if (authMechanisms.includes(AuthMechanism.Azure)) {
|
|
||||||
app.use(getAzureSubApp())
|
|
||||||
} else {
|
|
||||||
app.get('/', indexRouter)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const authMechanisms = getAuthMechanisms()
|
||||||
|
app.get(Routes.Login, (req, res) => {
|
||||||
|
res.render('sasjslogon.html', { authMechanisms })
|
||||||
|
})
|
||||||
app.get('/error', (req, res) => res.redirect('/500.html'))
|
app.get('/error', (req, res) => res.redirect('/500.html'))
|
||||||
app.get('/unauthorized', (req, res) => res.redirect('/401.html'))
|
app.get('/unauthorized', (req, res) => res.redirect('/401.html'))
|
||||||
app.get('*', (req, res) => res.status(404).redirect('/404.html'))
|
app.get('*', (req, res) => res.status(404).redirect('/404.html'))
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
import express from 'express'
|
|
||||||
import session from 'express-session'
|
|
||||||
import indexRouter from '../routes'
|
|
||||||
|
|
||||||
export const getAzureSubApp = () => {
|
|
||||||
console.log('Using Azure Authentication')
|
|
||||||
const app = express()
|
|
||||||
|
|
||||||
const msalWrapper = require('msal-express-wrapper')
|
|
||||||
const appSettings = {
|
|
||||||
appCredentials: {
|
|
||||||
clientId: process.env.CLIENTID ?? ' ',
|
|
||||||
tenantId: process.env.TENANTID ?? ' ',
|
|
||||||
clientSecret: process.env.CLIENTSECRET ?? ' '
|
|
||||||
},
|
|
||||||
authRoutes: {
|
|
||||||
redirect: '/redirect',
|
|
||||||
error: '/error', // the wrapper will redirect to this route in case of any error.
|
|
||||||
unauthorized: '/unauthorized' // the wrapper will redirect to this route in case of unauthorized access attempt.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Using express-session middleware. Be sure to familiarize yourself with available options
|
|
||||||
* and set them as desired. Visit: https://www.npmjs.com/package/express-session
|
|
||||||
*/
|
|
||||||
const sessionConfig = {
|
|
||||||
secret: appSettings.appCredentials.clientSecret,
|
|
||||||
resave: false,
|
|
||||||
saveUninitialized: false,
|
|
||||||
cookie: {
|
|
||||||
secure: false // set this to true on production
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (app.get('env') === 'production') {
|
|
||||||
app.set('trust proxy', 1) // trust first proxy
|
|
||||||
sessionConfig.cookie.secure = true // serve secure cookies
|
|
||||||
}
|
|
||||||
|
|
||||||
app.use(session(sessionConfig))
|
|
||||||
|
|
||||||
// instantiate the wrapper
|
|
||||||
const authProvider = new msalWrapper.AuthProvider(appSettings)
|
|
||||||
|
|
||||||
// initialize the wrapper
|
|
||||||
app.use(authProvider.initialize())
|
|
||||||
|
|
||||||
// authentication routes
|
|
||||||
app.get('/signin-with-azure', authProvider.signIn({ successRedirect: '/' }))
|
|
||||||
app.get('/signout-with-azure', authProvider.signOut({ successRedirect: '/' }))
|
|
||||||
|
|
||||||
// secure routes
|
|
||||||
app.get('/', authProvider.isAuthenticated(), indexRouter)
|
|
||||||
|
|
||||||
return app
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './azure'
|
|
||||||
146
src/middleware.ts
Normal file
146
src/middleware.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import jwt from 'jsonwebtoken'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
import express from 'express'
|
||||||
|
import session from 'express-session'
|
||||||
|
import passport from 'passport'
|
||||||
|
import { Strategy } from 'passport-local'
|
||||||
|
const AzureAdOAuth2Strategy = require('passport-azure-ad-oauth2')
|
||||||
|
import { ensureLoggedIn } from 'connect-ensure-login'
|
||||||
|
import { AuthMechanism } from './types'
|
||||||
|
import { getAuthMechanisms } from './utils'
|
||||||
|
import indexRouter, { Routes } from './routes'
|
||||||
|
|
||||||
|
export const passportMiddleware = (): express.Express => {
|
||||||
|
dotenv.config()
|
||||||
|
const authMechanisms = getAuthMechanisms()
|
||||||
|
|
||||||
|
const middleware = express()
|
||||||
|
|
||||||
|
const sessionConfig = {
|
||||||
|
secret: 'keyboard cat',
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
cookie: {
|
||||||
|
secure: false // set this to true on production
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (middleware.get('env') === 'production') {
|
||||||
|
middleware.set('trust proxy', 1) // trust first proxy
|
||||||
|
sessionConfig.cookie.secure = true // serve secure cookies
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware.use(session(sessionConfig))
|
||||||
|
|
||||||
|
setupPassportStrategies(authMechanisms)
|
||||||
|
|
||||||
|
middleware.use(passport.initialize())
|
||||||
|
middleware.use(passport.authenticate('session'))
|
||||||
|
|
||||||
|
setupPassportRoutes(middleware, authMechanisms)
|
||||||
|
|
||||||
|
middleware.get('/signout', (req, res, next) => {
|
||||||
|
req.logout()
|
||||||
|
res.redirect('/')
|
||||||
|
})
|
||||||
|
|
||||||
|
if (
|
||||||
|
authMechanisms.length === 1 &&
|
||||||
|
authMechanisms[0] === AuthMechanism.NoSecurity
|
||||||
|
) {
|
||||||
|
console.log('Using No Security')
|
||||||
|
middleware.get('/', indexRouter)
|
||||||
|
} else {
|
||||||
|
middleware.get(
|
||||||
|
'/',
|
||||||
|
ensureLoggedIn({ redirectTo: '/SASjsLogon' }),
|
||||||
|
indexRouter
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return middleware
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupPassportStrategies = (authMechanisms: string[]) => {
|
||||||
|
if (authMechanisms.includes(AuthMechanism.Local)) {
|
||||||
|
console.log('Using Local Authentication')
|
||||||
|
passport.use(
|
||||||
|
new Strategy((username: string, password: string, cb: Function) => {
|
||||||
|
console.log('username', username)
|
||||||
|
if (username !== 'SaadJutt')
|
||||||
|
return cb(null, false, { message: 'Incorrect Username' })
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
id: 'SOMEID',
|
||||||
|
username: username,
|
||||||
|
displayName: 'displayName'
|
||||||
|
}
|
||||||
|
return cb(null, user)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (authMechanisms.includes(AuthMechanism.Azure)) {
|
||||||
|
console.log('Using Azure Authentication')
|
||||||
|
passport.use(
|
||||||
|
new AzureAdOAuth2Strategy(
|
||||||
|
{
|
||||||
|
clientID: process.env.CLIENTID as string,
|
||||||
|
clientSecret: process.env.CLIENTSECRET as string,
|
||||||
|
callbackURL: '/redirect'
|
||||||
|
},
|
||||||
|
function (
|
||||||
|
accessToken: any,
|
||||||
|
refresh_token: any,
|
||||||
|
params: any,
|
||||||
|
profile: any,
|
||||||
|
done: any
|
||||||
|
) {
|
||||||
|
// currently we can't find a way to exchange access token by user info (see userProfile implementation), so
|
||||||
|
// you will need a jwt-package like https://github.com/auth0/node-jsonwebtoken to decode id_token and get waad profile
|
||||||
|
var waadProfile = profile || jwt.decode(params.id_token)
|
||||||
|
|
||||||
|
done(null, { id: waadProfile.upn })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
passport.serializeUser((user: any, cb) => {
|
||||||
|
process.nextTick(() => {
|
||||||
|
cb(null, { id: user.id, username: user.username })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
passport.deserializeUser((user: any, cb) => {
|
||||||
|
process.nextTick(() => {
|
||||||
|
return cb(null, user)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupPassportRoutes = (
|
||||||
|
app: express.Express,
|
||||||
|
authMechanisms: string[]
|
||||||
|
) => {
|
||||||
|
if (authMechanisms.includes(AuthMechanism.Azure)) {
|
||||||
|
app.get(Routes.AzureSignIn, passport.authenticate(['azure_ad_oauth2']))
|
||||||
|
app.get(
|
||||||
|
Routes.AzureSignInRedirect,
|
||||||
|
passport.authenticate('azure_ad_oauth2', {
|
||||||
|
successRedirect: '/',
|
||||||
|
failureRedirect: Routes.Login,
|
||||||
|
failureMessage: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (authMechanisms.includes(AuthMechanism.Local)) {
|
||||||
|
app.post(
|
||||||
|
Routes.LocalSignIn,
|
||||||
|
passport.authenticate(['local'], {
|
||||||
|
successRedirect: '/',
|
||||||
|
failureRedirect: Routes.Login,
|
||||||
|
failureMessage: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -79,3 +79,4 @@ router.get('/SASjsExecutor/do', async (req, res) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
export * from './routes'
|
||||||
|
|||||||
6
src/routes/routes.ts
Normal file
6
src/routes/routes.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export const Routes = {
|
||||||
|
Login: '/SASjsLogon',
|
||||||
|
AzureSignIn: '/signin-with-azure',
|
||||||
|
AzureSignInRedirect: '/redirect',
|
||||||
|
LocalSignIn: '/signin-with-local'
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export enum AuthMechanism {
|
export enum AuthMechanism {
|
||||||
Azure = 'azure',
|
Azure = 'azure',
|
||||||
|
Local = 'local',
|
||||||
NoSecurity = 'nosecurity'
|
NoSecurity = 'nosecurity'
|
||||||
}
|
}
|
||||||
|
|||||||
8
src/utils/getAuthMechanisms.ts
Normal file
8
src/utils/getAuthMechanisms.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { AuthMechanism } from '../types'
|
||||||
|
|
||||||
|
export const getAuthMechanisms = () => {
|
||||||
|
const authsMechanisms =
|
||||||
|
process.env.AUTH?.split(' ').filter((auth) => !!auth) ?? []
|
||||||
|
|
||||||
|
return authsMechanisms.length ? authsMechanisms : [AuthMechanism.NoSecurity]
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
export * from './file'
|
export * from './file'
|
||||||
|
export * from './getAuthMechanisms'
|
||||||
|
|||||||
50
src/views/sasjslogon.html
Normal file
50
src/views/sasjslogon.html
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" status="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
|
<title>SASjs Logon</title>
|
||||||
|
<style>
|
||||||
|
#azure,
|
||||||
|
#local {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="row" style="margin: auto">
|
||||||
|
<div id="card-div" class="col-md-3">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h1>Welcome!</h1>
|
||||||
|
<div id="azure">
|
||||||
|
<h2>Sign-in with Azure</h2>
|
||||||
|
<a href="/signin-with-azure" role="button">Sign in Using Azure</a>
|
||||||
|
</div>
|
||||||
|
<div id="local">
|
||||||
|
<h2>Sign-in with Local</h2>
|
||||||
|
|
||||||
|
<form action="/signin-with-local" method="post">
|
||||||
|
<input type="text" name="username" placeholder="Username" />
|
||||||
|
<input type="password" name="password" placeholder="Password" />
|
||||||
|
<button type="submit" class="contrast">Sign in</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const authMechanisms = '<%= authMechanisms %>'.split(',')
|
||||||
|
if (authMechanisms.includes('azure')) {
|
||||||
|
const azureDiv = document.getElementById('azure')
|
||||||
|
azureDiv.style.display = 'block'
|
||||||
|
}
|
||||||
|
if (authMechanisms.includes('local')) {
|
||||||
|
const localDiv = document.getElementById('local')
|
||||||
|
localDiv.style.display = 'block'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user