Username & Password
Wasp supports username & password authentication out of the box with login and signup flows. It provides you with the server-side implementation and the UI components for the client side.
Setting Up Username & Password Authentication
To set up username authentication we need to:
- Enable username authentication in the Wasp file
- Add the
User
entity - Add the auth routes and pages
- Use Auth UI components in our pages
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 Username Authentication
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 username authentication
usernameAndPassword: {},
},
onAuthFailedRedirectTo: "/login"
}
}
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 username authentication
usernameAndPassword: {},
},
onAuthFailedRedirectTo: "/login"
}
}
Read more about the usernameAndPassword
auth method options here.
2. Add the User Entity
The User
entity can be as simple as including only the id
field:
- JavaScript
- TypeScript
// 3. Define the user entity
model User {
id Int @id @default(autoincrement())
// Add your own fields below
// ...
}
// 3. 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 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"
}
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 } 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>
</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>
)
}
// 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 } 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>
</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>
)
}
// 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.
Conclusion
That's it! We have set up username authentication in our app. 🎉
Running wasp db migrate-dev
and then wasp start
should give you a working app with username authentication. If you want to put some of the pages behind authentication, read the auth overview docs.
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.
Customizing the Auth Flow
The login and signup flows are pretty standard: they allow the user to sign up and then log in with their username and password. The signup flow validates the username and password and then creates a new user entity in the database.
Read more about the default username and password validation rules in the auth overview docs.
If you require more control in your authentication flow, you can achieve that in the following ways:
- Create your UI and use
signup
andlogin
actions. - Create your custom sign-up action which uses the lower-level API, along with your custom code.
1. Using the signup
and login
actions
login()
An action for logging in the user.
It takes two arguments:
username: string
required
Username of the user logging in.
password: string
required
Password of the user logging in.
You can use it like this:
- JavaScript
- TypeScript
import { login } from 'wasp/client/auth'
import { useState } from 'react'
import { useNavigate, Link } from 'react-router-dom'
export function LoginPage() {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState(null)
const navigate = useNavigate()
async function handleSubmit(event) {
event.preventDefault()
try {
await login(username, password)
navigate('/')
} catch (error) {
setError(error)
}
}
return <form onSubmit={handleSubmit}>{/* ... */}</form>
}
import { login } from 'wasp/client/auth'
import { useState } from 'react'
import { useNavigate, Link } from 'react-router-dom'
export function LoginPage() {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState<Error | null>(null)
const navigate = useNavigate()
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
try {
await login(username, password)
navigate('/')
} catch (error: unknown) {
setError(error as Error)
}
}
return <form onSubmit={handleSubmit}>{/* ... */}</form>
}
When using the exposed login()
function, make sure to implement your redirect on success login logic (e.g. redirecting to home).
signup()
An action for signing up the user. This action does not log in the user, you still need to call login()
.
It takes one argument:
userFields: object
requiredIt has the following fields:
username: string
requiredpassword: string
required
infoBy default, Wasp will only save the
username
andpassword
fields. If you want to add extra fields to your signup process, read about defining extra signup fields.
You can use it like this:
- JavaScript
- TypeScript
import { signup, login } from 'wasp/client/auth'
import { useState } from 'react'
import { useNavigate, Link } from 'react-router-dom'
export function Signup() {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState(null)
const navigate = useNavigate()
async function handleSubmit(event) {
event.preventDefault()
try {
await signup({
username,
password,
})
await login(username, password)
navigate('/')
} catch (error) {
setError(error)
}
}
return <form onSubmit={handleSubmit}>{/* ... */}</form>
}
import { signup, login } from 'wasp/client/auth'
import { useState } from 'react'
import { useNavigate, Link } from 'react-router-dom'
export function Signup() {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState<Error | null>(null)
const navigate = useNavigate()
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
try {
await signup({
username,
password,
})
await login(username, password)
navigate('/')
} catch (error: unknown) {
setError(error as Error)
}
}
return <form onSubmit={handleSubmit}>{/* ... */}</form>
}
2. Creating your custom sign-up action
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,
ensureValidUsername,
createProviderId,
sanitizeAndSerializeProviderData,
createUser,
} from 'wasp/server/auth'
export const signup = async (args, _context) => {
ensureValidUsername(args)
ensurePasswordIsPresent(args)
ensureValidPassword(args)
try {
const providerId = createProviderId('username', args.username)
const providerData = await sanitizeAndSerializeProviderData({
hashedPassword: args.password,
})
await createUser(
providerId,
providerData,
// Any additional data you want to store on the User entity
{}
)
} 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",
}
import {
ensurePasswordIsPresent,
ensureValidPassword,
ensureValidUsername,
createProviderId,
sanitizeAndSerializeProviderData,
createUser,
} from 'wasp/server/auth'
import type { CustomSignup } from 'wasp/server/operations'
type CustomSignupInput = {
username: string
password: string
}
type CustomSignupOutput = {
success: boolean
message: string
}
export const signup: CustomSignup<
CustomSignupInput,
CustomSignupOutput
> = async (args, _context) => {
ensureValidUsername(args)
ensurePasswordIsPresent(args)
ensureValidPassword(args)
try {
const providerId = createProviderId('username', args.username)
const providerData = await sanitizeAndSerializeProviderData<'username'>({
hashedPassword: args.password,
})
await createUser(
providerId,
providerData,
// Any additional data you want to store on the User entity
{}
)
} 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.
Username
ensureValidUsername(args)
Checks if the username 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 username like this:
const usernameIdentity = user.identities.username
// Username that the user used to sign up, e.g. "fluffyllama"
usernameIdentity.id
Read more about accessing the user data in the Accessing User Data section of the docs.
API Reference
userEntity
fields
- JavaScript
- TypeScript
app myApp {
wasp: {
version: "^0.15.0"
},
title: "My App",
auth: {
userEntity: User,
methods: {
usernameAndPassword: {},
},
onAuthFailedRedirectTo: "/login"
}
}
model User {
id Int @id @default(autoincrement())
}
app myApp {
wasp: {
version: "^0.15.0"
},
title: "My App",
auth: {
userEntity: User,
methods: {
usernameAndPassword: {},
},
onAuthFailedRedirectTo: "/login"
}
}
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 usernameAndPassword
dict
- JavaScript
- TypeScript
app myApp {
wasp: {
version: "^0.15.0"
},
title: "My App",
auth: {
userEntity: User,
methods: {
usernameAndPassword: {
userSignupFields: import { userSignupFields } from "@src/auth/email.js",
},
},
onAuthFailedRedirectTo: "/login"
}
}
// ...
app myApp {
wasp: {
version: "^0.15.0"
},
title: "My App",
auth: {
userEntity: User,
methods: {
usernameAndPassword: {
userSignupFields: import { userSignupFields } from "@src/auth/email.js",
},
},
onAuthFailedRedirectTo: "/login"
}
}
// ...
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,
})