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

Implement login hooks #2211

Merged
merged 14 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion waspc/data/Generator/templates/sdk/wasp/server/auth/hooks.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -21,6 21,16 @@ export type OnBeforeOAuthRedirectHook = (
params: Expand<OnBeforeOAuthRedirectHookParams>,
) => { url: URL } | Promise<{ url: URL }>

// PUBLIC API
export type OnBeforeLoginHook = (
sodic marked this conversation as resolved.
Show resolved Hide resolved
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 = {
/**
Expand Down Expand Up @@ -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
2 changes: 2 additions & 0 deletions waspc/data/Generator/templates/sdk/wasp/server/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 27,8 @@ export type {
OnBeforeSignupHook,
OnAfterSignupHook,
OnBeforeOAuthRedirectHook,
OnBeforeLoginHook,
OnAfterLoginHook,
InternalAuthHookParams,
} from './hooks.js'

Expand Down
37 changes: 37 additions & 0 deletions waspc/data/Generator/templates/server/src/auth/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 4,8 @@ import type {
OnAfterSignupHook,
OnBeforeOAuthRedirectHook,
OnBeforeSignupHook,
OnBeforeLoginHook,
OnAfterLoginHook,
InternalAuthHookParams,
} from 'wasp/server/auth'
{=# onBeforeSignupHook.isDefined =}
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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()
}
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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({})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? {}
Expand All @@ -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,
})
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading