Overview
Auth is an essential piece of any serious application. That's why Wasp provides authentication and authorization support out of the box.
Here's a 1-minute tour of how full-stack auth works in Wasp:
Enabling auth for your app is optional and can be done by configuring the auth field of your app declaration:
- JavaScript
- TypeScript
appMyApp{
title: "My app",
//...
auth: {
userEntity: User,
methods: {
usernameAndPassword: {},// use this or email, not both
email: {},// use this or usernameAndPassword, not both
google: {},
gitHub: {},
},
onAuthFailedRedirectTo: "/someRoute"
}
}
//...
appMyApp{
title: "My app",
//...
auth: {
userEntity: User,
methods: {
usernameAndPassword: {},// use this or email, not both
email: {},// use this or usernameAndPassword, not both
google: {},
gitHub: {},
},
onAuthFailedRedirectTo: "/someRoute"
}
}
//...
Read more about the auth field options in the API Reference section.
We will provide a quick overview of auth in Wasp and link to more detailed documentation for each auth method.
Available auth methodsβ
Wasp supports the following auth methods:
Email Β»
Email verification, password reset, etc.
Username & Password Β»
The simplest way to get started
Google Β»
Users sign in with their Google account
Github Β»
Users sign in with their Github account
Keycloak Β»
Users sign in with their Keycloak account
Discord Β»
Users sign in with their Discord account
Click on each auth method for more details.
Let's say we enabled the Username & password authentication.
We get an auth backend with signup and login endpoints. We also get the user object in our Operations and we can decide what to do based on whether the user is logged in or not.
We would also get the Auth UI generated for us. We can set up our login and signup pages where our users can create their account and login. We can then protect certain pages by setting authRequired: true for them. This will make sure that only logged-in users can access them.
We will also have access to the user object in our frontend code, so we can show different UI to logged-in and logged-out users. For example, we can show the user's name in the header alongside a logout button or a login button if the user is not logged in.
Protecting a page with authRequiredβ
When declaring a page, you can set the authRequired property.
If you set it to true, only authenticated users can access the page. Unauthenticated users are redirected to a route defined by the app.auth.onAuthFailedRedirectTo field.
- JavaScript
- TypeScript
pageMainPage{
component: importMainfrom"@src/pages/Main",
authRequired: true
}
pageMainPage{
component: importMainfrom"@src/pages/Main",
authRequired: true
}
You can only use authRequired if your app uses one of the available auth methods.
If authRequired is set to true, the page's React component (specified by the component property) receives the user object as a prop. Read more about the user object in the Accessing the logged-in user section.
Logout actionβ
We provide an action for logging out the user. Here's how you can use it:
- JavaScript
- TypeScript
import{ logout }from'wasp/client/auth'
constLogoutButton=()=>{
return<buttononClick={logout}>Logout</button>
}
import{ logout }from'wasp/client/auth'
constLogoutButton=()=>{
return<buttononClick={logout}>Logout</button>
}
Accessing the logged-in userβ
You can get access to the user object both on the server and on the client. The user object contains the logged-in user's data.
The user object has all the fields that you defined in your User entity. In addition to that, it will also contain all the auth-related fields that Wasp stores. This includes things like the username or the email verification status. For example, if you have a user that signed up using an email and password, the user object might look like this:
const user ={
// User data
id:"cluqsex9500017cn7i2hwsg17",
address:"Some address",
// Auth methods specific data
identities:{
email:{
id:"[email protected]",
isEmailVerified:true,
emailVerificationSentAt:"2024εΉ΄04ζ08ζ₯T10:06:02.204Z",
passwordResetSentAt:null,
},
},
}
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.
On the clientβ
There are two ways to access the user object on the client:
- the
userprop - the
useAuthhook
Using the user propβ
If the page's declaration sets authRequired to true, the page's React component receives the user object as a prop:
- JavaScript
- TypeScript
// ...
pageAccountPage{
component: importAccountfrom"@src/pages/Account",
authRequired: true
}
importButtonfrom'./Button'
import{ logout }from'wasp/client/auth'
constAccountPage=({ user })=>{
return(
<div>
<ButtononClick={logout}>Logout</Button>
{JSON.stringify(user,null,2)}
</div>
)
}
exportdefaultAccountPage
// ...
pageAccountPage{
component: importAccountfrom"@src/pages/Account",
authRequired: true
}
import{typeAuthUser}from'wasp/auth'
importButtonfrom'./Button'
import{ logout }from'wasp/client/auth'
constAccountPage=({ user }:{ user:AuthUser})=>{
return(
<div>
<ButtononClick={logout}>Logout</Button>
{JSON.stringify(user,null,2)}
</div>
)
}
exportdefaultAccountPage
Using the useAuth hookβ
Wasp provides a React hook you can use in the client components - useAuth.
This hook is a thin wrapper over Wasp's useQuery hook and returns data in the same format.
- JavaScript
- TypeScript
import{ useAuth, logout }from'wasp/client/auth'
import{Link}from'react-router-dom'
importTodofrom'../Todo'
exportfunctionMain(){
const{data: user }=useAuth()
if(!user){
return(
<span>
Please <Linkto="/login">login</Link> or{' '}
<Linkto="/signup">sign up</Link>.
</span>
)
}else{
return(
<>
<buttononClick={logout}>Logout</button>
<Todo/>
</>
)
}
}
import{ useAuth, logout }from'wasp/client/auth'
import{Link}from'react-router-dom'
importTodofrom'../Todo'
exportfunctionMain(){
const{ data: user }=useAuth()
if(!user){
return(
<span>
Please <Linkto='/login'>login</Link> or <Linkto='/signup'>sign up</Link>.
</span>
)
}else{
return(
<>
<buttononClick={logout}>Logout</button>
<Todo/>
< />
)
}
}
Since the user prop is only available in a page's React component: use the user prop in the page's React component and the useAuth hook in any other React component.
On the serverβ
Using the context.user objectβ
When authentication is enabled, all queries and actions have access to the user object through the context argument. context.user contains all User entity's fields and the auth identities connected to the user. We strip out the hashedPassword field from the identities for security reasons.
- JavaScript
- TypeScript
import{HttpError}from'wasp/server'
exportconstcreateTask=async(task, context)=>{
if(!context.user){
thrownewHttpError(403)
}
constTask= context.entities.Task
returnTask.create({
data:{
description: task.description,
user:{
connect:{id: context.user.id},
},
},
})
}
import{typeTask}from'wasp/entities'
import{typeCreateTask}from'wasp/server/operations'
import{ HttpError }from'wasp/server'
typeCreateTaskPayload= Pick<Task,'description'>
exportconst createTask: CreateTask<CreateTaskPayload, Task>=async(
args,
context
)=>{
if(!context.user){
thrownewHttpError(403)
}
const Task = context.entities.Task
return Task.create({
data:{
description: args.description,
user:{
connect:{ id: context.user.id },
},
},
})
}
To implement access control in your app, each operation must check context.user and decide what to do. For example, if context.user is undefined inside a private operation, the user's access should be denied.
When using WebSockets, the user object is also available on the socket.data object. Read more in the WebSockets section.
Sessionsβ
Wasp's auth uses sessions to keep track of the logged-in user. The session is stored in localStorage on the client and in the database on the server. Under the hood, Wasp uses the excellent Lucia Auth v3 library for session management.
When users log in, Wasp creates a session for them and stores it in the database. The session is then sent to the client and stored in localStorage. When users log out, Wasp deletes the session from the database and from localStorage.
User Entityβ
Password Hashingβ
If you are saving a user's password in the database, you should never save it as plain text. You can use Wasp's helper functions for serializing and deserializing provider data which will automatically hash the password for you:
// ...
actionupdatePassword{
fn: import{ updatePassword }from"@src/auth",
}
- JavaScript
- TypeScript
import{
createProviderId,
findAuthIdentity,
updateAuthIdentityProviderData,
deserializeAndSanitizeProviderData,
}from'wasp/server/auth';
exportconstupdatePassword=async(args, context)=>{
const providerId =createProviderId('email', args.email)
const authIdentity =awaitfindAuthIdentity(providerId)
if(!authIdentity){
thrownewHttpError(400,"Unknown user")
}
const providerData =deserializeAndSanitizeProviderData(authIdentity.providerData)
// Updates the password and hashes it automatically.
awaitupdateAuthIdentityProviderData(providerId, providerData,{
hashedPassword: args.password,
})
}
import{
createProviderId,
findAuthIdentity,
updateAuthIdentityProviderData,
deserializeAndSanitizeProviderData,
}from'wasp/server/auth';
import{typeUpdatePassword}from'wasp/server/operations'
exportconst updatePassword: UpdatePassword<
{ email:string; password:string},
void,
>=async(args, context)=>{
const providerId =createProviderId('email', args.email)
const authIdentity =awaitfindAuthIdentity(providerId)
if(!authIdentity){
thrownewHttpError(400,"Unknown user")
}
const providerData =deserializeAndSanitizeProviderData<'email'>(authIdentity.providerData)
// Updates the password and hashes it automatically.
awaitupdateAuthIdentityProviderData(providerId, providerData,{
hashedPassword: args.password,
})
}
Default Validationsβ
When you are using the default authentication flow, Wasp validates the fields with some default validations. These validations run if you use Wasp's built-in Auth UI or if you use the provided auth actions.
If you decide to create your custom auth actions, you'll need to run the validations yourself.
Default validations depend on the auth method you use.
Username & Passwordβ
If you use Username & password authentication, the default validations are:
- The
usernamemust not be empty - The
passwordmust not be empty, have at least 8 characters, and contain a number
Note that usernames are stored in a case-insensitive manner.
Emailβ
If you use Email authentication, the default validations are:
- The
emailmust not be empty and a valid email address - The
passwordmust not be empty, have at least 8 characters, and contain a number
Note that emails are stored in a case-insensitive manner.
Customizing the Signup Processβ
Sometimes you want to include extra fields in your signup process, like first name and last name and save them in the User entity.
For this to happen:
- you need to define the fields that you want saved in the database,
- you need to customize the
SignupForm(in the case of Email or Username & Password auth)
Other times, you might need to just add some extra UI elements to the form, like a checkbox for terms of service. In this case, customizing only the UI components is enough.
Let's see how to do both.
1. Defining Extra Fieldsβ
If we want to save some extra fields in our signup process, we need to tell our app they exist.
We do that by defining an object where the keys represent the field name, and the values are functions that receive the data sent from the client* and return the value of the field.
* We exclude the password field from this object to prevent it from being saved as plain-text in the database. The password field is handled by Wasp's auth backend.
First, we add the auth.methods.{authMethod}.userSignupFields field in our main.wasp file. The {authMethod} depends on the auth method you are using.
For example, if you are using Username & Password, you would add the auth.methods.usernameAndPassword.userSignupFields field:
- JavaScript
- TypeScript
appcrudTesting{
// ...
auth: {
userEntity: User,
methods: {
usernameAndPassword: {
userSignupFields: import{ userSignupFields }from"@src/auth/signup",
},
},
onAuthFailedRedirectTo: "/login",
},
}
model User{
id Int@id@default(autoincrement())
address String?
}
Then we'll define the userSignupFields object in the src/auth/signup.js file:
import{ defineUserSignupFields }from'wasp/server/auth'
exportconst userSignupFields =defineUserSignupFields({
address:async(data)=>{
const address = data.address
if(typeof address !=='string'){
thrownewError('Address is required')
}
if(address.length <5){
thrownewError('Address must be at least 5 characters long')
}
return address
},
})
appcrudTesting{
// ...
auth: {
userEntity: User,
methods: {
usernameAndPassword: {
userSignupFields: import{ userSignupFields }from"@src/auth/signup",
},
},
onAuthFailedRedirectTo: "/login",
},
}
model User{
id Int@id@default(autoincrement())
address String?
}
Then we'll define the userSignupFields object in the src/auth/signup.js file:
import{ defineUserSignupFields }from'wasp/server/auth'
exportconst userSignupFields =defineUserSignupFields({
address:async(data)=>{
const address = data.address
if(typeof address !=='string'){
thrownewError('Address is required')
}
if(address.length <5){
thrownewError('Address must be at least 5 characters long')
}
return address
},
})
Read more about the userSignupFields object in the API Reference.
Keep in mind, that these field names need to exist on the userEntity you defined in your main.wasp file e.g. address needs to be a field on the User entity you defined in the schema.prisma file.
The field function will receive the data sent from the client and it needs to return the value that will be saved into the database. If the field is invalid, the function should throw an error.
You can use any validation library you want to validate the fields. For example, you can use zod like this:
Click to see the code
- JavaScript
- TypeScript
import{ defineUserSignupFields }from'wasp/server/auth'
import*as zfrom'zod'
exportconst userSignupFields =defineUserSignupFields({
address:(data)=>{
constAddressSchema= z
.string({
required_error:'Address is required',
invalid_type_error:'Address must be a string',
})
.min(10,'Address must be at least 10 characters long')
const result =AddressSchema.safeParse(data.address)
if(result.success===false){
thrownewError(result.error.issues[0].message)
}
return result.data
},
})
import{ defineUserSignupFields }from'wasp/server/auth'
import*as z from'zod'
exportconst userSignupFields =defineUserSignupFields({
address:(data)=>{
const AddressSchema = z
.string({
required_error:'Address is required',
invalid_type_error:'Address must be a string',
})
.min(10,'Address must be at least 10 characters long')
const result = AddressSchema.safeParse(data.address)
if(result.success ===false){
thrownewError(result.error.issues[0].message)
}
return result.data
},
})
Now that we defined the fields, Wasp knows how to:
- Validate the data sent from the client
- Save the data to the database
Next, let's see how to customize Auth UI to include those fields.
2. Customizing the Signup Componentβ
If you are using Wasp's Auth UI, you can customize the SignupForm component by passing the additionalFields prop to it. It can be either a list of extra fields or a render function.
Using a List of Extra Fieldsβ
When you pass in a list of extra fields to the SignupForm, they are added to the form one by one, in the order you pass them in.
Inside the list, there can be either objects or render functions (you can combine them):
- Objects are a simple way to describe new fields you need, but a bit less flexible than render functions.
- Render functions can be used to render any UI you want, but they require a bit more code. The render functions receive the
react-hook-formobject and the form state object as arguments.
- JavaScript
- TypeScript
import{
SignupForm,
FormError,
FormInput,
FormItemGroup,
FormLabel,
}from'wasp/client/auth'
exportconstSignupPage=()=>{
return(
<SignupForm
additionalFields={[
/* The address field is defined using an object */
{
name:'address',
label:'Address',
type:'input',
validations:{
required:'Address is required',
},
},
/* The phone number is defined using a render function */
(form, state)=>{
return(
<FormItemGroup>
<FormLabel>Phone Number</FormLabel>
<FormInput
{...form.register('phoneNumber',{
required:'Phone number is required',
})}
disabled={state.isLoading}
/>
{form.formState.errors.phoneNumber&&(
<FormError>
{form.formState.errors.phoneNumber.message}
</FormError>
)}
</FormItemGroup>
)
},
]}
/>
)
}
import{
SignupForm,
FormError,
FormInput,
FormItemGroup,
FormLabel,
}from'wasp/client/auth'
exportconstSignupPage=()=>{
return(
<SignupForm
additionalFields={[
/* The address field is defined using an object */
{
name:'address',
label:'Address',
type:'input',
validations:{
required:'Address is required',
},
},
/* The phone number is defined using a render function */
(form, state)=>{
return(
<FormItemGroup>
<FormLabel>Phone Number</FormLabel>
<FormInput
{...form.register('phoneNumber',{
required:'Phone number is required',
})}
disabled={state.isLoading}
/>
{form.formState.errors.phoneNumber&&(
<FormError>
{form.formState.errors.phoneNumber.message}
</FormError>
)}
</FormItemGroup>
)
},
]}
/>
)
}
Read more about the extra fields in the API Reference.
Using a Single Render Functionβ
Instead of passing in a list of extra fields, you can pass in a render function which will receive the react-hook-form object and the form state object as arguments. What ever the render function returns, will be rendered below the default fields.
- JavaScript
- TypeScript
import{SignupForm,FormItemGroup}from'wasp/client/auth'
exportconstSignupPage=()=>{
return(
<SignupForm
additionalFields={(form, state)=>{
const username = form.watch('username')
return(
username &&(
<FormItemGroup>
Hello there <strong>{username}</strong> π
</FormItemGroup>
)
)
}}
/>
)
}
import{SignupForm,FormItemGroup}from'wasp/client/auth'
exportconstSignupPage=()=>{
return(
<SignupForm
additionalFields={(form, state)=>{
const username = form.watch('username')
return(
username &&(
<FormItemGroup>
Hello there <strong>{username}</strong> π
</FormItemGroup>
)
)
}}
/>
)
}
Read more about the render function in the API Reference.
API Referenceβ
Auth Fieldsβ
- JavaScript
- TypeScript
title: "My app",
//...
auth: {
userEntity: User,
methods: {
usernameAndPassword: {},// use this or email, not both
email: {},// use this or usernameAndPassword, not both
google: {},
gitHub: {},
},
onAuthFailedRedirectTo: "/someRoute",
}
}
//...
appMyApp{
title: "My app",
//...
auth: {
userEntity: User,
methods: {
usernameAndPassword: {},// use this or email, not both
email: {},// use this or usernameAndPassword, not both
google: {},
gitHub: {},
},
onAuthFailedRedirectTo: "/someRoute",
}
}
//...
app.auth is a dictionary with the following fields:
userEntity: entity requiredβ
The entity representing the user connected to your business logic.
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.
methods: dict requiredβ
A dictionary of auth methods enabled for the app.
Email Β»
Email verification, password reset, etc.
Username & Password Β»
The simplest way to get started
Google Β»
Users sign in with their Google account
Github Β»
Users sign in with their Github account
Keycloak Β»
Users sign in with their Keycloak account
Discord Β»
Users sign in with their Discord account
Click on each auth method for more details.
onAuthFailedRedirectTo: String requiredβ
The route to which Wasp should redirect unauthenticated user when they try to access a private page (i.e., a page that has authRequired: true).
Check out these essential docs on auth to see an example of usage.
onAuthSucceededRedirectTo: Stringβ
The route to which Wasp will send a successfully authenticated after a successful login/signup.
The default value is "/".
Automatic redirect on successful login only works when using the Wasp-provided Auth UI.
Signup Fields Customizationβ
If you want to add extra fields to the signup process, the server needs to know how to save them to the database. You do that by defining the auth.methods.{authMethod}.userSignupFields field in your main.wasp file.
- JavaScript
- TypeScript
appcrudTesting{
// ...
auth: {
userEntity: User,
methods: {
usernameAndPassword: {
userSignupFields: import{ userSignupFields }from"@src/auth/signup",
},
},
onAuthFailedRedirectTo: "/login",
},
}
Then we'll export the userSignupFields object from the src/auth/signup.js file:
import{ defineUserSignupFields }from'wasp/server/auth'
exportconst userSignupFields =defineUserSignupFields({
address:async(data)=>{
const address = data.address
if(typeof address !=='string'){
thrownewError('Address is required')
}
if(address.length <5){
thrownewError('Address must be at least 5 characters long')
}
return address
},
})
appcrudTesting{
// ...
auth: {
userEntity: User,
methods: {
usernameAndPassword: {
userSignupFields: import{ userSignupFields }from"@src/auth/signup",
},
},
onAuthFailedRedirectTo: "/login",
},
}
Then we'll export the userSignupFields object from the src/auth/signup.ts file:
import{ defineUserSignupFields }from'wasp/server/auth'
exportconst userSignupFields =defineUserSignupFields({
address:async(data)=>{
const address = data.address
if(typeof address !=='string'){
thrownewError('Address is required')
}
if(address.length <5){
thrownewError('Address must be at least 5 characters long')
}
return address
},
})
The userSignupFields object is an object where the keys represent the field name, and the values are functions that receive the data sent from the client* and return the value of the field.
If the value that the function received is invalid, the function should throw an error.
* We exclude the password field from this object to prevent it from being saved as plain text in the database. The password field is handled by Wasp's auth backend.
SignupForm Customizationβ
To customize the SignupForm component, you need to pass in the additionalFields prop. It can be either a list of extra fields or a render function.
- JavaScript
- TypeScript
import{
SignupForm,
FormError,
FormInput,
FormItemGroup,
FormLabel,
}from'wasp/client/auth'
exportconstSignupPage=()=>{
return(
<SignupForm
additionalFields={[
{
name:'address',
label:'Address',
type:'input',
validations:{
required:'Address is required',
},
},
(form, state)=>{
return(
<FormItemGroup>
<FormLabel>Phone Number</FormLabel>
<FormInput
{...form.register('phoneNumber',{
required:'Phone number is required',
})}
disabled={state.isLoading}
/>
{form.formState.errors.phoneNumber&&(
<FormError>
{form.formState.errors.phoneNumber.message}
</FormError>
)}
</FormItemGroup>
)
},
]}
/>
)
}
import{
SignupForm,
FormError,
FormInput,
FormItemGroup,
FormLabel,
}from'wasp/client/auth'
exportconstSignupPage=()=>{
return(
<SignupForm
additionalFields={[
{
name:'address',
label:'Address',
type:'input',
validations:{
required:'Address is required',
},
},
(form, state)=>{
return(
<FormItemGroup>
<FormLabel>Phone Number</FormLabel>
<FormInput
{...form.register('phoneNumber',{
required:'Phone number is required',
})}
disabled={state.isLoading}
/>
{form.formState.errors.phoneNumber&&(
<FormError>
{form.formState.errors.phoneNumber.message}
</FormError>
)}
</FormItemGroup>
)
},
]}
/>
)
}
The extra fields can be either objects or render functions (you can combine them):
-
Objects are a simple way to describe new fields you need, but a bit less flexible than render functions.
The objects have the following properties:
-
namerequired- the name of the field
-
labelrequired- the label of the field (used in the UI)
-
typerequired- the type of the field, which can be
inputortextarea
- the type of the field, which can be
-
validations- an object with the validation rules for the field. The keys are the validation names, and the values are the validation error messages. Read more about the available validation rules in the react-hook-form docs.
-
-
Render functions receive the
react-hook-formobject and the form state as arguments, and they can use them to render arbitrary UI elements.The render function has the following signature:
(form: UseFormReturn, state: FormState)=> React.ReactNode-
formrequired- the
react-hook-formobject, read more about it in the react-hook-form docs - you need to use the
form.registerfunction to register your fields
- the
-
staterequired- the form state object which has the following properties:
isLoading: boolean- whether the form is currently submitting
- the form state object which has the following properties:
-