Skip to content

Commit

Permalink
chore(auth): move common middleware to factory functions (#3178)
Browse files Browse the repository at this point in the history
  • Loading branch information
cdriesler authored Oct 2, 2024
1 parent ebef771 commit 5110648
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 150 deletions.
140 changes: 140 additions & 0 deletions packages/server/modules/auth/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import type { RequestHandler } from 'express'
import ExpressSession from 'express-session'
import ConnectRedis from 'connect-redis'
import { createRedisClient } from '@/modules/shared/redis/redis'
import {
isSSLServer,
getRedisUrl,
getFrontendOrigin,
enableMixpanel,
getMailchimpStatus,
getMailchimpOnboardingIds,
getMailchimpNewsletterIds
} from '@/modules/shared/helpers/envHelper'
import { getSessionSecret } from '@/modules/shared/helpers/envHelper'
import { isString, noop } from 'lodash'
import { CreateAuthorizationCode } from '@/modules/auth/domain/operations'
import { getUserById } from '@/modules/core/services/users'
import { mixpanel } from '@/modules/shared/utils/mixpanel'
import {
addToMailchimpAudience,
triggerMailchimpCustomerJourney
} from '@/modules/auth/services/mailchimp'
import { authLogger, logger } from '@/logging/logging'
import { ensureError } from '@speckle/shared'

export const sessionMiddlewareFactory = (): RequestHandler => {
const RedisStore = ConnectRedis(ExpressSession)
const redisClient = createRedisClient(getRedisUrl(), {})
const sessionMiddleware = ExpressSession({
store: new RedisStore({ client: redisClient }),
secret: getSessionSecret(),
saveUninitialized: false,
resave: false,
cookie: {
maxAge: 1000 * 60 * 3, // 3 minutes
secure: isSSLServer()
}
})

return sessionMiddleware
}

/**
* Move incoming auth query params to session, for easier access
*/
export const moveAuthParamsToSessionMiddlewareFactory =
(): RequestHandler => (req, res, next) => {
if (!req.query.challenge)
return res.status(400).send('Invalid request: no challenge detected.')

req.session.challenge =
req.query.challenge && isString(req.query.challenge)
? req.query.challenge
: undefined

const token = req.query.token || req.query.inviteId
if (token && isString(token)) {
req.session.token = token
}

const newsletterConsent = req.query.newsletter || null
if (newsletterConsent) {
req.session.newsletterConsent = true
}

next()
}

/**
* Finalizes authentication for the main frontend application.
*/
export const finalizeAuthMiddlewareFactory =
(deps: {
createAuthorizationCode: CreateAuthorizationCode
getUserById: typeof getUserById
}): RequestHandler =>
async (req, res) => {
try {
if (!req.user) {
throw new Error('Cannot finalize auth - No user attached to session')
}

const ac = await deps.createAuthorizationCode({
appId: 'spklwebapp',
userId: req.user.id,
challenge: req.session.challenge!
})

let newsletterConsent = false
if (req.session.newsletterConsent) newsletterConsent = true // NOTE: it's only set if it's true

if (req.session) req.session.destroy(noop)

// Resolve redirect URL
const urlObj = new URL(req.authRedirectPath || '/', getFrontendOrigin())
urlObj.searchParams.set('access_code', ac)

if (req.user.isNewUser) {
urlObj.searchParams.set('register', 'true')

// Send event to MP
const userEmail = req.user.email
const isInvite = !!req.user.isInvite
if (userEmail && enableMixpanel()) {
await mixpanel({ userEmail, req }).track('Sign Up', {
isInvite
})
}

if (getMailchimpStatus()) {
try {
const user = await deps.getUserById({ userId: req.user.id })
if (!user)
throw new Error(
'Could not register user for mailchimp lists - no db user record found.'
)
const onboardingIds = getMailchimpOnboardingIds()
await triggerMailchimpCustomerJourney(user, onboardingIds)

if (newsletterConsent) {
const { listId } = getMailchimpNewsletterIds()
await addToMailchimpAudience(user, listId)
}
} catch (error) {
logger.warn(error, 'Failed to sign up user to mailchimp lists')
}
}
}

const redirectUrl = urlObj.toString()

return res.redirect(redirectUrl)
} catch (err) {
authLogger.error(err, 'Could not finalize auth')
if (req.session) req.session.destroy(noop)
return res.status(401).send({
err: ensureError(err, 'Unexpected issue arose while finalizing auth').message
})
}
}
135 changes: 9 additions & 126 deletions packages/server/modules/auth/strategies.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,17 @@
import ExpressSession from 'express-session'
import ConnectRedis from 'connect-redis'
import passport from 'passport'
import {
getFrontendOrigin,
getMailchimpStatus,
getMailchimpNewsletterIds,
getMailchimpOnboardingIds,
getSessionSecret,
enableMixpanel
} from '@/modules/shared/helpers/envHelper'
import { isSSLServer, getRedisUrl } from '@/modules/shared/helpers/envHelper'
import { authLogger, logger } from '@/logging/logging'
import { createRedisClient } from '@/modules/shared/redis/redis'
import { mixpanel } from '@/modules/shared/utils/mixpanel'
import {
addToMailchimpAudience,
triggerMailchimpCustomerJourney
} from '@/modules/auth/services/mailchimp'
import { getUserById } from '@/modules/core/services/users'
import type { Express, RequestHandler } from 'express'
import type { Express } from 'express'
import {
AuthStrategyBuilder,
AuthStrategyMetadata,
AuthStrategyPassportUser
} from '@/modules/auth/helpers/types'
import { isString, noop } from 'lodash'
import { ensureError } from '@speckle/shared'
import { CreateAuthorizationCode } from '@/modules/auth/domain/operations'
import {
finalizeAuthMiddlewareFactory,
moveAuthParamsToSessionMiddlewareFactory,
sessionMiddlewareFactory
} from '@/modules/auth/middleware'

const setupStrategiesFactory =
(deps: {
Expand All @@ -46,111 +31,9 @@ const setupStrategiesFactory =

app.use(passport.initialize())

const RedisStore = ConnectRedis(ExpressSession)
const redisClient = createRedisClient(getRedisUrl(), {})
const sessionMiddleware = ExpressSession({
store: new RedisStore({ client: redisClient }),
secret: getSessionSecret(),
saveUninitialized: false,
resave: false,
cookie: {
maxAge: 1000 * 60 * 3, // 3 minutes
secure: isSSLServer()
}
})

/**
* Move incoming auth query params to session, for easier access
*/
const moveAuthParamsToSessionMiddleware: RequestHandler = (req, res, next) => {
if (!req.query.challenge)
return res.status(400).send('Invalid request: no challenge detected.')

req.session.challenge =
req.query.challenge && isString(req.query.challenge)
? req.query.challenge
: undefined

const token = req.query.token || req.query.inviteId
if (token && isString(token)) {
req.session.token = token
}

const newsletterConsent = req.query.newsletter || null
if (newsletterConsent) {
req.session.newsletterConsent = true
}

next()
}

/**
* Finalizes authentication for the main frontend application.
*/
const finalizeAuthMiddleware: RequestHandler = async (req, res) => {
try {
if (!req.user) {
throw new Error('Cannot finalize auth - No user attached to session')
}

const ac = await deps.createAuthorizationCode({
appId: 'spklwebapp',
userId: req.user.id,
challenge: req.session.challenge!
})

let newsletterConsent = false
if (req.session.newsletterConsent) newsletterConsent = true // NOTE: it's only set if it's true

if (req.session) req.session.destroy(noop)

// Resolve redirect URL
const urlObj = new URL(req.authRedirectPath || '/', getFrontendOrigin())
urlObj.searchParams.set('access_code', ac)

if (req.user.isNewUser) {
urlObj.searchParams.set('register', 'true')

// Send event to MP
const userEmail = req.user.email
const isInvite = !!req.user.isInvite
if (userEmail && enableMixpanel()) {
await mixpanel({ userEmail, req }).track('Sign Up', {
isInvite
})
}

if (getMailchimpStatus()) {
try {
const user = await deps.getUserById({ userId: req.user.id })
if (!user)
throw new Error(
'Could not register user for mailchimp lists - no db user record found.'
)
const onboardingIds = getMailchimpOnboardingIds()
await triggerMailchimpCustomerJourney(user, onboardingIds)

if (newsletterConsent) {
const { listId } = getMailchimpNewsletterIds()
await addToMailchimpAudience(user, listId)
}
} catch (error) {
logger.warn(error, 'Failed to sign up user to mailchimp lists')
}
}
}

const redirectUrl = urlObj.toString()

return res.redirect(redirectUrl)
} catch (err) {
authLogger.error(err, 'Could not finalize auth')
if (req.session) req.session.destroy(noop)
return res.status(401).send({
err: ensureError(err, 'Unexpected issue arose while finalizing auth').message
})
}
}
const sessionMiddleware = sessionMiddlewareFactory()
const moveAuthParamsToSessionMiddleware = moveAuthParamsToSessionMiddlewareFactory()
const finalizeAuthMiddleware = finalizeAuthMiddlewareFactory({ ...deps })

/*
* Strategies initialisation & listing
Expand Down
37 changes: 13 additions & 24 deletions packages/server/modules/workspaces/rest/sso.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,7 @@ import {
getProviderAuthorizationUrl,
initializeIssuerAndClient
} from '@/modules/workspaces/clients/oidcProvider'
import {
getFrontendOrigin,
getRedisUrl,
getServerOrigin,
getSessionSecret,
isSSLServer
} from '@/modules/shared/helpers/envHelper'
import { getFrontendOrigin, getServerOrigin } from '@/modules/shared/helpers/envHelper'
import {
storeOIDCProviderValidationRequestFactory,
getOIDCProviderFactory,
Expand All @@ -30,32 +24,26 @@ import { buildDecryptor, buildEncryptor } from '@/modules/shared/utils/libsodium
import { getEncryptionKeyPair } from '@/modules/automate/services/encryption'
import { getGenericRedis } from '@/modules/core'
import { generators } from 'openid-client'
import { createRedisClient } from '@/modules/shared/redis/redis'
// temp imports
import ConnectRedis from 'connect-redis'
import ExpressSession from 'express-session'
import { noop } from 'lodash'
import { OIDCProvider, oidcProvider } from '@/modules/workspaces/domain/sso'
import { getWorkspaceBySlugFactory } from '@/modules/workspaces/repositories/workspaces'
import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace'
import { authorizeResolver } from '@/modules/shared'
import { Roles } from '@speckle/shared'
import { createUserEmailFactory } from '@/modules/core/repositories/userEmails'
import {
finalizeAuthMiddlewareFactory,
sessionMiddlewareFactory
} from '@/modules/auth/middleware'
import { createAuthorizationCodeFactory } from '@/modules/auth/repositories/apps'
import { getUserById } from '@/modules/core/services/users'

const router = Router()

// todo, this should be using the app wide session middleware
const RedisStore = ConnectRedis(ExpressSession)
const redisClient = createRedisClient(getRedisUrl(), {})
const sessionMiddleware = ExpressSession({
store: new RedisStore({ client: redisClient }),
secret: getSessionSecret(),
saveUninitialized: false,
resave: false,
cookie: {
maxAge: 1000 * 60 * 3, // 3 minutes
secure: isSSLServer()
}
const sessionMiddleware = sessionMiddlewareFactory()
const finalizeAuthMiddleware = finalizeAuthMiddlewareFactory({
createAuthorizationCode: createAuthorizationCodeFactory({ db }),
getUserById
})

const buildAuthRedirectUrl = (workspaceSlug: string): URL =>
Expand Down Expand Up @@ -249,7 +237,8 @@ router.get(
} else {
// this must be using the generic OIDC login flow somehow
}
}
},
finalizeAuthMiddleware
)

export default router

0 comments on commit 5110648

Please sign in to comment.