Skip to content

Commit 37e2da0

Browse files
authored
feat(next)!: allows auth strategies to return headers that need to be… (#6964)
## Description Some authentication strategies may need to set headers for responses, such as updating cookies via a refresh token, and similar. This PR extends Payload's auth strategy capabilities with a manner of accomplishing this. This is a breaking change if you have custom authentication strategies in Payload's 3.0 beta. But it's a simple one to update. Instead of your custom auth strategy returning the `user`, now you must return an object with a `user` property. This is because you can now also optionally return `responseHeaders`, which will be returned by Payload API responses if you define them in your auth strategies. This can be helpful for cases where you need to set cookies and similar, directly within your auth strategies. Before: ```ts return user ``` After: ```ts return { user } ```
1 parent 07f3f27 commit 37e2da0

File tree

14 files changed

+267
-49
lines changed

14 files changed

+267
-49
lines changed

docs/authentication/custom-strategies.mdx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ The `authenticate` function is passed the following arguments:
3333

3434
### Example Strategy
3535

36-
At its core a strategy simply takes information from the incoming request and returns a user. This is exactly how Payloads built-in strategies function.
36+
At its core a strategy simply takes information from the incoming request and returns a user. This is exactly how Payload's built-in strategies function.
37+
38+
Your `authenticate` method should return an object containing a Payload user document and any optional headers that you'd like Payload to set for you when we return a response.
3739

3840
```ts
39-
import { CollectionConfig } from 'payload/types'
41+
import { CollectionConfig } from 'payload'
4042

4143
export const Users: CollectionConfig = {
4244
slug: 'users',
@@ -59,7 +61,18 @@ export const Users: CollectionConfig = {
5961
},
6062
})
6163

62-
return usersQuery.docs[0] || null
64+
return {
65+
// Send the user back to authenticate,
66+
// or send null if no user should be authenticated
67+
user: usersQuery.docs[0] || null,
68+
69+
// Optionally, you can return headers
70+
// that you'd like Payload to set here when
71+
// it returns the response
72+
responseHeaders: new Headers({
73+
'some-header': 'my header value'
74+
})
75+
}
6376
}
6477
}
6578
]

packages/next/src/routes/graphql/handler.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { addDataAndFileToRequest } from '../../utilities/addDataAndFileToRequest
99
import { addLocalesToRequestFromData } from '../../utilities/addLocalesToRequest.js'
1010
import { createPayloadRequest } from '../../utilities/createPayloadRequest.js'
1111
import { headersWithCors } from '../../utilities/headersWithCors.js'
12+
import { mergeHeaders } from '../../utilities/mergeHeaders.js'
1213

1314
const handleError = async (
1415
payload: Payload,
@@ -122,7 +123,7 @@ export const POST =
122123
return response
123124
},
124125
schema,
125-
validationRules: (request, args, defaultRules) => defaultRules.concat(validationRules(args)),
126+
validationRules: (_, args, defaultRules) => defaultRules.concat(validationRules(args)),
126127
})(originalRequest)
127128

128129
const resHeaders = headersWithCors({
@@ -134,6 +135,10 @@ export const POST =
134135
resHeaders.append(key, headers[key])
135136
}
136137

138+
if (basePayloadRequest.responseHeaders) {
139+
mergeHeaders(basePayloadRequest.responseHeaders, resHeaders)
140+
}
141+
137142
return new Response(apiResponse.body, {
138143
headers: resHeaders,
139144
status: apiResponse.status,

packages/next/src/routes/rest/index.ts

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { addDataAndFileToRequest } from '../../utilities/addDataAndFileToRequest
2121
import { addLocalesToRequestFromData } from '../../utilities/addLocalesToRequest.js'
2222
import { createPayloadRequest } from '../../utilities/createPayloadRequest.js'
2323
import { headersWithCors } from '../../utilities/headersWithCors.js'
24+
import { mergeHeaders } from '../../utilities/mergeHeaders.js'
2425
import { access } from './auth/access.js'
2526
import { forgotPassword } from './auth/forgotPassword.js'
2627
import { init } from './auth/init.js'
@@ -122,15 +123,15 @@ const endpoints = {
122123
},
123124
}
124125

125-
const handleCustomEndpoints = ({
126+
const handleCustomEndpoints = async ({
126127
endpoints,
127128
entitySlug,
128129
payloadRequest,
129130
}: {
130131
endpoints: Endpoint[] | GlobalConfig['endpoints']
131132
entitySlug?: string
132133
payloadRequest: PayloadRequest
133-
}): Promise<Response> | Response => {
134+
}): Promise<Response> => {
134135
if (endpoints && endpoints.length > 0) {
135136
let handlerParams = {}
136137
const { pathname } = payloadRequest
@@ -170,7 +171,15 @@ const handleCustomEndpoints = ({
170171
...payloadRequest.routeParams,
171172
...handlerParams,
172173
}
173-
return customEndpoint.handler(payloadRequest)
174+
const res = await customEndpoint.handler(payloadRequest)
175+
176+
if (res instanceof Response) {
177+
if (payloadRequest.responseHeaders) {
178+
mergeHeaders(payloadRequest.responseHeaders, res.headers)
179+
}
180+
181+
return res
182+
}
174183
}
175184
}
176185

@@ -376,13 +385,20 @@ export const GET =
376385
res = await endpoints.root.GET[slug1]({ req: payloadRequest })
377386
}
378387

379-
if (res instanceof Response) return res
388+
if (res instanceof Response) {
389+
if (req.responseHeaders) {
390+
mergeHeaders(req.responseHeaders, res.headers)
391+
}
392+
393+
return res
394+
}
380395

381396
// root routes
382397
const customEndpointResponse = await handleCustomEndpoints({
383398
endpoints: req.payload.config.endpoints,
384399
payloadRequest: req,
385400
})
401+
386402
if (customEndpointResponse) return customEndpointResponse
387403

388404
return RouteNotFoundResponse({
@@ -545,13 +561,20 @@ export const POST =
545561
res = await endpoints.root.POST[slug1]({ req: payloadRequest })
546562
}
547563

548-
if (res instanceof Response) return res
564+
if (res instanceof Response) {
565+
if (req.responseHeaders) {
566+
mergeHeaders(req.responseHeaders, res.headers)
567+
}
568+
569+
return res
570+
}
549571

550572
// root routes
551573
const customEndpointResponse = await handleCustomEndpoints({
552574
endpoints: req.payload.config.endpoints,
553575
payloadRequest: req,
554576
})
577+
555578
if (customEndpointResponse) return customEndpointResponse
556579

557580
return RouteNotFoundResponse({
@@ -626,13 +649,20 @@ export const DELETE =
626649
}
627650
}
628651

629-
if (res instanceof Response) return res
652+
if (res instanceof Response) {
653+
if (req.responseHeaders) {
654+
mergeHeaders(req.responseHeaders, res.headers)
655+
}
656+
657+
return res
658+
}
630659

631660
// root routes
632661
const customEndpointResponse = await handleCustomEndpoints({
633662
endpoints: req.payload.config.endpoints,
634663
payloadRequest: req,
635664
})
665+
636666
if (customEndpointResponse) return customEndpointResponse
637667

638668
return RouteNotFoundResponse({
@@ -708,13 +738,20 @@ export const PATCH =
708738
}
709739
}
710740

711-
if (res instanceof Response) return res
741+
if (res instanceof Response) {
742+
if (req.responseHeaders) {
743+
mergeHeaders(req.responseHeaders, res.headers)
744+
}
745+
746+
return res
747+
}
712748

713749
// root routes
714750
const customEndpointResponse = await handleCustomEndpoints({
715751
endpoints: req.payload.config.endpoints,
716752
payloadRequest: req,
717753
})
754+
718755
if (customEndpointResponse) return customEndpointResponse
719756

720757
return RouteNotFoundResponse({

packages/next/src/utilities/createPayloadRequest.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,15 @@ export const createPayloadRequest = async ({
9797

9898
req.payloadDataLoader = getDataLoader(req)
9999

100-
req.user = await executeAuthStrategies({
100+
const { responseHeaders, user } = await executeAuthStrategies({
101101
headers: req.headers,
102102
isGraphQL,
103103
payload,
104104
})
105105

106+
req.user = user
107+
108+
if (responseHeaders) req.responseHeaders = responseHeaders
109+
106110
return req
107111
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const headersToJoin = ['set-cookie', 'warning', 'www-authenticate', 'proxy-authenticate', 'vary']
2+
3+
export function mergeHeaders(sourceHeaders: Headers, destinationHeaders: Headers): void {
4+
// Create a map to store combined headers
5+
const combinedHeaders = new Headers()
6+
7+
// Add existing destination headers to the combined map
8+
destinationHeaders.forEach((value, key) => {
9+
combinedHeaders.set(key, value)
10+
})
11+
12+
// Add source headers to the combined map, joining specific headers
13+
sourceHeaders.forEach((value, key) => {
14+
const lowerKey = key.toLowerCase()
15+
if (headersToJoin.includes(lowerKey)) {
16+
if (combinedHeaders.has(key)) {
17+
combinedHeaders.set(key, `${combinedHeaders.get(key)}, ${value}`)
18+
} else {
19+
combinedHeaders.set(key, value)
20+
}
21+
} else {
22+
combinedHeaders.set(key, value)
23+
}
24+
})
25+
26+
// Clear the destination headers and set the combined headers
27+
destinationHeaders.forEach((_, key) => {
28+
destinationHeaders.delete(key)
29+
})
30+
combinedHeaders.forEach((value, key) => {
31+
destinationHeaders.append(key, value)
32+
})
33+
}
Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
import type { TypedUser } from '../index.js'
2-
import type { AuthStrategyFunctionArgs } from './index.js'
1+
import type { AuthStrategyFunctionArgs, AuthStrategyResult } from './index.js'
32

43
export const executeAuthStrategies = async (
54
args: AuthStrategyFunctionArgs,
6-
): Promise<TypedUser | null> => {
7-
return args.payload.authStrategies.reduce(async (accumulatorPromise, strategy) => {
8-
const authUser = await accumulatorPromise
9-
if (!authUser) {
10-
return strategy.authenticate(args)
11-
}
12-
return authUser
13-
}, Promise.resolve(null))
5+
): Promise<AuthStrategyResult> => {
6+
return args.payload.authStrategies.reduce(
7+
async (accumulatorPromise, strategy) => {
8+
const result: AuthStrategyResult = await accumulatorPromise
9+
if (!result.user) {
10+
return strategy.authenticate(args)
11+
}
12+
return result
13+
},
14+
Promise.resolve({ user: null }),
15+
)
1416
}

packages/payload/src/auth/operations/auth.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type AuthArgs = {
1515

1616
export type AuthResult = {
1717
permissions: Permissions
18+
responseHeaders?: Headers
1819
user: TypedUser | null
1920
}
2021

@@ -26,12 +27,13 @@ export const auth = async (args: Required<AuthArgs>): Promise<AuthResult> => {
2627
try {
2728
const shouldCommit = await initTransaction(req)
2829

29-
const user = await executeAuthStrategies({
30+
const { responseHeaders, user } = await executeAuthStrategies({
3031
headers,
3132
payload,
3233
})
3334

3435
req.user = user
36+
req.responseHeaders = responseHeaders
3537

3638
const permissions = await getAccessResults({
3739
req,
@@ -41,6 +43,7 @@ export const auth = async (args: Required<AuthArgs>): Promise<AuthResult> => {
4143

4244
return {
4345
permissions,
46+
responseHeaders,
4447
user,
4548
}
4649
} catch (error: unknown) {

packages/payload/src/auth/strategies/apiKey.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,14 @@ export const APIKeyAuthentication =
4848
user.collection = collectionConfig.slug
4949
user._strategy = 'api-key'
5050

51-
return user as User
51+
return {
52+
user: user as User,
53+
}
5254
}
5355
} catch (err) {
54-
return null
56+
return { user: null }
5557
}
5658
}
5759

58-
return null
60+
return { user: null }
5961
}

packages/payload/src/auth/strategies/jwt.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@ export const JWTAuthentication: AuthStrategyFunction = async ({
2929
if (user && (!collection.config.auth.verify || user._verified)) {
3030
user.collection = collection.config.slug
3131
user._strategy = 'local-jwt'
32-
return user as User
32+
return {
33+
user: user as User,
34+
}
3335
} else {
34-
return null
36+
return { user: null }
3537
}
3638
} catch (error) {
37-
return null
39+
return { user: null }
3840
}
3941
}

packages/payload/src/auth/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,15 @@ export type AuthStrategyFunctionArgs = {
101101
isGraphQL?: boolean
102102
payload: Payload
103103
}
104+
105+
export type AuthStrategyResult = {
106+
responseHeaders?: Headers
107+
user: User | null
108+
}
109+
104110
export type AuthStrategyFunction = (
105111
args: AuthStrategyFunctionArgs,
106-
) => Promise<User | null> | User | null
112+
) => AuthStrategyResult | Promise<AuthStrategyResult>
107113
export type AuthStrategy = {
108114
authenticate: AuthStrategyFunction
109115
name: string

0 commit comments

Comments
 (0)