Implement login hooks (#2211)

This commit is contained in:
Mihovil Ilakovac 2024-08-08 18:28:00 +02:00 committed by GitHub
parent e9a55629af
commit b9c02855cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 711 additions and 94 deletions

View File

@ -1,5 +1,5 @@
import type { Request as ExpressRequest } from 'express'
import type { ProviderId, createUser } from '../../auth/utils.js'
import { type ProviderId, createUser, findAuthWithUserBy } from '../../auth/utils.js'
import { prisma } from '../index.js'
import { Expand } from '../../universal/types.js'
@ -21,6 +21,16 @@ export type OnBeforeOAuthRedirectHook = (
params: Expand<OnBeforeOAuthRedirectHookParams>,
) => { url: URL } | Promise<{ url: URL }>
// PUBLIC API
export type OnBeforeLoginHook = (
params: Expand<OnBeforeLoginHookParams>,
) => void | Promise<void>
// PUBLIC API
export type OnAfterLoginHook = (
params: Expand<OnAfterLoginHookParams>,
) => void | Promise<void>
// PRIVATE API (used in the SDK and the server)
export type InternalAuthHookParams = {
/**
@ -85,3 +95,39 @@ type OnBeforeOAuthRedirectHookParams = {
*/
req: ExpressRequest
} & InternalAuthHookParams
type OnBeforeLoginHookParams = {
/**
* Provider ID object that contains the provider name and the provide user ID.
*/
providerId: ProviderId
/**
* Request object that can be used to access the incoming request.
*/
req: ExpressRequest
} & InternalAuthHookParams
type OnAfterLoginHookParams = {
/**
* Provider ID object that contains the provider name and the provide user ID.
*/
providerId: ProviderId
oauth?: {
/**
* Access token that was received during the OAuth flow.
*/
accessToken: string
/**
* Unique request ID that was generated during the OAuth flow.
*/
uniqueRequestId: string
},
/**
* User that is logged in.
*/
user: Awaited<ReturnType<typeof findAuthWithUserBy>>['user']
/**
* Request object that can be used to access the incoming request.
*/
req: ExpressRequest
} & InternalAuthHookParams

View File

@ -27,6 +27,8 @@ export type {
OnBeforeSignupHook,
OnAfterSignupHook,
OnBeforeOAuthRedirectHook,
OnBeforeLoginHook,
OnAfterLoginHook,
InternalAuthHookParams,
} from './hooks.js'

View File

@ -4,6 +4,8 @@ import type {
OnAfterSignupHook,
OnBeforeOAuthRedirectHook,
OnBeforeSignupHook,
OnBeforeLoginHook,
OnAfterLoginHook,
InternalAuthHookParams,
} from 'wasp/server/auth'
{=# onBeforeSignupHook.isDefined =}
@ -15,6 +17,12 @@ import type {
{=# onBeforeOAuthRedirectHook.isDefined =}
{=& onBeforeOAuthRedirectHook.importStatement =}
{=/ onBeforeOAuthRedirectHook.isDefined =}
{=# onBeforeLoginHook.isDefined =}
{=& onBeforeLoginHook.importStatement =}
{=/ onBeforeLoginHook.isDefined =}
{=# onAfterLoginHook.isDefined =}
{=& onAfterLoginHook.importStatement =}
{=/ onAfterLoginHook.isDefined =}
/*
These are "internal hook functions" based on the user defined hook functions.
@ -67,6 +75,35 @@ export const onBeforeOAuthRedirectHook: InternalFunctionForHook<OnBeforeOAuthRed
export const onBeforeOAuthRedirectHook: InternalFunctionForHook<OnBeforeOAuthRedirectHook> = async (params) => params
{=/ onBeforeOAuthRedirectHook.isDefined =}
{=# onBeforeLoginHook.isDefined =}
export const onBeforeLoginHook: InternalFunctionForHook<OnBeforeLoginHook> = (params) =>
{= onBeforeLoginHook.importIdentifier =}({
prisma,
...params,
})
{=/ onBeforeLoginHook.isDefined =}
{=^ onBeforeLoginHook.isDefined =}
/**
* This is a no-op function since the user didn't define the onBeforeLogin hook.
*/
export const onBeforeLoginHook: InternalFunctionForHook<OnBeforeLoginHook> = async (_params) => {}
{=/ onBeforeLoginHook.isDefined =}
{=# onAfterLoginHook.isDefined =}
export const onAfterLoginHook: InternalFunctionForHook<OnAfterLoginHook> = (params) =>
{= onAfterLoginHook.importIdentifier =}({
prisma,
...params,
})
{=/ onAfterLoginHook.isDefined =}
{=^ onAfterLoginHook.isDefined =}
/**
* This is a no-op function since the user didn't define the onAfterLogin hook.
*/
export const onAfterLoginHook: InternalFunctionForHook<OnAfterLoginHook> = async (_params) => {}
{=/ onAfterLoginHook.isDefined =}
/*
We pass extra params to the user defined hook functions, but we don't want to
pass the extra params (e.g. 'prisma') when we call the hooks in the server code.

View File

@ -9,6 +9,7 @@ import {
} from 'wasp/auth/utils'
import { createSession } from 'wasp/auth/session'
import { ensureValidEmail, ensurePasswordIsPresent } from 'wasp/auth/validation'
import { onBeforeLoginHook, onAfterLoginHook } from '../../hooks.js';
export function getLoginRoute() {
return async function login(
@ -18,9 +19,8 @@ export function getLoginRoute() {
const fields = req.body ?? {}
ensureValidArgs(fields)
const authIdentity = await findAuthIdentity(
createProviderId("email", fields.email)
)
const providerId = createProviderId("email", fields.email)
const authIdentity = await findAuthIdentity(providerId)
if (!authIdentity) {
throwInvalidCredentialsError()
}
@ -35,7 +35,16 @@ export function getLoginRoute() {
}
const auth = await findAuthWithUserBy({ id: authIdentity.authId })
await onBeforeLoginHook({ req, providerId })
const session = await createSession(auth.id)
await onAfterLoginHook({
req,
providerId,
user: auth.user,
})
return res.json({
sessionId: session.id,

View File

@ -6,13 +6,19 @@ import {
sanitizeAndSerializeProviderData,
validateAndGetUserFields,
createProviderId,
findAuthWithUserBy,
} from 'wasp/auth/utils'
import { type {= authEntityUpper =} } from 'wasp/entities'
import { prisma } from 'wasp/server'
import { type UserSignupFields, type ProviderConfig } from 'wasp/auth/providers/types'
import { getRedirectUriForOneTimeCode } from './redirect'
import { tokenStore } from './oneTimeCode'
import { onBeforeSignupHook, onAfterSignupHook } from '../../hooks.js';
import {
onBeforeSignupHook,
onAfterSignupHook,
onBeforeLoginHook,
onAfterLoginHook,
} from '../../hooks.js'
export async function finishOAuthFlowAndGetRedirectUri({
provider,
@ -42,9 +48,9 @@ export async function finishOAuthFlowAndGetRedirectUri({
oAuthState,
});
const oneTimeCode = await tokenStore.createToken(authId);
const oneTimeCode = await tokenStore.createToken(authId)
return getRedirectUriForOneTimeCode(oneTimeCode);
return getRedirectUriForOneTimeCode(oneTimeCode)
}
// We need a user id to create the auth token, so we either find an existing user
@ -78,12 +84,37 @@ async function getAuthIdFromProviderDetails({
})
if (existingAuthIdentity) {
return existingAuthIdentity.{= authFieldOnAuthIdentityEntityName =}.id
const authId = existingAuthIdentity.{= authFieldOnAuthIdentityEntityName =}.id
// NOTE: We are calling login hooks here even though we didn't log in the user yet.
// It's because we have access to the OAuth tokens here and we want to pass them to the hooks.
// We could have stored the tokens temporarily and called the hooks after the session is created,
// but this keeps the implementation simpler.
// The downside of this approach is that we can't provide the session to the login hooks, but this is
// an okay trade-off because OAuth tokens are more valuable to users than the session ID.
await onBeforeLoginHook({ req, providerId })
// NOTE: Fetching the user to pass it to the onAfterLoginHook - it's a bit wasteful
// but we wanted to keep the onAfterLoginHook params consistent for all auth providers.
const auth = await findAuthWithUserBy({ id: authId })
// NOTE: check the comment above onBeforeLoginHook for the explanation why we call onAfterLoginHook here.
await onAfterLoginHook({
req,
providerId,
oauth: {
accessToken,
uniqueRequestId: oAuthState.state,
},
user: auth.user,
})
return authId
} else {
const userFields = await validateAndGetUserFields(
{ profile: providerProfile },
userSignupFields,
);
)
// For now, we don't have any extra data for the oauth providers, so we just pass an empty object.
const providerData = await sanitizeAndSerializeProviderData({})

View File

@ -11,6 +11,7 @@ import {
} from 'wasp/auth/utils'
import { createSession } from 'wasp/auth/session'
import { ensureValidUsername, ensurePasswordIsPresent } from 'wasp/auth/validation'
import { onBeforeLoginHook, onAfterLoginHook } from '../../hooks.js';
export default handleRejection(async (req, res) => {
const fields = req.body ?? {}
@ -34,8 +35,16 @@ export default handleRejection(async (req, res) => {
id: authIdentity.authId
})
await onBeforeLoginHook({ req, providerId })
const session = await createSession(auth.id)
await onAfterLoginHook({
req,
providerId,
user: auth.user,
})
return res.json({
sessionId: session.id,
})

View File

@ -529,14 +529,14 @@
"file",
"../out/sdk/wasp/server/auth/hooks.ts"
],
"0a33f1809cfbcf86e00c4cd1d349192f686a4a8324120c90b2e53381078fda2a"
"3105d318bcb86a6638f414a155aad29dcea3613f7f14c6c282b677b9333eac57"
],
[
[
"file",
"../out/sdk/wasp/server/auth/index.ts"
],
"4d44320b8bdfb4dfc07664f813677420319f0610071e73c5b62be96ad35dd959"
"20277db8773191867bd7938215abdf4a6814f84ba38fd55dd36ae5ebbbbe0754"
],
[
[
@ -872,7 +872,7 @@
"file",
"server/src/auth/hooks.ts"
],
"c9e80d960136127dbd5a1cfc955d08323c40d5733221e2bd2bb28e12b85e274b"
"74f3d9cd68ef3d01d1f9a0729c93173b42725ecb49f5f6218fa8c1175e4c2a94"
],
[
[
@ -949,7 +949,7 @@
"file",
"server/src/auth/providers/oauth/user.ts"
],
"260527c819fee29840447730e057391dca3205fc8cbb6c491db580d8722a7875"
"4ae56549dccf3fa5d75250ef9593e1cf65012bf48219895dc92df586e5d95a45"
],
[
[

View File

@ -1,5 +1,5 @@
import type { Request as ExpressRequest } from 'express';
import type { ProviderId, createUser } from '../../auth/utils.js';
import { type ProviderId, createUser, findAuthWithUserBy } from '../../auth/utils.js';
import { prisma } from '../index.js';
import { Expand } from '../../universal/types.js';
export type OnBeforeSignupHook = (params: Expand<OnBeforeSignupHookParams>) => void | Promise<void>;
@ -12,6 +12,8 @@ export type OnBeforeOAuthRedirectHook = (params: Expand<OnBeforeOAuthRedirectHoo
} | Promise<{
url: URL;
}>;
export type OnBeforeLoginHook = (params: Expand<OnBeforeLoginHookParams>) => void | Promise<void>;
export type OnAfterLoginHook = (params: Expand<OnAfterLoginHookParams>) => void | Promise<void>;
export type InternalAuthHookParams = {
/**
* Prisma instance that can be used to interact with the database.
@ -66,4 +68,38 @@ type OnBeforeOAuthRedirectHookParams = {
*/
req: ExpressRequest;
} & InternalAuthHookParams;
type OnBeforeLoginHookParams = {
/**
* Provider ID object that contains the provider name and the provide user ID.
*/
providerId: ProviderId;
/**
* Request object that can be used to access the incoming request.
*/
req: ExpressRequest;
} & InternalAuthHookParams;
type OnAfterLoginHookParams = {
/**
* Provider ID object that contains the provider name and the provide user ID.
*/
providerId: ProviderId;
oauth?: {
/**
* Access token that was received during the OAuth flow.
*/
accessToken: string;
/**
* Unique request ID that was generated during the OAuth flow.
*/
uniqueRequestId: string;
};
/**
* User that is logged in.
*/
user: Awaited<ReturnType<typeof findAuthWithUserBy>>['user'];
/**
* Request object that can be used to access the incoming request.
*/
req: ExpressRequest;
} & InternalAuthHookParams;
export {};

View File

@ -1,4 +1,4 @@
export { defineUserSignupFields, } from '../../auth/providers/types.js';
export { createProviderId, sanitizeAndSerializeProviderData, updateAuthIdentityProviderData, deserializeAndSanitizeProviderData, findAuthIdentity, createUser, type ProviderId, type ProviderName, type EmailProviderData, type UsernameProviderData, type OAuthProviderData, } from '../../auth/utils.js';
export { ensurePasswordIsPresent, ensureValidPassword, ensureTokenIsPresent, } from '../../auth/validation.js';
export type { OnBeforeSignupHook, OnAfterSignupHook, OnBeforeOAuthRedirectHook, InternalAuthHookParams, } from './hooks.js';
export type { OnBeforeSignupHook, OnAfterSignupHook, OnBeforeOAuthRedirectHook, OnBeforeLoginHook, OnAfterLoginHook, InternalAuthHookParams, } from './hooks.js';

View File

@ -1,5 +1,5 @@
import type { Request as ExpressRequest } from 'express'
import type { ProviderId, createUser } from '../../auth/utils.js'
import { type ProviderId, createUser, findAuthWithUserBy } from '../../auth/utils.js'
import { prisma } from '../index.js'
import { Expand } from '../../universal/types.js'
@ -21,6 +21,16 @@ export type OnBeforeOAuthRedirectHook = (
params: Expand<OnBeforeOAuthRedirectHookParams>,
) => { url: URL } | Promise<{ url: URL }>
// PUBLIC API
export type OnBeforeLoginHook = (
params: Expand<OnBeforeLoginHookParams>,
) => void | Promise<void>
// PUBLIC API
export type OnAfterLoginHook = (
params: Expand<OnAfterLoginHookParams>,
) => void | Promise<void>
// PRIVATE API (used in the SDK and the server)
export type InternalAuthHookParams = {
/**
@ -85,3 +95,39 @@ type OnBeforeOAuthRedirectHookParams = {
*/
req: ExpressRequest
} & InternalAuthHookParams
type OnBeforeLoginHookParams = {
/**
* Provider ID object that contains the provider name and the provide user ID.
*/
providerId: ProviderId
/**
* Request object that can be used to access the incoming request.
*/
req: ExpressRequest
} & InternalAuthHookParams
type OnAfterLoginHookParams = {
/**
* Provider ID object that contains the provider name and the provide user ID.
*/
providerId: ProviderId
oauth?: {
/**
* Access token that was received during the OAuth flow.
*/
accessToken: string
/**
* Unique request ID that was generated during the OAuth flow.
*/
uniqueRequestId: string
},
/**
* User that is logged in.
*/
user: Awaited<ReturnType<typeof findAuthWithUserBy>>['user']
/**
* Request object that can be used to access the incoming request.
*/
req: ExpressRequest
} & InternalAuthHookParams

View File

@ -26,6 +26,8 @@ export type {
OnBeforeSignupHook,
OnAfterSignupHook,
OnBeforeOAuthRedirectHook,
OnBeforeLoginHook,
OnAfterLoginHook,
InternalAuthHookParams,
} from './hooks.js'

View File

@ -3,6 +3,8 @@ import type {
OnAfterSignupHook,
OnBeforeOAuthRedirectHook,
OnBeforeSignupHook,
OnBeforeLoginHook,
OnAfterLoginHook,
InternalAuthHookParams,
} from 'wasp/server/auth'
import { onBeforeSignup as onBeforeSignupHook_ext } from '../../../../../src/auth/hooks.js'
@ -36,6 +38,17 @@ export const onBeforeOAuthRedirectHook: InternalFunctionForHook<OnBeforeOAuthRed
...params,
})
/**
* This is a no-op function since the user didn't define the onBeforeLogin hook.
*/
export const onBeforeLoginHook: InternalFunctionForHook<OnBeforeLoginHook> = async (_params) => {}
/**
* This is a no-op function since the user didn't define the onAfterLogin hook.
*/
export const onAfterLoginHook: InternalFunctionForHook<OnAfterLoginHook> = async (_params) => {}
/*
We pass extra params to the user defined hook functions, but we don't want to
pass the extra params (e.g. 'prisma') when we call the hooks in the server code.

View File

@ -5,13 +5,19 @@ import {
sanitizeAndSerializeProviderData,
validateAndGetUserFields,
createProviderId,
findAuthWithUserBy,
} from 'wasp/auth/utils'
import { type Auth } from 'wasp/entities'
import { prisma } from 'wasp/server'
import { type UserSignupFields, type ProviderConfig } from 'wasp/auth/providers/types'
import { getRedirectUriForOneTimeCode } from './redirect'
import { tokenStore } from './oneTimeCode'
import { onBeforeSignupHook, onAfterSignupHook } from '../../hooks.js';
import {
onBeforeSignupHook,
onAfterSignupHook,
onBeforeLoginHook,
onAfterLoginHook,
} from '../../hooks.js'
export async function finishOAuthFlowAndGetRedirectUri({
provider,
@ -41,9 +47,9 @@ export async function finishOAuthFlowAndGetRedirectUri({
oAuthState,
});
const oneTimeCode = await tokenStore.createToken(authId);
const oneTimeCode = await tokenStore.createToken(authId)
return getRedirectUriForOneTimeCode(oneTimeCode);
return getRedirectUriForOneTimeCode(oneTimeCode)
}
// We need a user id to create the auth token, so we either find an existing user
@ -77,12 +83,37 @@ async function getAuthIdFromProviderDetails({
})
if (existingAuthIdentity) {
return existingAuthIdentity.auth.id
const authId = existingAuthIdentity.auth.id
// NOTE: We are calling login hooks here even though we didn't log in the user yet.
// It's because we have access to the OAuth tokens here and we want to pass them to the hooks.
// We could have stored the tokens temporarily and called the hooks after the session is created,
// but this keeps the implementation simpler.
// The downside of this approach is that we can't provide the session to the login hooks, but this is
// an okay trade-off because OAuth tokens are more valuable to users than the session ID.
await onBeforeLoginHook({ req, providerId })
// NOTE: Fetching the user to pass it to the onAfterLoginHook - it's a bit wasteful
// but we wanted to keep the onAfterLoginHook params consistent for all auth providers.
const auth = await findAuthWithUserBy({ id: authId })
// NOTE: check the comment above onBeforeLoginHook for the explanation why we call onAfterLoginHook here.
await onAfterLoginHook({
req,
providerId,
oauth: {
accessToken,
uniqueRequestId: oAuthState.state,
},
user: auth.user,
})
return authId
} else {
const userFields = await validateAndGetUserFields(
{ profile: providerProfile },
userSignupFields,
);
)
// For now, we don't have any extra data for the oauth providers, so we just pass an empty object.
const providerData = await sanitizeAndSerializeProviderData({})

View File

@ -48,6 +48,8 @@ app todoApp {
onBeforeSignup: import { onBeforeSignup } from "@src/auth/hooks.js",
onAfterSignup: import { onAfterSignup } from "@src/auth/hooks.js",
onBeforeOAuthRedirect: import { onBeforeOAuthRedirect } from "@src/auth/hooks.js",
onBeforeLogin: import { onBeforeLogin } from "@src/auth/hooks.js",
onAfterLogin: import { onAfterLogin } from "@src/auth/hooks.js",
},
server: {
setupFn: import setup from "@src/serverSetup",

View File

@ -3,6 +3,8 @@ import type {
OnAfterSignupHook,
OnBeforeOAuthRedirectHook,
OnBeforeSignupHook,
OnBeforeLoginHook,
OnAfterLoginHook,
} from 'wasp/server/auth'
export const onBeforeSignup: OnBeforeSignupHook = async (args) => {
@ -47,6 +49,20 @@ export const onBeforeOAuthRedirect: OnBeforeOAuthRedirectHook = async (
return { url: args.url }
}
export const onBeforeLogin: OnBeforeLoginHook = async (args) => {
const log = createLoggerForHook('onBeforeLogin')
log('providerId object', args.providerId)
}
export const onAfterLogin: OnAfterLoginHook = async (args) => {
const log = createLoggerForHook('onAfterLogin')
log('providerId object', args.providerId)
log('user object', args.user)
if (args.oauth) {
log('accessToken', args.oauth.accessToken)
}
}
function createLoggerForHook(hookName: string) {
return (...args: unknown[]) => {
console.log(`[${hookName}]`, ...args)

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "isOnAfterLoginHookCalled" BOOLEAN NOT NULL DEFAULT false;

View File

@ -12,7 +12,8 @@ generator client {
model User {
id Int @id @default(autoincrement())
isOnAfterSignupHookCalled Boolean @default(false)
tasks Task[]
isOnAfterLoginHookCalled Boolean @default(false)
tasks Task[]
}
model Task {

View File

@ -2,19 +2,36 @@ import { HttpError } from 'wasp/server'
import type {
OnAfterSignupHook,
OnBeforeSignupHook,
OnBeforeLoginHook,
OnAfterLoginHook,
} from 'wasp/server/auth'
export const onBeforeSignup: OnBeforeSignupHook = async (args) => {
if (args.providerId.providerUserId === 'notallowed@email.com') {
export const onBeforeSignup: OnBeforeSignupHook = async ({ providerId }) => {
if (providerId.providerUserId === 'notallowed@email.com') {
throw new HttpError(403, 'On Before Signup Hook disallows this email.')
}
}
export const onAfterSignup: OnAfterSignupHook = async (args) => {
await args.prisma.user.update({
where: { id: args.user.id },
export const onAfterSignup: OnAfterSignupHook = async ({ prisma, user }) => {
await prisma.user.update({
where: { id: user.id },
data: {
isOnAfterSignupHookCalled: true,
},
})
}
export const onBeforeLogin: OnBeforeLoginHook = async ({ providerId }) => {
if (providerId.providerUserId === 'cantlogin@email.com') {
throw new HttpError(403, 'On Before Login Hook disallows this email.')
}
}
export const onAfterLogin: OnAfterLoginHook = async ({ prisma, user }) => {
await prisma.user.update({
where: { id: user.id },
data: {
isOnAfterLoginHookCalled: true,
},
})
}

View File

@ -27,6 +27,10 @@ export const ProfilePage = ({ user }: { user: User }) => {
Value of <code>user.isOnAfterSignupHookCalled</code> is{' '}
<strong>{user.isOnAfterSignupHookCalled ? 'true' : 'false'}</strong>.
</div>
<div>
Value of <code>user.isOnAfterLoginHookCalled</code> is{' '}
<strong>{user.isOnAfterLoginHookCalled ? 'true' : 'false'}</strong>.
</div>
<br />
<Link to="/">Go to dashboard</Link>
</>

View File

@ -26,6 +26,8 @@ app todoApp {
onAuthSucceededRedirectTo: "/profile",
onBeforeSignup: import { onBeforeSignup } from "@src/auth/hooks.js",
onAfterSignup: import { onAfterSignup } from "@src/auth/hooks.js",
onBeforeLogin: import { onBeforeLogin } from "@src/auth/hooks.js",
onAfterLogin: import { onAfterLogin } from "@src/auth/hooks.js",
},
server: {
setupFn: import setup from "@src/server/serverSetup.js",

View File

@ -27,8 +27,9 @@ test.describe('auth hooks', () => {
/*
We set up the "after signup hook" to set a value in the user object.
We also set up the "after login hook" to set a value in the user object.
*/
test('after signup hook works', async ({ page }) => {
test('after signup and after login hooks work', async ({ page }) => {
const { email, password } = generateRandomCredentials()
await performSignup(page, {
@ -46,5 +47,31 @@ test.describe('auth hooks', () => {
await expect(page.locator('body')).toContainText(
'Value of user.isOnAfterSignupHookCalled is true.',
)
await expect(page.locator('body')).toContainText(
'Value of user.isOnAfterLoginHookCalled is true.',
)
})
/*
We set up the "before login hook" to throw an error for a specific email address.
*/
test('before login hook works', async ({ page }) => {
const emailThatThrowsError = 'cantlogin@email.com'
const password = '12345678'
await performSignup(page, {
email: emailThatThrowsError,
password,
})
await performLogin(page, {
email: emailThatThrowsError,
password,
})
await expect(page.locator('body')).toContainText(
'On Before Login Hook disallows this email.',
)
})
})

View File

@ -37,7 +37,9 @@ data Auth = Auth
onAuthSucceededRedirectTo :: Maybe String,
onBeforeSignup :: Maybe ExtImport,
onAfterSignup :: Maybe ExtImport,
onBeforeOAuthRedirect :: Maybe ExtImport
onBeforeOAuthRedirect :: Maybe ExtImport,
onBeforeLogin :: Maybe ExtImport,
onAfterLogin :: Maybe ExtImport
}
deriving (Show, Eq, Data)

View File

@ -110,11 +110,15 @@ genAuthHooks auth = return $ C.mkTmplFdWithData [relfile|src/auth/hooks.ts|] (Ju
object
[ "onBeforeSignupHook" .= onBeforeSignupHook,
"onAfterSignupHook" .= onAfterSignupHook,
"onBeforeOAuthRedirectHook" .= onBeforeOAuthRedirectHook
"onBeforeOAuthRedirectHook" .= onBeforeOAuthRedirectHook,
"onBeforeLoginHook" .= onBeforeLoginHook,
"onAfterLoginHook" .= onAfterLoginHook
]
onBeforeSignupHook = extImportToAliasedImportJson "onBeforeSignupHook_ext" relPathToServerSrcDir $ AS.Auth.onBeforeSignup auth
onAfterSignupHook = extImportToAliasedImportJson "onAfterSignupHook_ext" relPathToServerSrcDir $ AS.Auth.onAfterSignup auth
onBeforeOAuthRedirectHook = extImportToAliasedImportJson "onBeforeOAuthRedirectHook_ext" relPathToServerSrcDir $ AS.Auth.onBeforeOAuthRedirect auth
onBeforeLoginHook = extImportToAliasedImportJson "onBeforeLoginHook_ext" relPathToServerSrcDir $ AS.Auth.onBeforeLogin auth
onAfterLoginHook = extImportToAliasedImportJson "onAfterLoginHook_ext" relPathToServerSrcDir $ AS.Auth.onAfterLogin auth
relPathToServerSrcDir :: Path Posix (Rel importLocation) (Dir C.ServerSrcDir)
relPathToServerSrcDir = [reldirP|../|]

View File

@ -152,7 +152,9 @@ spec_Analyzer = do
Auth.onAuthSucceededRedirectTo = Nothing,
Auth.onBeforeSignup = Nothing,
Auth.onAfterSignup = Nothing,
Auth.onBeforeOAuthRedirect = Nothing
Auth.onBeforeOAuthRedirect = Nothing,
Auth.onBeforeLogin = Nothing,
Auth.onAfterLogin = Nothing
},
App.server =
Just

View File

@ -123,7 +123,9 @@ spec_AppSpecValid = do
AS.Auth.onAuthSucceededRedirectTo = Nothing,
AS.Auth.onBeforeSignup = Nothing,
AS.Auth.onAfterSignup = Nothing,
AS.Auth.onBeforeOAuthRedirect = Nothing
AS.Auth.onBeforeOAuthRedirect = Nothing,
AS.Auth.onBeforeLogin = Nothing,
AS.Auth.onAfterLogin = Nothing
}
describe "should validate that when a page has authRequired, app.auth is also set." $ do
@ -169,7 +171,9 @@ spec_AppSpecValid = do
AS.Auth.onAuthSucceededRedirectTo = Nothing,
AS.Auth.onBeforeSignup = Nothing,
AS.Auth.onAfterSignup = Nothing,
AS.Auth.onBeforeOAuthRedirect = Nothing
AS.Auth.onBeforeOAuthRedirect = Nothing,
AS.Auth.onBeforeLogin = Nothing,
AS.Auth.onAfterLogin = Nothing
},
AS.App.emailSender =
Just
@ -311,7 +315,9 @@ spec_AppSpecValid = do
AS.Auth.onAuthSucceededRedirectTo = Nothing,
AS.Auth.onBeforeSignup = Nothing,
AS.Auth.onAfterSignup = Nothing,
AS.Auth.onBeforeOAuthRedirect = Nothing
AS.Auth.onBeforeOAuthRedirect = Nothing,
AS.Auth.onBeforeLogin = Nothing,
AS.Auth.onAfterLogin = Nothing
},
AS.App.emailSender = emailSender
},

View File

@ -13,8 +13,10 @@ The following auth hooks are available in Wasp:
- [`onBeforeSignup`](#executing-code-before-the-user-signs-up)
- [`onAfterSignup`](#executing-code-after-the-user-signs-up)
- [`onBeforeOAuthRedirect`](#executing-code-before-the-oauth-redirect)
- [`onBeforeLogin`](#executing-code-before-the-user-logs-in)
- [`onAfterLogin`](#executing-code-after-the-user-logs-in)
We'll go through each of these hooks in detail. But first, let's see how the hooks fit into the signup flow:
We'll go through each of these hooks in detail. But first, let's see how the hooks fit into the auth flows:
<ImgWithCaption
source="/img/auth-hooks/signup_flow_with_hooks.png"
@ -22,7 +24,19 @@ We'll go through each of these hooks in detail. But first, let's see how the hoo
caption="Signup Flow with Hooks"
/>
If you are using OAuth, the flow includes extra steps before the signup flow:
<ImgWithCaption
source="/img/auth-hooks/login_flow_with_hooks.png"
alt="Login Flow with Hooks"
caption="Login Flow with Hooks *"
/>
<small>
\* When using the OAuth auth providers, the login hooks are both called before the session is created but the session is created quickly afterward, so it shouldn't make any difference in practice.
</small>
If you are using OAuth, the flow includes extra steps before the auth flow:
<ImgWithCaption
source="/img/auth-hooks/oauth_flow_with_hooks.png"
@ -50,6 +64,8 @@ app myApp {
onBeforeSignup: import { onBeforeSignup } from "@src/auth/hooks",
onAfterSignup: import { onAfterSignup } from "@src/auth/hooks",
onBeforeOAuthRedirect: import { onBeforeOAuthRedirect } from "@src/auth/hooks",
onBeforeLogin: import { onBeforeLogin } from "@src/auth/hooks",
onAfterLogin: import { onAfterLogin } from "@src/auth/hooks",
},
}
```
@ -69,6 +85,8 @@ app myApp {
onBeforeSignup: import { onBeforeSignup } from "@src/auth/hooks",
onAfterSignup: import { onAfterSignup } from "@src/auth/hooks",
onBeforeOAuthRedirect: import { onBeforeOAuthRedirect } from "@src/auth/hooks",
onBeforeLogin: import { onBeforeLogin } from "@src/auth/hooks",
onAfterLogin: import { onAfterLogin } from "@src/auth/hooks",
},
}
```
@ -268,7 +286,7 @@ Read more about the data the `onAfterSignup` hook receives in the [API Reference
Wasp calls the `onBeforeOAuthRedirect` hook after the OAuth redirect URL is generated but before redirecting the user. This hook can access the request object sent from the client at the start of the OAuth process.
The `onBeforeOAuthRedirect` hook can be useful if you want to save some data (e.g. request query parameters) that can be used later in the OAuth flow. You can use the `uniqueRequestId` parameter to reference this data later in the `onAfterSignup` hook.
The `onBeforeOAuthRedirect` hook can be useful if you want to save some data (e.g. request query parameters) that you can use later in the OAuth flow. You can use the `uniqueRequestId` parameter to reference this data later in the `onAfterSignup` or `onAfterLogin` hooks.
Works with <DiscordPill /> <GithubPill /> <GooglePill /> <KeycloakPill />
@ -294,7 +312,7 @@ export const onBeforeOAuthRedirect = async ({
}) => {
console.log('query params before oAuth redirect', req.query)
// Saving query params for later use in the onAfterSignup hook
// Saving query params for later use in onAfterSignup or onAfterLogin hooks
const id = uniqueRequestId
someKindOfStore.set(id, req.query)
@ -326,7 +344,7 @@ export const onBeforeOAuthRedirect: OnBeforeOAuthRedirectHook = async ({
}) => {
console.log('query params before oAuth redirect', req.query)
// Saving query params for later use in the onAfterSignup hook
// Saving query params for later use in onAfterSignup or onAfterLogin hooks
const id = uniqueRequestId
someKindOfStore.set(id, req.query)
@ -341,6 +359,167 @@ This hook's return value must be an object that looks like this: `{ url: URL }`.
Read more about the data the `onBeforeOAuthRedirect` hook receives in the [API Reference](#the-onbeforeoauthredirect-hook).
### Executing code before the user logs in
Wasp calls the `onBeforeLogin` hook before the user is logged in.
The `onBeforeLogin` hook can be useful if you want to reject a user based on some criteria before they log in.
Works with <EmailPill /> <UsernameAndPasswordPill /> <DiscordPill /> <GithubPill /> <GooglePill /> <KeycloakPill />
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```wasp title="main.wasp"
app myApp {
...
auth: {
...
onBeforeLogin: import { onBeforeLogin } from "@src/auth/hooks",
},
}
```
```js title="src/auth/hooks.js"
import { HttpError } from 'wasp/server'
export const onBeforeLogin = async ({
providerId,
prisma,
req,
}) => {
if (providerId.providerName === 'email' && providerId.providerUserId === 'some@email.com') {
throw new HttpError(403, 'You cannot log in with this email')
}
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```wasp title="main.wasp"
app myApp {
...
auth: {
...
onBeforeLogin: import { onBeforeLogin } from "@src/auth/hooks",
},
}
```
```ts title="src/auth/hooks.ts"
import { HttpError } from 'wasp/server'
import type { OnBeforeLoginHook } from 'wasp/server/auth'
export const onBeforeLogin: OnBeforeLoginHook = async ({
providerId,
prisma,
req,
}) => {
if (providerId.providerName === 'email' && providerId.providerUserId === 'some@email.com') {
throw new HttpError(403, 'You cannot log in with this email')
}
}
```
</TabItem>
</Tabs>
Read more about the data the `onBeforeLogin` hook receives in the [API Reference](#the-onbeforelogin-hook).
### Executing code after the user logs in
Wasp calls the `onAfterLogin` hook after the user logs in.
The `onAfterLogin` hook can be useful if you want to perform some action after the user logs in, like syncing the user with a third-party service.
Since the `onAfterLogin` hook receives the OAuth access token, it can also be used to update the OAuth access token for the user in your database.
Works with <EmailPill /> <UsernameAndPasswordPill /> <DiscordPill /> <GithubPill /> <GooglePill /> <KeycloakPill />
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```wasp title="main.wasp"
app myApp {
...
auth: {
...
onAfterLogin: import { onAfterLogin } from "@src/auth/hooks",
},
}
```
```js title="src/auth/hooks.js"
export const onAfterLogin = async ({
providerId,
user,
oauth,
prisma,
req,
}) => {
console.log('user object', user)
// If this is an OAuth signup, we have the access token and uniqueRequestId
if (oauth) {
console.log('accessToken', oauth.accessToken)
console.log('uniqueRequestId', oauth.uniqueRequestId)
const id = oauth.uniqueRequestId
const data = someKindOfStore.get(id)
if (data) {
console.log('saved data for the ID', data)
}
someKindOfStore.delete(id)
}
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```wasp title="main.wasp"
app myApp {
...
auth: {
...
onAfterLogin: import { onAfterLogin } from "@src/auth/hooks",
},
}
```
```ts title="src/auth/hooks.ts"
import type { OnAfterLoginHook } from 'wasp/server/auth'
export const onAfterLogin: OnAfterLoginHook = async ({
providerId,
user,
oauth,
prisma,
req,
}) => {
console.log('user object', user)
// If this is an OAuth signup, we have the access token and uniqueRequestId
if (oauth) {
console.log('accessToken', oauth.accessToken)
console.log('uniqueRequestId', oauth.uniqueRequestId)
const id = oauth.uniqueRequestId
const data = someKindOfStore.get(id)
if (data) {
console.log('saved data for the ID', data)
}
someKindOfStore.delete(id)
}
}
```
</TabItem>
</Tabs>
Read more about the data the `onAfterLogin` hook receives in the [API Reference](#the-onafterlogin-hook).
## API Reference
<Tabs groupId="js-ts">
@ -359,6 +538,8 @@ app myApp {
onBeforeSignup: import { onBeforeSignup } from "@src/auth/hooks",
onAfterSignup: import { onAfterSignup } from "@src/auth/hooks",
onBeforeOAuthRedirect: import { onBeforeOAuthRedirect } from "@src/auth/hooks",
onBeforeLogin: import { onBeforeLogin } from "@src/auth/hooks",
onAfterLogin: import { onAfterLogin } from "@src/auth/hooks",
},
}
```
@ -378,26 +559,38 @@ app myApp {
onBeforeSignup: import { onBeforeSignup } from "@src/auth/hooks",
onAfterSignup: import { onAfterSignup } from "@src/auth/hooks",
onBeforeOAuthRedirect: import { onBeforeOAuthRedirect } from "@src/auth/hooks",
onBeforeLogin: import { onBeforeLogin } from "@src/auth/hooks",
onAfterLogin: import { onAfterLogin } from "@src/auth/hooks",
},
}
```
</TabItem>
</Tabs>
### Common hook input
The following properties are available in all auth hooks:
- `prisma: PrismaClient`
The Prisma client instance which you can use to query your database.
- `req: Request`
The [Express request object](https://expressjs.com/en/api.html#req) from which you can access the request headers, cookies, etc.
### The `onBeforeSignup` hook
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```js title="src/auth/hooks.js"
import { HttpError } from 'wasp/server'
export const onBeforeSignup = async ({
providerId,
prisma,
req,
}) => {
// Hook code here
// Hook code goes here
}
```
@ -405,7 +598,6 @@ export const onBeforeSignup = async ({
<TabItem value="ts" label="TypeScript">
```ts title="src/auth/hooks.ts"
import { HttpError } from 'wasp/server'
import type { OnBeforeSignupHook } from 'wasp/server/auth'
export const onBeforeSignup: OnBeforeSignupHook = async ({
@ -413,7 +605,7 @@ export const onBeforeSignup: OnBeforeSignupHook = async ({
prisma,
req,
}) => {
// Hook code here
// Hook code goes here
}
```
@ -422,21 +614,9 @@ export const onBeforeSignup: OnBeforeSignupHook = async ({
The hook receives an object as **input** with the following properties:
- `providerId: ProviderId`
- [`providerId: ProviderId`](#providerid-fields)
The user's provider ID is an object with two properties:
- `providerName: string`
The provider's name (e.g. `'email'`, `'google'`, `'github'`)
- `providerUserId: string`
The user's unique ID in the provider's system (e.g. email, Google ID, GitHub ID)
- `prisma: PrismaClient`
The Prisma client instance which you can use to query your database.
- `req: Request`
The [Express request object](https://expressjs.com/en/api.html#req) from which you can access the request headers, cookies, etc.
- Plus the [common hook input](#common-hook-input)
Wasp ignores this hook's **return value**.
@ -453,7 +633,7 @@ export const onAfterSignup = async ({
prisma,
req,
}) => {
// Hook code here
// Hook code goes here
}
```
@ -470,7 +650,7 @@ export const onAfterSignup: OnAfterSignupHook = async ({
prisma,
req,
}) => {
// Hook code here
// Hook code goes here
}
```
@ -478,36 +658,14 @@ export const onAfterSignup: OnAfterSignupHook = async ({
</Tabs>
The hook receives an object as **input** with the following properties:
- `providerId: ProviderId`
The user's provider ID is an object with two properties:
- `providerName: string`
- [`providerId: ProviderId`](#providerid-fields)
The provider's name (e.g. `'email'`, `'google'`, `'github'`)
- `providerUserId: string`
The user's unique ID in the provider's system (e.g. email, Google ID, GitHub ID)
- `user: User`
The user object that was created.
- `oauth?: OAuthFields`
- [`oauth?: OAuthFields`](#oauth-fields)
This object is present only when the user is created using [Social Auth](./social-auth/overview.md).
It contains the following fields:
- `accessToken: string`
You can use the OAuth access token to use the provider's API on user's behalf.
- `uniqueRequestId: string`
The unique request ID for the OAuth flow (you might know it as the `state` parameter in OAuth.)
You can use the unique request ID to get the data saved in the `onBeforeOAuthRedirect` hook.
- `prisma: PrismaClient`
The Prisma client instance which you can use to query your database.
- `req: Request`
The [Express request object](https://expressjs.com/en/api.html#req) from which you can access the request headers, cookies, etc.
- Plus the [common hook input](#common-hook-input)
Wasp ignores this hook's **return value**.
@ -523,7 +681,7 @@ export const onBeforeOAuthRedirect = async ({
prisma,
req,
}) => {
// Hook code here
// Hook code goes here
return { url }
}
@ -541,7 +699,7 @@ export const onBeforeOAuthRedirect: OnBeforeOAuthRedirectHook = async ({
prisma,
req,
}) => {
// Hook code here
// Hook code goes here
return { url }
}
@ -558,12 +716,124 @@ The hook receives an object as **input** with the following properties:
The unique request ID for the OAuth flow (you might know it as the `state` parameter in OAuth.)
You can use the unique request ID to save data (e.g. request query params) that you can later use in the `onAfterSignup` hook.
- `prisma: PrismaClient`
The Prisma client instance which you can use to query your database.
- `req: Request`
The [Express request object](https://expressjs.com/en/api.html#req) from which you can access the request headers, cookies, etc.
You can use the unique request ID to save data (e.g. request query params) that you can later use in the `onAfterSignup` or `onAfterLogin` hooks.
- Plus the [common hook input](#common-hook-input)
This hook's return value must be an object that looks like this: `{ url: URL }`. Wasp uses the URL to redirect the user to the OAuth provider.
### The `onBeforeLogin` hook
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```js title="src/auth/hooks.js"
export const onBeforeLogin = async ({
providerId,
prisma,
req,
}) => {
// Hook code goes here
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```ts title="src/auth/hooks.ts"
import type { OnBeforeLoginHook } from 'wasp/server/auth'
export const onBeforeLogin: OnBeforeLoginHook = async ({
providerId,
prisma,
req,
}) => {
// Hook code goes here
}
```
</TabItem>
</Tabs>
The hook receives an object as **input** with the following properties:
- [`providerId: ProviderId`](#providerid-fields)
- Plus the [common hook input](#common-hook-input)
Wasp ignores this hook's **return value**.
### The `onAfterLogin` hook
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```js title="src/auth/hooks.js"
export const onAfterLogin = async ({
providerId,
user,
oauth,
prisma,
req,
}) => {
// Hook code goes here
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```ts title="src/auth/hooks.ts"
import type { OnAfterLoginHook } from 'wasp/server/auth'
export const onAfterLogin: OnAfterLoginHook = async ({
providerId,
user,
oauth,
prisma,
req,
}) => {
// Hook code goes here
}
```
</TabItem>
</Tabs>
The hook receives an object as **input** with the following properties:
- [`providerId: ProviderId`](#providerid-fields)
- `user: User`
The logged-in user's object.
- [`oauth?: OAuthFields`](#oauth-fields)
- Plus the [common hook input](#common-hook-input)
Wasp ignores this hook's **return value**.
### ProviderId fields
The `providerId` object represents the user for the current authentication method. Wasp passes it to the `onBeforeSignup`, `onAfterSignup`, `onBeforeLogin`, and `onAfterLogin` hooks.
It has the following fields:
- `providerName: string`
The provider's name (e.g. `'email'`, `'google'`, `'github`)
- `providerUserId: string`
The user's unique ID in the provider's system (e.g. email, Google ID, GitHub ID)
### OAuth fields
Wasp passes the `oauth` object to the `onAfterSignup` and `onAfterLogin` hooks only when the user is authenticated with [Social Auth](./social-auth/overview.md).
It has the following fields:
- `accessToken: string`
You can use the OAuth access token to make requests to the provider's API on the user's behalf.
- `uniqueRequestId: string`
The unique request ID for the OAuth flow (you might know it as the `state` parameter in OAuth.)
You can use the unique request ID to get the data that was saved in the `onBeforeOAuthRedirect` hook.

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 418 KiB

After

Width:  |  Height:  |  Size: 417 KiB