Wasp supports e-mail authentication out of the box, along with email verification and "forgot your password?" flows. It provides you with the server-side implementation and email templates for all of these flows.
Wasp currently doesn't support multiple auth identities for a single user. This means, for example, that a user can't have both an email-based auth identity and a Google-based auth identity. This is something we will add in the future with the introduction of the account merging feature.
Account merging means that multiple auth identities can be merged into a single user account. For example, a user's email and Google identity can be merged into a single user account. Then the user can log in with either their email or Google account and they will be logged into the same account.
Setting Up Email Authentication
We'll need to take the following steps to set up email authentication:
- Enable email authentication in the Wasp file
- Add the
User
entity - Add the auth routes and pages
- Use Auth UI components in our pages
- Set up the email sender
Structure of the main.wasp
file we will end up with:
// Configuring e-mail authentication
app myApp {
auth: { ... }
}
// Defining routes and pages
route SignupRoute { ... }
page SignupPage { ... }
// ...
1. Enable Email Authentication in main.wasp
Let's start with adding the following to our main.wasp
file:
- JavaScript
- TypeScript
app myApp {
wasp: {
version: "^0.15.0"
},
title: "My App",
auth: {
// 1. Specify the user entity (we'll define it next)
userEntity: User,
methods: {
// 2. Enable email authentication
email: {
// 3. Specify the email from field
fromField: {
name: "My App Postman",
email: "[email protected]"
},
// 4. Specify the email verification and password reset options (we'll talk about them later)
emailVerification: {
clientRoute: EmailVerificationRoute,
},
passwordReset: {
clientRoute: PasswordResetRoute,
},
},
},
onAuthFailedRedirectTo: "/login",
onAuthSucceededRedirectTo: "/"
},
}
app myApp {
wasp: {
version: "^0.15.0"
},
title: "My App",
auth: {
// 1. Specify the user entity (we'll define it next)
userEntity: User,
methods: {
// 2. Enable email authentication
email: {
// 3. Specify the email from field
fromField: {
name: "My App Postman",
email: "[email protected]"
},
// 4. Specify the email verification and password reset options (we'll talk about them later)
emailVerification: {
clientRoute: EmailVerificationRoute,
},
passwordReset: {
clientRoute: PasswordResetRoute,
},
},
},
onAuthFailedRedirectTo: "/login",
onAuthSucceededRedirectTo: "/"
},
}
Read more about the email
auth method options here.
2. Add the User Entity
The User
entity can be as simple as including only the id
field:
- JavaScript
- TypeScript
// 5. Define the user entity
model User {
id Int @id @default(autoincrement())
// Add your own fields below
// ...
}
// 5. Define the user entity
model User {
id Int @id @default(autoincrement())
// Add your own fields below
// ...
}
You can read more about how the User
is connected to the rest of the auth system and how you can access the user data in the Accessing User Data section of the docs.
3. Add the Routes and Pages
Next, we need to define the routes and pages for the authentication pages.
Add the following to the main.wasp
file:
- JavaScript
- TypeScript
// ...
route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import { Login } from "@src/pages/auth.jsx"
}
route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import { Signup } from "@src/pages/auth.jsx"
}
route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage }
page RequestPasswordResetPage {
component: import { RequestPasswordReset } from "@src/pages/auth.jsx",
}
route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage }
page PasswordResetPage {
component: import { PasswordReset } from "@src/pages/auth.jsx",
}
route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage }
page EmailVerificationPage {
component: import { EmailVerification } from "@src/pages/auth.jsx",
}
// ...
route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import { Login } from "@src/pages/auth.tsx"
}
route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import { Signup } from "@src/pages/auth.tsx"
}
route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage }
page RequestPasswordResetPage {
component: import { RequestPasswordReset } from "@src/pages/auth.tsx",
}
route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage }
page PasswordResetPage {
component: import { PasswordReset } from "@src/pages/auth.tsx",
}
route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage }
page EmailVerificationPage {
component: import { EmailVerification } from "@src/pages/auth.tsx",
}
We'll define the React components for these pages in the src/pages/auth.tsx
file below.
4. Create the Client Pages
We are using Tailwind CSS to style the pages. Read more about how to add it here.
Let's create a auth.tsx
file in the src/pages
folder and add the following to it:
- JavaScript
- TypeScript
import {
LoginForm,
SignupForm,
VerifyEmailForm,
ForgotPasswordForm,
ResetPasswordForm,
} from 'wasp/client/auth'
import { Link } from 'react-router-dom'
export function Login() {
return (
<Layout>
<LoginForm />
<br />
<span className="text-sm font-medium text-gray-900">
Don't have an account yet? <Link to="/signup">go to signup</Link>.
</span>
<br />
<span className="text-sm font-medium text-gray-900">
Forgot your password? <Link to="/request-password-reset">reset it</Link>.
</span>
</Layout>
)
}
export function Signup() {
return (
<Layout>
<SignupForm />
<br />
<span className="text-sm font-medium text-gray-900">
I already have an account (<Link to="/login">go to login</Link>).
</span>
</Layout>
)
}
export function EmailVerification() {
return (
<Layout>
<VerifyEmailForm />
<br />
<span className="text-sm font-medium text-gray-900">
If everything is okay, <Link to="/login">go to login</Link>
</span>
</Layout>
)
}
export function RequestPasswordReset() {
return (
<Layout>
<ForgotPasswordForm />
</Layout>
)
}
export function PasswordReset() {
return (
<Layout>
<ResetPasswordForm />
<br />
<span className="text-sm font-medium text-gray-900">
If everything is okay, <Link to="/login">go to login</Link>
</span>
</Layout>
)
}
// A layout component to center the content
export function Layout({ children }) {
return (
<div className="h-full w-full bg-white">
<div className="flex min-h-[75vh] min-w-full items-center justify-center">
<div className="h-full w-full max-w-sm bg-white p-5">
<div>{children}</div>
</div>
</div>
</div>
)
}
import {
LoginForm,
SignupForm,
VerifyEmailForm,
ForgotPasswordForm,
ResetPasswordForm,
} from 'wasp/client/auth'
import { Link } from 'react-router-dom'
export function Login() {
return (
<Layout>
<LoginForm />
<br />
<span className="text-sm font-medium text-gray-900">
Don't have an account yet? <Link to="/signup">go to signup</Link>.
</span>
<br />
<span className="text-sm font-medium text-gray-900">
Forgot your password? <Link to="/request-password-reset">reset it</Link>.
</span>
</Layout>
)
}
export function Signup() {
return (
<Layout>
<SignupForm />
<br />
<span className="text-sm font-medium text-gray-900">
I already have an account (<Link to="/login">go to login</Link>).
</span>
</Layout>
)
}
export function EmailVerification() {
return (
<Layout>
<VerifyEmailForm />
<br />
<span className="text-sm font-medium text-gray-900">
If everything is okay, <Link to="/login">go to login</Link>
</span>
</Layout>
)
}
export function RequestPasswordReset() {
return (
<Layout>
<ForgotPasswordForm />
</Layout>
)
}
export function PasswordReset() {
return (
<Layout>
<ResetPasswordForm />
<br />
<span className="text-sm font-medium text-gray-900">
If everything is okay, <Link to="/login">go to login</Link>
</span>
</Layout>
)
}
// A layout component to center the content
export function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="h-full w-full bg-white">
<div className="flex min-h-[75vh] min-w-full items-center justify-center">
<div className="h-full w-full max-w-sm bg-white p-5">
<div>{children}</div>
</div>
</div>
</div>
)
}
We imported the generated Auth UI components and used them in our pages. Read more about the Auth UI components here.
5. Set up an Email Sender
To support e-mail verification and password reset flows, we need an e-mail sender. Luckily, Wasp supports several email providers out of the box.
We'll use the Dummy
provider to speed up the setup. It just logs the emails to the console instead of sending them. You can use any of the supported email providers.
To set up the Dummy
provider to send emails, add the following to the main.wasp
file:
- JavaScript
- TypeScript
app myApp {
// ...
// 7. Set up the email sender
emailSender: {
provider: Dummy,
}
}
app myApp {
// ...
// 7. Set up the email sender
emailSender: {
provider: Dummy,
}
}
Conclusion
That's it! We have set up email authentication in our app. 🎉
Running wasp db migrate-dev
and then wasp start
should give you a working app with email authentication. If you want to put some of the pages behind authentication, read the auth overview.
Login and Signup Flows
Login
Signup
Some of the behavior you get out of the box:
- Rate limiting
We are limiting the rate of sign-up requests to 1 request per minute per email address. This is done to prevent spamming.
- Preventing user email leaks
If somebody tries to signup with an email that already exists and it's verified, we pretend that the account was created instead of saying it's an existing account. This is done to prevent leaking the user's email address.
- Allowing registration for unverified emails
If a user tries to register with an existing but unverified email, we'll allow them to do that. This is done to prevent bad actors from locking out other users from registering with their email address.
- Password validation
Read more about the default password validation rules and how to override them in auth overview docs.
Email Verification Flow
In development mode, you can skip the email verification step by setting the SKIP_EMAIL_VERIFICATION_IN_DEV
environment variable to true
in your .env.server
file:
SKIP_EMAIL_VERIFICATION_IN_DEV=true
This is useful when you are developing your app and don't want to go through the email verification flow every time you sign up. It can be also useful when you are writing automated tests for your app.
By default, Wasp requires the e-mail to be verified before allowing the user to log in. This is done by sending a verification email to the user's email address and requiring the user to click on a link in the email to verify their email address.
Our setup looks like this:
- JavaScript
- TypeScript
// ...
emailVerification: {
clientRoute: EmailVerificationRoute,
}
// ...
emailVerification: {
clientRoute: EmailVerificationRoute,
}
When the user receives an e-mail, they receive a link that goes to the client route specified in the clientRoute
field. In our case, this is the EmailVerificationRoute
route we defined in the main.wasp
file.
The content of the e-mail can be customized, read more about it here.
Email Verification Page
We defined our email verification page in the auth.tsx
file.
Password Reset Flow
Users can request a password and then they'll receive an e-mail with a link to reset their password.
Some of the behavior you get out of the box:
- Rate limiting
We are limiting the rate of sign-up requests to 1 request per minute per email address. This is done to prevent spamming.
- Preventing user email leaks
If somebody requests a password reset with an unknown email address, we'll give back the same response as if the user requested a password reset successfully. This is done to prevent leaking information.
Our setup in main.wasp
looks like this:
- JavaScript
- TypeScript
// ...
passwordReset: {
clientRoute: PasswordResetRoute,
}
// ...
passwordReset: {
clientRoute: PasswordResetRoute,
}
Request Password Reset Page
Users request their password to be reset by going to the /request-password-reset
route. We defined our request password reset page in the auth.tsx
file.
Password Reset Page
When the user receives an e-mail, they receive a link that goes to the client route specified in the clientRoute
field. In our case, this is the PasswordResetRoute
route we defined in the main.wasp
file.
Users can enter their new password there.
The content of the e-mail can be customized, read more about it here.
Creating a Custom Sign-up Action
We don't recommend creating a custom sign-up action unless you have a good reason to do so. It is a complex process and you can easily make a mistake that will compromise the security of your app.
The code of your custom sign-up action can look like this:
- JavaScript
- TypeScript
// ...
action customSignup {
fn: import { signup } from "@src/auth/signup.js",
}
import {
ensurePasswordIsPresent,
ensureValidPassword,
ensureValidEmail,
createProviderId,
sanitizeAndSerializeProviderData,
deserializeAndSanitizeProviderData,
findAuthIdentity,
createUser,
createEmailVerificationLink,
sendEmailVerificationEmail,
} from 'wasp/server/auth'
export const signup = async (args, _context) => {
ensureValidEmail(args)
ensurePasswordIsPresent(args)
ensureValidPassword(args)
try {
const providerId = createProviderId('email', args.email)
const existingAuthIdentity = await findAuthIdentity(providerId)
if (existingAuthIdentity) {
const providerData = deserializeAndSanitizeProviderData(existingAuthIdentity.providerData)
// Your custom code here
} else {
// sanitizeAndSerializeProviderData will hash the user's password
const newUserProviderData = await sanitizeAndSerializeProviderData({
hashedPassword: args.password,
isEmailVerified: false,
emailVerificationSentAt: null,
passwordResetSentAt: null,
})
await createUser(
providerId,
providerData,
// Any additional data you want to store on the User entity
{},
)
// Verification link links to a client route e.g. /email-verification
const verificationLink = await createEmailVerificationLink(args.email, '/email-verification');
try {
await sendEmailVerificationEmail(
args.email,
{
from: {
name: "My App Postman",
email: "[email protected]",
},
to: args.email,
subject: "Verify your email",
text: `Click the link below to verify your email: ${verificationLink}`,
html: `
<p>Click the link below to verify your email</p>
<a href="${verificationLink}">Verify email</a>
`,
}
);
} catch (e: unknown) {
console.error("Failed to send email verification email:", e);
throw new HttpError(500, "Failed to send email verification email.");
}
}
} catch (e) {
return {
success: false,
message: e.message,
}
}
// Your custom code after sign-up.
// ...
return {
success: true,
message: 'User created successfully',
}
}
// ...
action customSignup {
fn: import { signup } from "@src/auth/signup.js",
}
import {
ensurePasswordIsPresent,
ensureValidPassword,
ensureValidEmail,
createProviderId,
sanitizeAndSerializeProviderData,
deserializeAndSanitizeProviderData,
findAuthIdentity,
createUser,
createEmailVerificationLink,
sendEmailVerificationEmail,
} from 'wasp/server/auth'
import type { CustomSignup } from 'wasp/server/operations'
type CustomSignupInput = {
email: string
password: string
}
type CustomSignupOutput = {
success: boolean
message: string
}
export const signup: CustomSignup<
CustomSignupInput,
CustomSignupOutput
> = async (args, _context) => {
ensureValidEmail(args)
ensurePasswordIsPresent(args)
ensureValidPassword(args)
try {
const providerId = createProviderId('email', args.email)
const existingAuthIdentity = await findAuthIdentity(providerId)
if (existingAuthIdentity) {
const providerData = deserializeAndSanitizeProviderData<'email'>(
existingAuthIdentity.providerData
)
// Your custom code here
} else {
// sanitizeAndSerializeProviderData will hash the user's password
const newUserProviderData =
await sanitizeAndSerializeProviderData<'email'>({
hashedPassword: args.password,
isEmailVerified: false,
emailVerificationSentAt: null,
passwordResetSentAt: null,
})
await createUser(
providerId,
providerData,
// Any additional data you want to store on the User entity
{}
)
// Verification link links to a client route e.g. /email-verification
const verificationLink = await createEmailVerificationLink(
args.email,
'/email-verification'
)
try {
await sendEmailVerificationEmail(args.email, {
from: {
name: 'My App Postman',
email: '[email protected]',
},
to: args.email,
subject: 'Verify your email',
text: `Click the link below to verify your email: ${verificationLink}`,
html: `
<p>Click the link below to verify your email</p>
<a href="${verificationLink}">Verify email</a>
`,
})
} catch (e: unknown) {
console.error('Failed to send email verification email:', e)
throw new HttpError(500, 'Failed to send email verification email.')
}
}
} catch (e) {
return {
success: false,
message: e.message,
}
}
// Your custom code after sign-up.
// ...
return {
success: true,
message: 'User created successfully',
}
}
We suggest using the built-in field validators for your authentication flow. You can import them from wasp/server/auth
. These are the same validators that Wasp uses internally for the default authentication flow.
Email
ensureValidEmail(args)
Checks if the email is valid and throws an error if it's not. Read more about the validation rules here.
Password
ensurePasswordIsPresent(args)
Checks if the password is present and throws an error if it's not.
ensureValidPassword(args)
Checks if the password is valid and throws an error if it's not. Read more about the validation rules here.
Using Auth
To read more about how to set up the logout button and how to get access to the logged-in user in our client and server code, read the auth overview docs.
When you receive the user
object on the client or the server, you'll be able to access the user's email and other information like this:
const emailIdentity = user.identities.email
// Email address the the user used to sign up, e.g. "[email protected]".
emailIdentity.id
// `true` if the user has verified their email address.
emailIdentity.isEmailVerified
// Datetime when the email verification email was sent.
emailIdentity.emailVerificationSentAt
// Datetime when the last password reset email was sent.
emailIdentity.passwordResetSentAt
Read more about accessing the user data in the Accessing User Data section of the docs.
API Reference
Let's go over the options we can specify when using email authentication.
userEntity
fields
- JavaScript
- TypeScript
app myApp {
title: "My app",
// ...
auth: {
userEntity: User,
methods: {
email: {
// We'll explain these options below
},
},
onAuthFailedRedirectTo: "/someRoute"
},
// ...
}
model User {
id Int @id @default(autoincrement())
}
app myApp {
title: "My app",
// ...
auth: {
userEntity: User,
methods: {
email: {
// We'll explain these options below
},
},
onAuthFailedRedirectTo: "/someRoute"
},
// ...
}
model User {
id Int @id @default(autoincrement())
}
The user entity needs to have the following fields:
id
requiredIt can be of any type, but it needs to be marked with
@id
You can add any other fields you want to the user entity. Make sure to also define them in the userSignupFields
field if they need to be set during the sign-up process.
Fields in the email
dict
- JavaScript
- TypeScript
app myApp {
title: "My app",
// ...
auth: {
userEntity: User,
methods: {
email: {
userSignupFields: import { userSignupFields } from "@src/auth.js",
fromField: {
name: "My App",
email: "[email protected]"
},
emailVerification: {
clientRoute: EmailVerificationRoute,
getEmailContentFn: import { getVerificationEmailContent } from "@src/auth/email.js",
},
passwordReset: {
clientRoute: PasswordResetRoute,
getEmailContentFn: import { getPasswordResetEmailContent } from "@src/auth/email.js",
},
},
},
onAuthFailedRedirectTo: "/someRoute"
},
// ...
}
app myApp {
title: "My app",
// ...
auth: {
userEntity: User,
methods: {
email: {
userSignupFields: import { userSignupFields } from "@src/auth.js",
fromField: {
name: "My App",
email: "[email protected]"
},
emailVerification: {
clientRoute: EmailVerificationRoute,
getEmailContentFn: import { getVerificationEmailContent } from "@src/auth/email.js",
},
passwordReset: {
clientRoute: PasswordResetRoute,
getEmailContentFn: import { getPasswordResetEmailContent } from "@src/auth/email.js",
},
},
},
onAuthFailedRedirectTo: "/someRoute"
},
// ...
}
userSignupFields: ExtImport
userSignupFields
defines all the extra fields that need to be set on the User
during the sign-up process. For example, if you have address
and phone
fields on your User
entity, you can set them by defining the userSignupFields
like this:
- JavaScript
- TypeScript
import { defineUserSignupFields } from 'wasp/server/auth'
export const userSignupFields = defineUserSignupFields({
address: (data) => {
if (!data.address) {
throw new Error('Address is required')
}
return data.address
}
phone: (data) => data.phone,
})
import { defineUserSignupFields } from 'wasp/server/auth'
export const userSignupFields = defineUserSignupFields({
address: (data) => {
if (!data.address) {
throw new Error('Address is required')
}
return data.address
}
phone: (data) => data.phone,
})
Read more about the userSignupFields
function here.
fromField: EmailFromField
required
fromField
is a dict that specifies the name and e-mail address of the sender of the e-mails sent by your app.
It has the following fields:
name
: name of the senderemail
: e-mail address of the sender required
emailVerification: EmailVerificationConfig
required
emailVerification
is a dict that specifies the details of the e-mail verification process.
It has the following fields:
clientRoute: Route
: a route that is used for the user to verify their e-mail address. requiredClient route should handle the process of taking a token from the URL and sending it to the server to verify the e-mail address. You can use our
verifyEmail
action for that.- JavaScript
- TypeScript
src/pages/EmailVerificationPage.jsximport { verifyEmail } from 'wasp/client/auth'
...
await verifyEmail({ token });src/pages/EmailVerificationPage.tsximport { verifyEmail } from 'wasp/client/auth'
...
await verifyEmail({ token });noteWe used Auth UI above to avoid doing this work of sending the token to the server manually.
getEmailContentFn: ExtImport
: a function that returns the content of the e-mail that is sent to the user.Defining
getEmailContentFn
can be done by defining a file in thesrc
directory.This is the default content of the e-mail, you can customize it to your liking.- JavaScript
- TypeScript
src/email.jsexport const getVerificationEmailContent = ({ verificationLink }) => ({
subject: 'Verify your email',
text: `Click the link below to verify your email: ${verificationLink}`,
html: `
<p>Click the link below to verify your email</p>
<a href="${verificationLink}">Verify email</a>
`,
})src/email.tsimport { GetVerificationEmailContentFn } from 'wasp/server/auth'
export const getVerificationEmailContent: GetVerificationEmailContentFn = ({
verificationLink,
}) => ({
subject: 'Verify your email',
text: `Click the link below to verify your email: ${verificationLink}`,
html: `
<p>Click the link below to verify your email</p>
<a href="${verificationLink}">Verify email</a>
`,
})
passwordReset: PasswordResetConfig
required
passwordReset
is a dict that specifies the password reset process.
It has the following fields:
clientRoute: Route
: a route that is used for the user to reset their password. requiredClient route should handle the process of taking a token from the URL and a new password from the user and sending it to the server. You can use our
requestPasswordReset
andresetPassword
actions to do that.- JavaScript
- TypeScript
src/pages/ForgotPasswordPage.jsximport { requestPasswordReset } from 'wasp/client/auth'
...
await requestPasswordReset({ email });src/pages/PasswordResetPage.jsximport { resetPassword } from 'wasp/client/auth'
...
await resetPassword({ password, token })src/pages/ForgotPasswordPage.tsximport { requestPasswordReset } from 'wasp/client/auth'
...
await requestPasswordReset({ email });src/pages/PasswordResetPage.tsximport { resetPassword } from 'wasp/client/auth'
...
await resetPassword({ password, token })noteWe used Auth UI above to avoid doing this work of sending the password request and the new password to the server manually.
getEmailContentFn: ExtImport
: a function that returns the content of the e-mail that is sent to the user.Defining
getEmailContentFn
is done by defining a function that looks like this:This is the default content of the e-mail, you can customize it to your liking.- JavaScript
- TypeScript
src/email.jsexport const getPasswordResetEmailContent = ({ passwordResetLink }) => ({
subject: 'Password reset',
text: `Click the link below to reset your password: ${passwordResetLink}`,
html: `
<p>Click the link below to reset your password</p>
<a href="${passwordResetLink}">Reset password</a>
`,
})src/email.tsimport { GetPasswordResetEmailContentFn } from 'wasp/server/auth'
export const getPasswordResetEmailContent: GetPasswordResetEmailContentFn = ({
passwordResetLink,
}) => ({
subject: 'Password reset',
text: `Click the link below to reset your password: ${passwordResetLink}`,
html: `
<p>Click the link below to reset your password</p>
<a href="${passwordResetLink}">Reset password</a>
`,
})