Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 13 additions & 6 deletions docs/authentication/operations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -180,19 +180,22 @@ As Payload sets HTTP-only cookies, logging out cannot be done by just removing a
**Example REST API logout**:

```ts
const res = await fetch('http://localhost:3000/api/[collection-slug]/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
const res = await fetch(
'http://localhost:3000/api/[collection-slug]/logout?allSessions=false',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
})
)
```

**Example GraphQL Mutation**:

```
mutation {
logout[collection-singular-label]
logoutUser(allSessions: false)
}
```

Expand All @@ -203,6 +206,10 @@ mutation {
docs](../local-api/server-functions#reusable-payload-server-functions).
</Banner>

#### Logging out with sessions enabled

By default, logging out will only end the session pertaining to the JWT that was used to log out with. However, you can pass `allSessions: true` to the logout operation in order to end all sessions for the user logging out.

## Refresh

Allows for "refreshing" JWTs. If your user has a token that is about to expire, but the user is still active and using the app, you might want to use the `refresh` operation to receive a new token by executing this operation via the authenticated user.
Expand Down
1 change: 1 addition & 0 deletions docs/authentication/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ The following options are available:
| **`strategies`** | Advanced - an array of custom authentication strategies to extend this collection's authentication with. [More details](./custom-strategies). |
| **`tokenExpiration`** | How long (in seconds) to keep the user logged in. JWTs and HTTP-only cookies will both expire at the same time. |
| **`useAPIKey`** | Payload Authentication provides for API keys to be set on each user within an Authentication-enabled Collection. [More details](./api-keys). |
| **`useSessions`** | True by default. Set to `false` to use stateless JWTs for authentication instead of sessions. |
| **`verify`** | Set to `true` or pass an object with verification options to require users to verify by email before they are allowed to log into your app. [More details](./email#email-verification). |

### Login With Username
Expand Down
9 changes: 4 additions & 5 deletions docs/local-api/server-functions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -393,15 +393,15 @@ export default function LoginForm() {

### Logout

Logs out the current user by clearing the authentication cookie.
Logs out the current user by clearing the authentication cookie and current sessions.

#### Importing the `logout` function

```ts
import { logout } from '@payloadcms/next/auth'
```

Similar to the login function, you now need to pass your Payload config to this function and this cannot be done in a client component. Use a helper server function as shown below.
Similar to the login function, you now need to pass your Payload config to this function and this cannot be done in a client component. Use a helper server function as shown below. To ensure all sessions are cleared, set `allSessions: true` in the options, if you wish to logout but keep current sessions active, you can set this to `false` or leave it `undefined`.

```ts
'use server'
Expand All @@ -411,7 +411,7 @@ import config from '@payload-config'

export async function logoutAction() {
try {
return await logout({ config })
return await logout({ allSessions: true, config })
} catch (error) {
throw new Error(
`Logout failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
Expand All @@ -434,7 +434,7 @@ export default function LogoutButton() {

### Refresh

Refreshes the authentication token for the logged-in user.
Refreshes the authentication token and current session for the logged-in user.

#### Importing the `refresh` function

Expand All @@ -453,7 +453,6 @@ import config from '@payload-config'
export async function refreshAction() {
try {
return await refresh({
collection: 'users', // pass your collection slug
config,
})
} catch (error) {
Expand Down
8 changes: 2 additions & 6 deletions docs/plugins/sentry.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,7 @@ import * as Sentry from '@sentry/nextjs'

const config = buildConfig({
collections: [Pages, Media],
plugins: [
sentryPlugin({ Sentry })
],
plugins: [sentryPlugin({ Sentry })],
})

export default config
Expand All @@ -98,9 +96,7 @@ export default buildConfig({
pool: { connectionString: process.env.DATABASE_URL },
pg, // Inject the patched pg driver for Sentry instrumentation
}),
plugins: [
sentryPlugin({ Sentry })
],
plugins: [sentryPlugin({ Sentry })],
})
```

Expand Down
1 change: 1 addition & 0 deletions packages/graphql/src/resolvers/auth/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { Context } from '../types.js'
export function logout(collection: Collection): any {
async function resolver(_, args, context: Context) {
const options = {
allSessions: args.allSessions,
collection,
req: isolateObjectProperty(context.req, 'transactionID'),
}
Expand Down
3 changes: 3 additions & 0 deletions packages/graphql/src/schema/initCollections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,9 @@ export function initCollections({ config, graphqlResult }: InitCollectionsGraphQ

graphqlResult.Mutation.fields[`logout${singularName}`] = {
type: GraphQLString,
args: {
allSessions: { type: GraphQLBoolean },
},
resolve: logout(collection),
}

Expand Down
34 changes: 28 additions & 6 deletions packages/next/src/auth/logout.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,46 @@
'use server'

import type { SanitizedConfig } from 'payload'

import { cookies as getCookies, headers as nextHeaders } from 'next/headers.js'
import { getPayload } from 'payload'
import { createLocalReq, getPayload, logoutOperation } from 'payload'

import { getExistingAuthToken } from '../utilities/getExistingAuthToken.js'

export async function logout({ config }: { config: any }) {
export async function logout({
allSessions = false,
config,
}: {
allSessions?: boolean
config: Promise<SanitizedConfig> | SanitizedConfig
}) {
const payload = await getPayload({ config })
const headers = await nextHeaders()
const result = await payload.auth({ headers })
const authResult = await payload.auth({ headers })

if (!result.user) {
if (!authResult.user) {
return { message: 'User already logged out', success: true }
}

const existingCookie = await getExistingAuthToken(payload.config.cookiePrefix)
const { user } = authResult
const req = await createLocalReq({ user }, payload)
const collection = payload.collections[user.collection]

const logoutResult = await logoutOperation({
allSessions,
collection,
req,
})

if (!logoutResult) {
return { message: 'Logout failed', success: false }
}

const existingCookie = await getExistingAuthToken(payload.config.cookiePrefix)
if (existingCookie) {
const cookies = await getCookies()
cookies.delete(existingCookie.name)
return { message: 'User logged out successfully', success: true }
}

return { message: 'User logged out successfully', success: true }
}
32 changes: 22 additions & 10 deletions packages/next/src/auth/refresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,45 @@
import type { CollectionSlug } from 'payload'

import { headers as nextHeaders } from 'next/headers.js'
import { getPayload } from 'payload'
import { createLocalReq, getPayload, refreshOperation } from 'payload'

import { getExistingAuthToken } from '../utilities/getExistingAuthToken.js'
import { setPayloadAuthCookie } from '../utilities/setPayloadAuthCookie.js'

export async function refresh({ collection, config }: { collection: CollectionSlug; config: any }) {
export async function refresh({ config }: { config: any }) {
const payload = await getPayload({ config })
const authConfig = payload.collections[collection]?.config.auth
const headers = await nextHeaders()
const result = await payload.auth({ headers })

if (!authConfig) {
if (!result.user) {
throw new Error('Cannot refresh token: user not authenticated')
}

const collection: CollectionSlug | undefined = result.user.collection
const collectionConfig = payload.collections[collection]

if (!collectionConfig?.config.auth) {
throw new Error(`No auth config found for collection: ${collection}`)
}

const { user } = await payload.auth({ headers: await nextHeaders() })
const req = await createLocalReq({ user: result.user }, payload)

if (!user) {
throw new Error('User not authenticated')
const refreshResult = await refreshOperation({
collection: collectionConfig,
req,
})

if (!refreshResult) {
return { message: 'Token refresh failed', success: false }
}

const existingCookie = await getExistingAuthToken(payload.config.cookiePrefix)

if (!existingCookie) {
return { message: 'No valid token found', success: false }
return { message: 'No valid token found to refresh', success: false }
}

await setPayloadAuthCookie({
authConfig,
authConfig: collectionConfig.config.auth,
cookiePrefix: payload.config.cookiePrefix,
token: existingCookie.value,
})
Expand Down
32 changes: 32 additions & 0 deletions packages/payload/src/auth/baseFields/sessions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { ArrayField } from '../../fields/config/types.js'

export const sessionsFieldConfig: ArrayField = {
name: 'sessions',
type: 'array',
access: {
read: ({ doc, req: { user } }) => {
return user?.id === doc?.id
},
update: () => false,
},
admin: {
disabled: true,
},
fields: [
{
name: 'id',
type: 'text',
required: true,
},
{
name: 'createdAt',
type: 'date',
defaultValue: () => new Date(),
},
{
name: 'expiresAt',
type: 'date',
required: true,
},
],
}
4 changes: 3 additions & 1 deletion packages/payload/src/auth/endpoints/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import { logoutOperation } from '../operations/logout.js'

export const logoutHandler: PayloadHandler = async (req) => {
const collection = getRequestCollection(req)
const { t } = req
const { searchParams, t } = req

const result = await logoutOperation({
allSessions: searchParams.get('allSessions') === 'true',
collection,
req,
})
Expand Down
5 changes: 5 additions & 0 deletions packages/payload/src/auth/getAuthFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { accountLockFields } from './baseFields/accountLock.js'
import { apiKeyFields } from './baseFields/apiKey.js'
import { baseAuthFields } from './baseFields/auth.js'
import { emailFieldConfig } from './baseFields/email.js'
import { sessionsFieldConfig } from './baseFields/sessions.js'
import { usernameFieldConfig } from './baseFields/username.js'
import { verificationFields } from './baseFields/verification.js'

Expand Down Expand Up @@ -52,6 +53,10 @@ export const getBaseAuthFields = (authConfig: IncomingAuthType): Field[] => {
if (authConfig?.maxLoginAttempts && authConfig.maxLoginAttempts > 0) {
authFields.push(...accountLockFields)
}

if (authConfig.useSessions) {
authFields.push(sessionsFieldConfig)
}
}

return authFields
Expand Down
7 changes: 6 additions & 1 deletion packages/payload/src/auth/getFieldsToSign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,21 @@ const traverseFields = ({
export const getFieldsToSign = (args: {
collectionConfig: CollectionConfig
email: string
sid?: string
user: PayloadRequest['user']
}): Record<string, unknown> => {
const { collectionConfig, email, user } = args
const { collectionConfig, email, sid, user } = args

const result: Record<string, unknown> = {
id: user?.id,
collection: collectionConfig.slug,
email,
}

if (sid) {
result.sid = sid
}

traverseFields({
data: user!,
fields: collectionConfig.fields,
Expand Down
Loading
Loading