fix(server): auto-verify on invited server registration (#2824)

This commit is contained in:
Kristaps Fabians Geikins
2024-09-02 10:40:53 +03:00
committed by GitHub
parent 7bc9af209e
commit 23d5a7b559
8 changed files with 79 additions and 115 deletions
@@ -31,6 +31,7 @@ import {
} from '@/modules/shared/helpers/envHelper'
import type { Request } from 'express'
import { ensureError, Optional } from '@speckle/shared'
import { ServerInviteRecord } from '@/modules/serverinvites/domain/types'
const azureAdStrategyBuilder: AuthStrategyBuilder = async (
app,
@@ -128,31 +129,6 @@ const azureAdStrategyBuilder: AuthStrategyBuilder = async (
return next()
}
// if the server is not invite only, go ahead and log the user in.
if (!serverInfo.inviteOnly) {
const myUser = await findOrCreateUser({
user
})
// ID is used later for verifying access token
req.user = {
...profile,
id: myUser.id,
email: myUser.email,
isNewUser: myUser.isNewUser
}
// process invites
if (myUser.isNewUser) {
await finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
})(user.email, myUser.id)
}
return next()
}
// if the server is invite only and we have no invite id, throw.
if (serverInfo.inviteOnly && !req.session.token) {
throw new UserInputError(
@@ -160,18 +136,22 @@ const azureAdStrategyBuilder: AuthStrategyBuilder = async (
)
}
// validate the invite
const validInvite = await validateServerInviteFactory({
findServerInvite: findServerInviteFactory({ db })
})(user.email, req.session.token)
// 2. if you have an invite it must be valid, both for invite only and public servers
let invite: Optional<ServerInviteRecord> = undefined
if (req.session.token) {
invite = await validateServerInviteFactory({
findServerInvite: findServerInviteFactory({ db })
})(user.email, req.session.token)
}
// create the user
const myUser = await findOrCreateUser({
user: {
...user,
role: validInvite
? getResourceTypeRole(validInvite.resource, ServerInviteResourceType)
: undefined
role: invite
? getResourceTypeRole(invite.resource, ServerInviteResourceType)
: undefined,
verified: !!invite
}
})
@@ -181,7 +161,7 @@ const azureAdStrategyBuilder: AuthStrategyBuilder = async (
id: myUser.id,
email: myUser.email,
isNewUser: myUser.isNewUser,
isInvite: !!validInvite
isInvite: !!invite
}
req.log = req.log.child({ userId: myUser.id })
@@ -193,7 +173,7 @@ const azureAdStrategyBuilder: AuthStrategyBuilder = async (
})(user.email, myUser.id)
// Resolve redirect path
req.authRedirectPath = resolveAuthRedirectPathFactory()(validInvite)
req.authRedirectPath = resolveAuthRedirectPathFactory()(invite)
// return to the auth flow
return next()
@@ -31,7 +31,8 @@ import {
} from '@/modules/shared/helpers/envHelper'
import type { Request } from 'express'
import { get } from 'lodash'
import { ensureError } from '@speckle/shared'
import { ensureError, Optional } from '@speckle/shared'
import { ServerInviteRecord } from '@/modules/serverinvites/domain/types'
const githubStrategyBuilder: AuthStrategyBuilder = async (
app,
@@ -102,21 +103,6 @@ const githubStrategyBuilder: AuthStrategyBuilder = async (
return done(null, myUser)
}
// if the server is not invite only, go ahead and log the user in.
if (!serverInfo.inviteOnly) {
const myUser = await findOrCreateUser({ user })
// process invites
if (myUser.isNewUser) {
await finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
})(user.email, myUser.id)
}
return done(null, myUser)
}
// if the server is invite only and we have no invite id, throw.
if (serverInfo.inviteOnly && !req.session.token) {
throw new UserInputError(
@@ -124,18 +110,22 @@ const githubStrategyBuilder: AuthStrategyBuilder = async (
)
}
// validate the invite
const validInvite = await validateServerInviteFactory({
findServerInvite: findServerInviteFactory({ db })
})(user.email, req.session.token)
// validate the invite, if any
let invite: Optional<ServerInviteRecord> = undefined
if (req.session.token) {
invite = await validateServerInviteFactory({
findServerInvite: findServerInviteFactory({ db })
})(user.email, req.session.token)
}
// create the user
const myUser = await findOrCreateUser({
user: {
...user,
role: validInvite
? getResourceTypeRole(validInvite.resource, ServerInviteResourceType)
: undefined
role: invite
? getResourceTypeRole(invite.resource, ServerInviteResourceType)
: undefined,
verified: !!invite
}
})
@@ -146,12 +136,12 @@ const githubStrategyBuilder: AuthStrategyBuilder = async (
})(user.email, myUser.id)
// Resolve redirect path
req.authRedirectPath = resolveAuthRedirectPathFactory()(validInvite)
req.authRedirectPath = resolveAuthRedirectPathFactory()(invite)
// return to the auth flow
return done(null, {
...myUser,
isInvite: !!validInvite
isInvite: !!invite
})
} catch (err) {
const e = ensureError(
@@ -26,7 +26,8 @@ import {
getGoogleClientId,
getGoogleClientSecret
} from '@/modules/shared/helpers/envHelper'
import { ensureError } from '@speckle/shared'
import { ensureError, Optional } from '@speckle/shared'
import { ServerInviteRecord } from '@/modules/serverinvites/domain/types'
const googleStrategyBuilder: AuthStrategyBuilder = async (
app,
@@ -85,21 +86,6 @@ const googleStrategyBuilder: AuthStrategyBuilder = async (
return done(null, myUser)
}
// if the server is not invite only, go ahead and log the user in.
if (!serverInfo.inviteOnly) {
const myUser = await findOrCreateUser({ user })
// process invites
if (myUser.isNewUser) {
await finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
})(user.email, myUser.id)
}
return done(null, myUser)
}
// if the server is invite only and we have no invite id, throw.
if (serverInfo.inviteOnly && !req.session.token) {
throw new UserInputError(
@@ -107,18 +93,22 @@ const googleStrategyBuilder: AuthStrategyBuilder = async (
)
}
// validate the invite
const validInvite = await validateServerInviteFactory({
findServerInvite: findServerInviteFactory({ db })
})(user.email, req.session.token)
// validate the invite, if any
let invite: Optional<ServerInviteRecord> = undefined
if (req.session.token) {
invite = await validateServerInviteFactory({
findServerInvite: findServerInviteFactory({ db })
})(user.email, req.session.token)
}
// create the user
const myUser = await findOrCreateUser({
user: {
...user,
role: validInvite
? getResourceTypeRole(validInvite.resource, ServerInviteResourceType)
: undefined
role: invite
? getResourceTypeRole(invite.resource, ServerInviteResourceType)
: undefined,
verified: !!invite
}
})
@@ -129,12 +119,12 @@ const googleStrategyBuilder: AuthStrategyBuilder = async (
})(user.email, myUser.id)
// Resolve redirect path
req.authRedirectPath = resolveAuthRedirectPathFactory()(validInvite)
req.authRedirectPath = resolveAuthRedirectPathFactory()(invite)
// return to the auth flow
return done(null, {
...myUser,
isInvite: !!validInvite
isInvite: !!invite
})
} catch (err) {
const e = ensureError(
@@ -112,7 +112,8 @@ const localStrategyBuilder: AuthStrategyBuilder = async (
...user,
role: invite
? getResourceTypeRole(invite.resource, ServerInviteResourceType)
: undefined
: undefined,
verified: !!invite
})
req.user = {
id: userId,
+14 -25
View File
@@ -29,6 +29,7 @@ import { getResourceTypeRole } from '@/modules/serverinvites/helpers/core'
import { AuthStrategyBuilder } from '@/modules/auth/helpers/types'
import { get } from 'lodash'
import { Optional } from '@speckle/shared'
import { ServerInviteRecord } from '@/modules/serverinvites/domain/types'
const oidcStrategyBuilder: AuthStrategyBuilder = async (
app,
@@ -98,39 +99,27 @@ const oidcStrategyBuilder: AuthStrategyBuilder = async (
return done(null, myUser)
}
// if the server is not invite only, go ahead and log the user in.
if (!serverInfo.inviteOnly) {
const myUser = await findOrCreateUser({
user
})
// process invites
if (myUser.isNewUser) {
await finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
})(user.email, myUser.id)
}
return done(null, myUser)
}
// if the server is invite only and we have no invite id, throw.
if (serverInfo.inviteOnly && !token) {
throw new Error('This server is invite only. Please provide an invite id.')
}
// validate the invite
const validInvite = await validateServerInviteFactory({
findServerInvite: findServerInviteFactory({ db })
})(user.email, token)
// validate the invite, if any
let invite: Optional<ServerInviteRecord> = undefined
if (token) {
invite = await validateServerInviteFactory({
findServerInvite: findServerInviteFactory({ db })
})(user.email, token)
}
// create the user
const myUser = await findOrCreateUser({
user: {
...user,
role: validInvite
? getResourceTypeRole(validInvite.resource, ServerInviteResourceType)
: undefined
role: invite
? getResourceTypeRole(invite.resource, ServerInviteResourceType)
: undefined,
verified: !!invite
}
})
@@ -140,12 +129,12 @@ const oidcStrategyBuilder: AuthStrategyBuilder = async (
})(user.email, myUser.id)
// Resolve redirect path
req.authRedirectPath = resolveAuthRedirectPathFactory()(validInvite)
req.authRedirectPath = resolveAuthRedirectPathFactory()(invite)
// return to the auth flow
return done(null, {
...myUser,
isInvite: !!validInvite
isInvite: !!invite
})
} catch (err) {
logger.error(err)
@@ -137,7 +137,7 @@ export const localAuthRestApi = (params: { express: Express }) => {
const authCheck = async (params: { token: string }) => {
const query =
'query LocalAuthRestApiAuthCheck { activeUser { id email name role } }'
'query LocalAuthRestApiAuthCheck { activeUser { id email name role emails { id email verified } } }'
const res = await request(express)
.post('/graphql')
.set('Authorization', `Bearer ${params.token}`)
@@ -148,7 +148,15 @@ export const localAuthRestApi = (params: { express: Express }) => {
}
const body = res.body as {
data: { activeUser?: { id: string; name: string; email: string; role: string } }
data: {
activeUser?: {
id: string
name: string
email: string
role: string
emails: Array<{ id: string; email: string; verified: boolean }>
}
}
errors?: { message: string; extensions: Record<string, string> }[]
}
if (!body.data.activeUser) {
@@ -88,7 +88,10 @@ describe('Server registration', () => {
const params = generateRegistrationParams()
params.challenge = challenge
await restApi.register(params)
const user = await restApi.register(params)
// email remains unverified
expect(user.emails.every((e) => !e.verified)).to.be.true
})
it('fails without challenge', async () => {
@@ -202,7 +205,9 @@ describe('Server registration', () => {
itEach(
[{ stream: true }, { stream: false }],
({ stream }) =>
`works with valid ${stream ? 'stream' : 'server'} invite token`,
`works with valid ${
stream ? 'stream' : 'server'
} invite token and auto-verifies email`,
async ({ stream }) => {
const challenge = 'bababooey'
const params = generateRegistrationParams()
@@ -221,6 +226,7 @@ describe('Server registration', () => {
const newUser = await restApi.register(params)
expect(newUser.role).to.equal(Roles.Server.Admin)
expect(newUser.emails.every((e) => e.verified)).to.be.true
}
)
})
@@ -156,7 +156,7 @@ module.exports = {
},
/**
* @param {{user: {email: string, name?: string, role?: import('@speckle/shared').ServerRoles}, bio?: string}} param0
* @param {{user: {email: string, name?: string, role?: import('@speckle/shared').ServerRoles, bio?: string, verified?: boolean}}} param0
* @returns {Promise<{
* id: string,
* email: string,