Skip to content

Commit 26d709d

Browse files
jmikrutdenolfeJessRynkarJarrodMFleschr1tsuu
authored
feat: auth sessions (#12483)
Adds full session functionality into Payload's existing local authentication strategy. It's enabled by default, because this is a more secure pattern that we should enforce. However, we have provided an opt-out pattern for those that want to stick to stateless JWT authentication by passing `collectionConfig.auth.useSessions: false`. Todo: - [x] @JessRynkar to update the Next.js server functions for refresh and logout to support these new features - [x] @JessRynkar resolve build errors --------- Co-authored-by: Elliot DeNolf <denolfe@gmail.com> Co-authored-by: Jessica Chowdhury <jessica@trbl.design> Co-authored-by: Jarrod Flesch <30633324+JarrodMFlesch@users.noreply.github.com> Co-authored-by: Sasha <64744993+r1tsuu@users.noreply.github.com>
1 parent c8b7214 commit 26d709d

29 files changed

Lines changed: 610 additions & 81 deletions

File tree

‎docs/authentication/operations.mdx‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -180,19 +180,22 @@ As Payload sets HTTP-only cookies, logging out cannot be done by just removing a
180180
**Example REST API logout**:
181181

182182
```ts
183-
const res =awaitfetch('http://localhost:3000/api/[collection-slug]/logout', {
184-
method: 'POST',
185-
headers: {
186-
'Content-Type': 'application/json',
183+
const res =awaitfetch(
184+
'http://localhost:3000/api/[collection-slug]/logout?allSessions=false',
185+
{
186+
method: 'POST',
187+
headers: {
188+
'Content-Type': 'application/json',
189+
},
187190
},
188-
})
191+
)
189192
```
190193

191194
**Example GraphQL Mutation**:
192195

193196
```
194197
mutation {
195-
logout[collection-singular-label]
198+
logoutUser(allSessions: false)
196199
}
197200
```
198201

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

209+
#### Logging out with sessions enabled
210+
211+
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.
212+
206213
## Refresh
207214

208215
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.

‎docs/authentication/overview.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ The following options are available:
9191
|**`strategies`**| Advanced - an array of custom authentication strategies to extend this collection's authentication with. [More details](./custom-strategies). |
9292
|**`tokenExpiration`**| How long (in seconds) to keep the user logged in. JWTs and HTTP-only cookies will both expire at the same time. |
9393
|**`useAPIKey`**| Payload Authentication provides for API keys to be set on each user within an Authentication-enabled Collection. [More details](./api-keys). |
94+
|**`useSessions`**| True by default. Set to `false` to use stateless JWTs for authentication instead of sessions. |
9495
|**`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). |
9596

9697
### Login With Username

‎docs/local-api/server-functions.mdx‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -393,15 +393,15 @@ export default function LoginForm() {
393393

394394
### Logout
395395

396-
Logs out the current user by clearing the authentication cookie.
396+
Logs out the current user by clearing the authentication cookie and current sessions.
397397

398398
#### Importing the `logout` function
399399

400400
```ts
401401
import { logout } from'@payloadcms/next/auth'
402402
```
403403

404-
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.
404+
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`.
405405

406406
```ts
407407
'use server'
@@ -411,7 +411,7 @@ import config from '@payload-config'
411411

412412
exportasyncfunction logoutAction() {
413413
try {
414-
returnawaitlogout({ config })
414+
returnawaitlogout({ allSessions: true, config })
415415
} catch (error) {
416416
thrownewError(
417417
`Logout failed: ${errorinstanceofError?error.message:'Unknown error'}`,
@@ -434,7 +434,7 @@ export default function LogoutButton() {
434434

435435
### Refresh
436436

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

439439
#### Importing the `refresh` function
440440

@@ -453,7 +453,6 @@ import config from '@payload-config'
453453
exportasyncfunction refreshAction() {
454454
try {
455455
returnawaitrefresh({
456-
collection: 'users', // pass your collection slug
457456
config,
458457
})
459458
} catch (error) {

‎docs/plugins/sentry.mdx‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,7 @@ import * as Sentry from '@sentry/nextjs'
7474

7575
const config =buildConfig({
7676
collections: [Pages, Media],
77-
plugins: [
78-
sentryPlugin({ Sentry })
79-
],
77+
plugins: [sentryPlugin({ Sentry })],
8078
})
8179

8280
exportdefaultconfig
@@ -98,9 +96,7 @@ export default buildConfig({
9896
pool: { connectionString: process.env.DATABASE_URL },
9997
pg, // Inject the patched pg driver for Sentry instrumentation
10098
}),
101-
plugins: [
102-
sentryPlugin({ Sentry })
103-
],
99+
plugins: [sentryPlugin({ Sentry })],
104100
})
105101
```
106102

‎packages/graphql/src/resolvers/auth/logout.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { Context } from '../types.js'
77
exportfunctionlogout(collection: Collection): any{
88
asyncfunctionresolver(_,args,context: Context){
99
constoptions={
10+
allSessions: args.allSessions,
1011
collection,
1112
req: isolateObjectProperty(context.req,'transactionID'),
1213
}

‎packages/graphql/src/schema/initCollections.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,9 @@ export function initCollections({ config, graphqlResult }: InitCollectionsGraphQ
487487

488488
graphqlResult.Mutation.fields[`logout${singularName}`]={
489489
type: GraphQLString,
490+
args: {
491+
allSessions: {type: GraphQLBoolean},
492+
},
490493
resolve: logout(collection),
491494
}
492495

‎packages/next/src/auth/logout.ts‎

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,46 @@
11
'use server'
22

3+
importtype{SanitizedConfig}from'payload'
4+
35
import{cookiesasgetCookies,headersasnextHeaders}from'next/headers.js'
4-
import{getPayload}from'payload'
6+
import{createLocalReq,getPayload,logoutOperation}from'payload'
57

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

8-
exportasyncfunctionlogout({ config }: {config: any}){
10+
exportasyncfunctionlogout({
11+
allSessions =false,
12+
config,
13+
}: {
14+
allSessions?: boolean
15+
config: Promise<SanitizedConfig>|SanitizedConfig
16+
}){
917
constpayload=awaitgetPayload({ config })
1018
constheaders=awaitnextHeaders()
11-
constresult=awaitpayload.auth({ headers })
19+
constauthResult=awaitpayload.auth({ headers })
1220

13-
if(!result.user){
21+
if(!authResult.user){
1422
return{message: 'User already logged out',success: true}
1523
}
1624

17-
constexistingCookie=awaitgetExistingAuthToken(payload.config.cookiePrefix)
25+
const{ user }=authResult
26+
constreq=awaitcreateLocalReq({ user },payload)
27+
constcollection=payload.collections[user.collection]
1828

29+
constlogoutResult=awaitlogoutOperation({
30+
allSessions,
31+
collection,
32+
req,
33+
})
34+
35+
if(!logoutResult){
36+
return{message: 'Logout failed',success: false}
37+
}
38+
39+
constexistingCookie=awaitgetExistingAuthToken(payload.config.cookiePrefix)
1940
if(existingCookie){
2041
constcookies=awaitgetCookies()
2142
cookies.delete(existingCookie.name)
22-
return{message: 'User logged out successfully',success: true}
2343
}
44+
45+
return{message: 'User logged out successfully',success: true}
2446
}

‎packages/next/src/auth/refresh.ts‎

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,45 @@
33
importtype{CollectionSlug}from'payload'
44

55
import{headersasnextHeaders}from'next/headers.js'
6-
import{getPayload}from'payload'
6+
import{createLocalReq,getPayload,refreshOperation}from'payload'
77

88
import{getExistingAuthToken}from'../utilities/getExistingAuthToken.js'
99
import{setPayloadAuthCookie}from'../utilities/setPayloadAuthCookie.js'
1010

11-
exportasyncfunctionrefresh({collection,config }: {collection: CollectionSlug;config: any}){
11+
exportasyncfunctionrefresh({ config }: {config: any}){
1212
constpayload=awaitgetPayload({ config })
13-
constauthConfig=payload.collections[collection]?.config.auth
13+
constheaders=awaitnextHeaders()
14+
constresult=awaitpayload.auth({ headers })
1415

15-
if(!authConfig){
16+
if(!result.user){
17+
thrownewError('Cannot refresh token: user not authenticated')
18+
}
19+
20+
constcollection: CollectionSlug|undefined=result.user.collection
21+
constcollectionConfig=payload.collections[collection]
22+
23+
if(!collectionConfig?.config.auth){
1624
thrownewError(`No auth config found for collection: ${collection}`)
1725
}
1826

19-
const{ user }=awaitpayload.auth({headers: awaitnextHeaders()})
27+
constreq=awaitcreateLocalReq({user: result.user},payload)
2028

21-
if(!user){
22-
thrownewError('User not authenticated')
29+
constrefreshResult=awaitrefreshOperation({
30+
collection: collectionConfig,
31+
req,
32+
})
33+
34+
if(!refreshResult){
35+
return{message: 'Token refresh failed',success: false}
2336
}
2437

2538
constexistingCookie=awaitgetExistingAuthToken(payload.config.cookiePrefix)
26-
2739
if(!existingCookie){
28-
return{message: 'No valid token found',success: false}
40+
return{message: 'No valid token found to refresh',success: false}
2941
}
3042

3143
awaitsetPayloadAuthCookie({
32-
authConfig,
44+
authConfig: collectionConfig.config.auth,
3345
cookiePrefix: payload.config.cookiePrefix,
3446
token: existingCookie.value,
3547
})
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
importtype{ArrayField}from'../../fields/config/types.js'
2+
3+
exportconstsessionsFieldConfig: ArrayField={
4+
name: 'sessions',
5+
type: 'array',
6+
access: {
7+
read: ({ doc,req: { user }})=>{
8+
returnuser?.id===doc?.id
9+
},
10+
update: ()=>false,
11+
},
12+
admin: {
13+
disabled: true,
14+
},
15+
fields: [
16+
{
17+
name: 'id',
18+
type: 'text',
19+
required: true,
20+
},
21+
{
22+
name: 'createdAt',
23+
type: 'date',
24+
defaultValue: ()=>newDate(),
25+
},
26+
{
27+
name: 'expiresAt',
28+
type: 'date',
29+
required: true,
30+
},
31+
],
32+
}

‎packages/payload/src/auth/endpoints/logout.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import { logoutOperation } from '../operations/logout.js'
99

1010
exportconstlogoutHandler: PayloadHandler=async(req)=>{
1111
constcollection=getRequestCollection(req)
12-
const{ t }=req
12+
const{ searchParams, t }=req
13+
1314
constresult=awaitlogoutOperation({
15+
allSessions: searchParams.get('allSessions')==='true',
1416
collection,
1517
req,
1618
})

0 commit comments

Comments
 (0)