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

Auth Hooks #1993

Merged
merged 39 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
7321db6
WIP auth hooks
infomiho Apr 24, 2024
0bdcaa5
e2e tests
infomiho May 22, 2024
de1472e
Fixes unit tests
infomiho Apr 29, 2024
f62f6b1
Updates changelog
infomiho May 9, 2024
e661d9a
Auth Hooks docs
infomiho May 2, 2024
7973d51
Updates docs with hookName
infomiho May 2, 2024
a175efa
Update e2e tests to use auth hooks. Cleanup old auth e2e tests.
infomiho May 8, 2024
d9c9d14
Playing around with oauth state WIP
infomiho May 8, 2024
8c9442b
Adds diagrams
infomiho May 9, 2024
09b4d41
Remove "onAfterOAuthTokenReceived" hook
infomiho May 22, 2024
e6cdb3c
Adds accessToken to "onAfterSignup" hook
infomiho May 22, 2024
22a07f6
Passing in OAuth state into the hooks
infomiho May 22, 2024
6aa946c
e2e tests
infomiho May 22, 2024
99bad30
Update docs
infomiho May 22, 2024
a94e42f
Make state required. Update params shape.
infomiho May 23, 2024
8f1451b
Updates docs
infomiho May 23, 2024
1ca9e55
Cleanup
infomiho May 23, 2024
bdd7e70
Update diagrams
infomiho May 23, 2024
95fee60
Adds auth hook headless tests (#2022)
infomiho Jun 5, 2024
0b93adc
PR comments
infomiho Jun 6, 2024
7c4499d
Cleanup
infomiho Jun 6, 2024
810f4f5
PR comments
infomiho Jun 7, 2024
93a1e3b
Docs cleanup
infomiho Jun 7, 2024
30ddf94
Simplify the diagrams
infomiho Jun 7, 2024
f3f95f7
Cleanup
infomiho Jun 7, 2024
74f42c6
Simplify types
infomiho Jun 7, 2024
38571c8
Docs update
infomiho Jun 10, 2024
d83e8fe
Rewrite common params type. Remove hook name
infomiho Jun 10, 2024
049ce5a
Update comment
infomiho Jun 10, 2024
da77339
Update state type types
infomiho Jun 10, 2024
5e3af82
Cleanup
infomiho Jun 10, 2024
2a2c58b
Cleanup
infomiho Jun 10, 2024
395486c
Cleanup
infomiho Jun 10, 2024
1d44ea1
Fixes types
infomiho Jun 10, 2024
f09c60a
Update types
infomiho Jun 10, 2024
0f30fae
Merge branch 'main' into miho-auth-hooks-2
infomiho Jun 19, 2024
c4dcf75
PR comments
infomiho Jun 20, 2024
df73c08
Split OAuth state generating from validation/storing
infomiho Jun 20, 2024
9933306
Update state and cookies typing
infomiho Jun 20, 2024
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
14 changes: 13 additions & 1 deletion waspc/ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
# Changelog

## 0.14.0 (2024-04-22)
## 0.14.0 (TBD)

### 🎉 New Features

- Simplified Auth User API: Introduced a simpler API for accessing user auth fields (for example `username`, `email`, `isEmailVerified`) directly on the `user` object, eliminating the need for helper functions.
- Improved API for calling Operations (Queries and Actions) directly.
- Auth Hooks: you can now hook into the auth process with `onBeforeSignup`, `onAfterSignup` hooks. You can also modify the OAuth redirect URL with `onBeforeOAuthRedirect` hook.

```wasp
app myApp {
...
auth: {
onBeforeSignup: import { onBeforeSignup } from "...",
onAfterSignup: import { onAfterSignup } from "...",
onBeforeOAuthRedirect: import { onBeforeOAuthRedirect } from "...",
},
}
```

### ⚠️ Breaking Changes & Migration Guide

Expand Down
87 changes: 87 additions & 0 deletions waspc/data/Generator/templates/sdk/wasp/server/auth/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type { Request as ExpressRequest } from 'express'
import type { ProviderId, createUser } from '../../auth/utils.js'
import { prisma } from '../index.js'
import { Expand } from '../../universal/types.js'

// PUBLIC API
export type OnBeforeSignupHook = (
params: Expand<OnBeforeSignupHookParams>,
) => void | Promise<void>

// PUBLIC API
export type OnAfterSignupHook = (
params: Expand<OnAfterSignupHookParams>,
) => void | Promise<void>

// PUBLIC API
/**
* @returns Object with a URL that the OAuth flow should redirect to.
*/
export type OnBeforeOAuthRedirectHook = (
params: Expand<OnBeforeOAuthRedirectHookParams>,
) => { url: URL } | Promise<{ url: URL }>

// PRIVATE API (used in the SDK and the server)
export type InternalAuthHookParams = {
/**
* Prisma instance that can be used to interact with the database.
*/
prisma: typeof prisma
}

// NOTE: We should be exporting types that can be reached by users via other
// exported types (e.g. using the Parameters<T> Typescript helper).
// However, we are not exporting this type to keep the API surface smaller.
// This type is only used internally by the SDK. Exporting it might confuse
// users since the name is too similar to the exported function type.
// Same goes for all other *Params types in this file.
type OnBeforeSignupHookParams = {
/**
* 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 OnAfterSignupHookParams = {
/**
* Provider ID object that contains the provider name and the provide user ID.
*/
sodic marked this conversation as resolved.
Show resolved Hide resolved
providerId: ProviderId
/**
* User object that was created during the signup process.
*/
user: Awaited<ReturnType<typeof createUser>>
sodic marked this conversation as resolved.
Show resolved Hide resolved
oauth?: {
/**
* Access token that was received during the OAuth flow.
*/
infomiho marked this conversation as resolved.
Show resolved Hide resolved
accessToken: string
/**
* Unique request ID that was generated during the OAuth flow.
*/
uniqueRequestId: string
},
/**
* Request object that can be used to access the incoming request.
*/
req: ExpressRequest
} & InternalAuthHookParams

type OnBeforeOAuthRedirectHookParams = {
/**
* URL that the OAuth flow should redirect to.
*/
url: URL
/**
* Unique request ID that was generated during the OAuth flow.
*/
uniqueRequestId: string
/**
* Request object that can be used to access the incoming request.
*/
req: ExpressRequest
} & InternalAuthHookParams
7 changes: 7 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 @@ -23,6 +23,13 @@ export {
ensureTokenIsPresent,
} from '../../auth/validation.js'

export type {
OnBeforeSignupHook,
OnAfterSignupHook,
OnBeforeOAuthRedirectHook,
InternalAuthHookParams,
} from './hooks.js'

{=# isEmailAuthEnabled =}
export * from './email/index.js'
{=/ isEmailAuthEnabled =}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export type AuthUser = AuthUserData & {
}

// PRIVATE API
/**
/*
* Ideally, we'd do something like this:
* ```
* export type AuthUserData = ReturnType<typeof createAuthUserData>
Expand Down
80 changes: 80 additions & 0 deletions waspc/data/Generator/templates/server/src/auth/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{{={= =}=}}
import { prisma } from 'wasp/server'
import type {
OnAfterSignupHook,
OnBeforeOAuthRedirectHook,
OnBeforeSignupHook,
InternalAuthHookParams,
} from 'wasp/server/auth'
{=# onBeforeSignupHook.isDefined =}
{=& onBeforeSignupHook.importStatement =}
{=/ onBeforeSignupHook.isDefined =}
{=# onAfterSignupHook.isDefined =}
{=& onAfterSignupHook.importStatement =}
{=/ onAfterSignupHook.isDefined =}
{=# onBeforeOAuthRedirectHook.isDefined =}
{=& onBeforeOAuthRedirectHook.importStatement =}
{=/ onBeforeOAuthRedirectHook.isDefined =}

/*
infomiho marked this conversation as resolved.
Show resolved Hide resolved
These are "internal hook functions" based on the user defined hook functions.

In the server code (e.g. email signup) we import these functions and call them.

We want to pass extra params to the user defined hook functions, but we don't want to
pass them when we call them in the server code.
*/

{=# onBeforeSignupHook.isDefined =}
export const onBeforeSignupHook: InternalFunctionForHook<OnBeforeSignupHook> = (params) =>
{= onBeforeSignupHook.importIdentifier =}({
prisma,
...params,
})
{=/ onBeforeSignupHook.isDefined =}
{=^ onBeforeSignupHook.isDefined =}
/**
* This is a no-op function since the user didn't define the onBeforeSignup hook.
*/
export const onBeforeSignupHook: InternalFunctionForHook<OnBeforeSignupHook> = async (params) => {}
{=/ onBeforeSignupHook.isDefined =}

{=# onAfterSignupHook.isDefined =}
export const onAfterSignupHook: InternalFunctionForHook<OnAfterSignupHook> = (params) =>
{= onAfterSignupHook.importIdentifier =}({
prisma,
...params,
})
{=/ onAfterSignupHook.isDefined =}
{=^ onAfterSignupHook.isDefined =}
/**
* This is a no-op function since the user didn't define the onAfterSignup hook.
*/
export const onAfterSignupHook: InternalFunctionForHook<OnAfterSignupHook> = async (params) => {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maaaybe just add an underscore to params.

{=/ onAfterSignupHook.isDefined =}

{=# onBeforeOAuthRedirectHook.isDefined =}
export const onBeforeOAuthRedirectHook: InternalFunctionForHook<OnBeforeOAuthRedirectHook> = (params) =>
{= onBeforeOAuthRedirectHook.importIdentifier =}({
prisma,
...params,
})
{=/ onBeforeOAuthRedirectHook.isDefined =}
{=^ onBeforeOAuthRedirectHook.isDefined =}
/**
* This is an identity function since the user didn't define the onBeforeOAuthRedirect hook.
*/
export const onBeforeOAuthRedirectHook: InternalFunctionForHook<OnBeforeOAuthRedirectHook> = async (params) => params
{=/ onBeforeOAuthRedirectHook.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.
So, we need to remove the extra params from the params object which is used to define the
internal hook functions.
*/
type InternalFunctionForHook<Fn extends (args: never) => unknown | Promise<unknown>> = Fn extends (
params: infer P,
) => infer R
? (args: Omit<P, keyof InternalAuthHookParams>) => R
: never
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,11 @@ const _waspConfig: ProviderConfig = {

return createOAuthProviderRouter({
provider,
stateTypes: ['state'],
oAuthType: 'OAuth2',
userSignupFields: _waspUserSignupFields,
getAuthorizationUrl: ({ state }) => github.createAuthorizationURL(state, config),
getProviderInfo: async ({ code }) => {
const { accessToken } = await github.validateAuthorizationCode(code);
return getGithubProfile(accessToken);
},
getProviderTokens: ({ code }) => github.validateAuthorizationCode(code),
infomiho marked this conversation as resolved.
Show resolved Hide resolved
getProviderInfo: ({ accessToken }) => getGithubProfile(accessToken),
});
},
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,11 @@ const _waspConfig: ProviderConfig = {

return createOAuthProviderRouter({
provider,
stateTypes: ['state', 'codeVerifier'],
oAuthType: 'OAuth2WithPKCE',
userSignupFields: _waspUserSignupFields,
getAuthorizationUrl: ({ state, codeVerifier }) => google.createAuthorizationURL(state, codeVerifier, config),
getProviderInfo: async ({ code, codeVerifier }) => {
const { accessToken } = await google.validateAuthorizationCode(code, codeVerifier);
return getGoogleProfile(accessToken);
},
getProviderTokens: ({ code, codeVerifier }) => google.validateAuthorizationCode(code, codeVerifier),
getProviderInfo: ({ accessToken }) => getGoogleProfile(accessToken),
});
},
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,11 @@ const _waspConfig: ProviderConfig = {

return createOAuthProviderRouter({
provider,
stateTypes: ['state', 'codeVerifier'],
oAuthType: 'OAuth2WithPKCE',
userSignupFields: _waspUserSignupFields,
getAuthorizationUrl: ({ state, codeVerifier }) => keycloak.createAuthorizationURL(state, codeVerifier, config),
getProviderInfo: async ({ code, codeVerifier }) => {
const { accessToken } = await keycloak.validateAuthorizationCode(code, codeVerifier);
return getKeycloakProfile(accessToken);
},
getProviderTokens: ({ code, codeVerifier }) => keycloak.validateAuthorizationCode(code, codeVerifier),
getProviderInfo: ({ accessToken }) => getKeycloakProfile(accessToken),
});
},
}
Expand Down
Loading
Loading