wasp/web/docs/auth/overview.md

640 lines
17 KiB
Markdown
Raw Normal View History

---
title: Using Auth
---
import { AuthMethodsGrid } from "@site/src/components/AuthMethodsGrid";
import { Required } from "@site/src/components/Required";
Auth is an essential piece of any serious application. Coincidentally, Wasp provides authentication and authorization support out of the box 🙃.
Enabling auth for your app is optional and can be done by configuring the `auth` field of the `app` declaration.
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```wasp title="main.wasp"
app MyApp {
title: "My app",
//...
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
usernameAndPassword: {}, // use this or email, not both
email: {}, // use this or usernameAndPassword, not both
google: {},
gitHub: {},
},
onAuthFailedRedirectTo: "/someRoute"
}
}
//...
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```wasp title="main.wasp"
app MyApp {
title: "My app",
//...
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
usernameAndPassword: {}, // use this or email, not both
email: {}, // use this or usernameAndPassword, not both
google: {},
gitHub: {},
},
onAuthFailedRedirectTo: "/someRoute"
}
}
//...
```
</TabItem>
</Tabs>
<small>
Read more about the `auth` field options in the [API Reference](#api-reference) section.
</small>
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:
<AuthMethodsGrid />
Let's say we enabled the [Username & password](/docs/auth/username-and-pass) authentication.
We get an auth backend with signup and login endpoints. We also get the `user` object in our [Operations](/docs/data-model/operations/overview) and we can decide what to do based on whether the user is logged in or not.
We would also get the [Auth UI](/docs/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.
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```wasp title="main.wasp"
page MainPage {
component: import Main from "@client/pages/Main.jsx",
authRequired: true
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```wasp title="main.wasp"
page MainPage {
component: import Main from "@client/pages/Main.tsx",
authRequired: true
}
```
</TabItem>
</Tabs>
:::caution Requires auth method
You can only use `authRequired` if your app uses one of the [available auth methods](#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](#accessing-the-logged-in-user).
## Logout action
We provide an action for logging out the user. Here's how you can use it:
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```jsx title="client/components/LogoutButton.jsx"
import logout from '@wasp/auth/logout'
const LogoutButton = () => {
return (
<button onClick={logout}>Logout</button>
)
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```tsx title="client/components/LogoutButton.tsx"
import logout from '@wasp/auth/logout'
const LogoutButton = () => {
return (
<button onClick={logout}>Logout</button>
)
}
```
</TabItem>
</Tabs>
## Accessing the logged-in user
You can get access to the `user` object both in the backend and on the frontend.
### On the client
There are two ways to access the `user` object on the client:
- the `user` prop
- the `useAuth` hook
#### 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:
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```wasp title="main.wasp"
// ...
page AccountPage {
component: import Account from "@client/pages/Account.jsx",
authRequired: true
}
```
```jsx title="client/pages/Account.jsx"
import Button from './Button';
import logout from '@wasp/auth/logout';
const AccountPage = ({ user }) => {
return (
<div>
<Button onClick={logout}>Logout</Button>
{JSON.stringify(user, null, 2)}
</div>
);
};
export default AccountPage;
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```wasp title="main.wasp"
// ...
page AccountPage {
component: import Account from "@client/pages/Account.tsx",
authRequired: true
}
```
```tsx title="client/pages/Account.tsx"
import type { User } from '@wasp/entities';
import Button from './Button';
import logout from '@wasp/auth/logout';
const AccountPage = ({ user }: { user: User }) => {
return (
<div>
<Button onClick={logout}>Logout</Button>
{JSON.stringify(user, null, 2)}
</div>
);
};
export default AccountPage;
```
</TabItem>
</Tabs>
#### 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.
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```jsx title="src/client/pages/MainPage.jsx"
import useAuth from '@wasp/auth/useAuth'
import { Link } from 'react-router-dom'
import logout from '@wasp/auth/logout'
import Todo from '../Todo'
export function Main() {
const { data: user } = useAuth()
if (!user) {
return (
<span>
Please <Link to='/login'>login</Link> or <Link to='/signup'>sign up</Link>.
</span>
)
} else {
return (
<>
<button onClick={logout}>Logout</button>
<Todo />
< />
)
}
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```tsx title="src/client/pages/MainPage.tsx"
import useAuth from '@wasp/auth/useAuth'
import { Link } from 'react-router-dom'
import logout from '@wasp/auth/logout'
import Todo from '../Todo'
export function Main() {
const { data: user } = useAuth()
if (!user) {
return (
<span>
Please <Link to='/login'>login</Link> or <Link to='/signup'>sign up</Link>.
</span>
)
} else {
return (
<>
<button onClick={logout}>Logout</button>
<Todo />
< />
)
}
}
```
</TabItem>
</Tabs>
:::tip
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](/docs/data-model/operations/overview) have access to the `user` object through the `context` argument. `context.user` contains all User entity's fields, except for the password.
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```js title="src/server/actions.js"
import HttpError from '@wasp/core/HttpError.js'
export const createTask = async (task, context) => {
if (!context.user) {
throw new HttpError(403)
}
const Task = context.entities.Task
return Task.create({
data: {
description: task.description,
user: {
connect: { id: context.user.id }
}
}
})
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```ts title="src/server/actions.ts"
import type { Task } from "@wasp/entities"
import type { CreateTask } from "@wasp/actions/types"
import HttpError from '@wasp/core/HttpError.js'
type CreateTaskPayload = Pick<Task, "description">
export const createTask: CreateTask<CreateTaskPayload, Task> = async (args, context) => {
if (!context.user) {
throw new HttpError(403)
}
const Task = context.entities.Task
return Task.create({
data: {
description: args.description,
user: {
connect: { id: context.user.id }
}
}
})
}
```
</TabItem>
</Tabs>
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](/docs/advanced/web-sockets#websocketfn-function).
## User entity
### Password hashing
You don't need to worry about hashing the password yourself. Even when directly using the Prisma client and calling `create()` with a plain-text password, Wasp's middleware makes sure to hash the password before storing it in the database.
For example, if you need to update a user's password, you can safely use the Prisma client to do so, e.g., inside an Action:
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```js title="src/server/actions.js"
export const updatePassword = async (args, context) => {
return context.entities.User.update({
where: { id: args.userId },
data: {
password: 'New pwd which will be hashed automatically!'
}
})
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```ts title="src/server/actions.ts"
import type { UpdatePassword } from "@wasp/actions/types"
import type { User } from "@wasp/entities"
type UpdatePasswordPayload = {
userId: User["id"]
}
export const updatePassword: UpdatePassword<UpdatePasswordPayload, User> = async (args, context) => {
return context.entities.User.update({
where: { id: args.userId },
data: {
password: 'New pwd which will be hashed automatically!'
}
})
}
```
</TabItem>
</Tabs>
### Default validations
Wasp includes several basic validation mechanisms. If you need something extra, the [next section](#customizing-validations) shows how to customize them.
Default validations depend on the auth method you use.
#### Username & password
If you use [Username & password](/docs/auth/username-and-pass) authentication, the default validations are:
- The `username` must not be empty
- The `password` must not be empty, have at least 8 characters, and contain a number
Note that `username`s are stored in a **case-sensitive** manner.
#### Email
If you use [Email](/docs/auth/email) authentication, the default validations are:
- The `email` must not be empty and a valid email address
- The `password` must not be empty, have at least 8 characters, and contain a number
Note that `email`s are stored in a **case-insensitive** manner.
### Customizing validations
:::note
You can only disable the default validation for **Username & password** authentication, but you can add custom validations can to both **Username & password** and **Email** auth methods.
This is a bug in Wasp that is being tracked [here](https://github.com/wasp-lang/wasp/issues/1358)
:::
To disable/enable default validations, or add your own, modify your custom signup function:
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```js
const newUser = context.entities.User.create({
data: {
username: args.username,
password: args.password // password hashed automatically by Wasp! 🐝
},
_waspSkipDefaultValidations: false, // can be omitted if false (default), or explicitly set to true
_waspCustomValidations: [
{
validates: 'password',
message: 'password must contain an uppercase letter',
validator: password => /[A-Z]/.test(password)
},
]
})
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```ts
const newUser = context.entities.User.create({
data: {
username: args.username,
password: args.password // password hashed automatically by Wasp! 🐝
},
_waspSkipDefaultValidations: false, // can be omitted if false (default), or explicitly set to true
_waspCustomValidations: [
{
validates: 'password',
message: 'password must contain an uppercase letter',
validator: password => /[A-Z]/.test(password)
},
]
})
```
</TabItem>
</Tabs>
:::info
Validations always run on `create()`.
For `update()`, they only run when the field mentioned in `validates` is present.
The validation process stops on the first `validator` to return false. If enabled, default validations run first and then custom validations.
:::
### Validation Error Handling
When creating, updating, or deleting entities, you may wish to handle validation errors. Wasp exposes a class called `AuthError` for this purpose.
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```js title="src/server/actions.js"
try {
await context.entities.User.update(...)
} catch (e) {
if (e instanceof AuthError) {
throw new HttpError(422, 'Validation failed', { message: e.message })
} else {
throw e
}
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```ts title="src/server/actions.ts"
try {
await context.entities.User.update(...)
} catch (e) {
if (e instanceof AuthError) {
throw new HttpError(422, 'Validation failed', { message: e.message })
} else {
throw e
}
}
```
</TabItem>
</Tabs>
## API Reference
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```wasp title="main.wasp"
title: "My app",
//...
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
usernameAndPassword: {}, // use this or email, not both
email: {}, // use this or usernameAndPassword, not both
google: {},
gitHub: {},
},
onAuthFailedRedirectTo: "/someRoute"
}
}
//...
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```wasp title="main.wasp"
app MyApp {
title: "My app",
//...
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
usernameAndPassword: {}, // use this or email, not both
email: {}, // use this or usernameAndPassword, not both
google: {},
gitHub: {},
},
onAuthFailedRedirectTo: "/someRoute"
}
}
//...
```
</TabItem>
</Tabs>
`app.auth` is a dictionary with the following fields:
#### `userEntity: entity` <Required />
The entity representing the user. Its mandatory fields depend on your chosen auth method.
#### `externalAuthEntity: entity`
Wasp requires you to set the field `auth.externalAuthEntity` for all authentication methods relying on an external authorizatino provider (e.g., Google). You also need to tweak the Entity referenced by `auth.userEntity`, as shown below.
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```wasp {4,14} title="main.wasp"
//...
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
//...
entity User {=psl
id Int @id @default(autoincrement())
//...
externalAuthAssociations SocialLogin[]
psl=}
entity SocialLogin {=psl
id Int @id @default(autoincrement())
provider String
providerId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
createdAt DateTime @default(now())
@@unique([provider, providerId, userId])
psl=}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```wasp {4,14} title="main.wasp"
//...
auth: {
userEntity: User,
externalAuthEntity: SocialLogin,
//...
entity User {=psl
id Int @id @default(autoincrement())
//...
externalAuthAssociations SocialLogin[]
psl=}
entity SocialLogin {=psl
id Int @id @default(autoincrement())
provider String
providerId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
createdAt DateTime @default(now())
@@unique([provider, providerId, userId])
psl=}
```
</TabItem>
</Tabs>
:::note
The same `externalAuthEntity` can be used across different social login providers (e.g., both GitHub and Google can use the same entity).
:::
See [Google docs](/docs/auth/social-auth/google) and [GitHub docs](/docs/auth/social-auth/github) for more details.
#### `methods: dict` <Required />
A dictionary of auth methods enabled for the app.
<AuthMethodsGrid />
#### `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 [essentials docs on auth](/docs/tutorial/auth#adding-auth-to-the-project) 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 `"/"`.
:::note
Automatic redirect on successful login only works when using the Wasp-provided [Auth UI](/docs/auth/ui).
:::