@10x-media/impersonation 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/access/filterTargets.js +51 -0
- package/dist/access/filterTargets.js.map +1 -0
- package/dist/auth/cookies.js +49 -0
- package/dist/auth/cookies.js.map +1 -0
- package/dist/auth/expiryCookies.js +59 -0
- package/dist/auth/expiryCookies.js.map +1 -0
- package/dist/auth/issue.js +126 -0
- package/dist/auth/issue.js.map +1 -0
- package/dist/auth/mode.js +69 -0
- package/dist/auth/mode.js.map +1 -0
- package/dist/auth/origin.js +55 -0
- package/dist/auth/origin.js.map +1 -0
- package/dist/auth/revoke.js +24 -0
- package/dist/auth/revoke.js.map +1 -0
- package/dist/auth/tenantCookie.js +74 -0
- package/dist/auth/tenantCookie.js.map +1 -0
- package/dist/endpoints/codes.d.ts +6 -0
- package/dist/endpoints/current.js +66 -0
- package/dist/endpoints/current.js.map +1 -0
- package/dist/endpoints/exit.js +242 -0
- package/dist/endpoints/exit.js.map +1 -0
- package/dist/endpoints/shared.js +76 -0
- package/dist/endpoints/shared.js.map +1 -0
- package/dist/endpoints/start.js +332 -0
- package/dist/endpoints/start.js.map +1 -0
- package/dist/endpoints/terminate.js +73 -0
- package/dist/endpoints/terminate.js.map +1 -0
- package/dist/exports/client.d.ts +9 -0
- package/dist/exports/client.js +10 -0
- package/dist/exports/i18n.d.ts +3 -0
- package/dist/exports/i18n.js +3 -0
- package/dist/exports/rsc.d.ts +4 -0
- package/dist/exports/rsc.js +4 -0
- package/dist/exports/types.d.ts +3 -0
- package/dist/exports/types.js +1 -0
- package/dist/getImpersonation.d.ts +49 -0
- package/dist/getImpersonation.js +78 -0
- package/dist/getImpersonation.js.map +1 -0
- package/dist/ids.js +27 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin/assertTargets.js +23 -0
- package/dist/plugin/assertTargets.js.map +1 -0
- package/dist/plugin/constants.d.ts +6 -0
- package/dist/plugin/constants.js +20 -0
- package/dist/plugin/constants.js.map +1 -0
- package/dist/plugin/decorateAuth.js +99 -0
- package/dist/plugin/decorateAuth.js.map +1 -0
- package/dist/plugin/lookup.js +23 -0
- package/dist/plugin/lookup.js.map +1 -0
- package/dist/plugin/normalizeOptions.js +83 -0
- package/dist/plugin/normalizeOptions.js.map +1 -0
- package/dist/plugin/registerAdmin.js +49 -0
- package/dist/plugin/registerAdmin.js.map +1 -0
- package/dist/plugin/registerCollection.js +183 -0
- package/dist/plugin/registerCollection.js.map +1 -0
- package/dist/plugin/registerEndpoints.js +19 -0
- package/dist/plugin/registerEndpoints.js.map +1 -0
- package/dist/plugin/registerHooks.js +59 -0
- package/dist/plugin/registerHooks.js.map +1 -0
- package/dist/plugin/registerRetention.js +57 -0
- package/dist/plugin/registerRetention.js.map +1 -0
- package/dist/plugin/registerTranslations.js +19 -0
- package/dist/plugin/registerTranslations.js.map +1 -0
- package/dist/plugin/registry.js +11 -0
- package/dist/plugin/registry.js.map +1 -0
- package/dist/plugin/startable.js +21 -0
- package/dist/plugin/startable.js.map +1 -0
- package/dist/session/close.js +65 -0
- package/dist/session/close.js.map +1 -0
- package/dist/session/closeStale.d.ts +11 -0
- package/dist/session/closeStale.js +65 -0
- package/dist/session/closeStale.js.map +1 -0
- package/dist/session/locks.js +32 -0
- package/dist/session/locks.js.map +1 -0
- package/dist/session/resolve.js +91 -0
- package/dist/session/resolve.js.map +1 -0
- package/dist/translations/ar.js +52 -0
- package/dist/translations/ar.js.map +1 -0
- package/dist/translations/de.js +52 -0
- package/dist/translations/de.js.map +1 -0
- package/dist/translations/en.js +52 -0
- package/dist/translations/en.js.map +1 -0
- package/dist/translations/es.js +52 -0
- package/dist/translations/es.js.map +1 -0
- package/dist/translations/fr.js +52 -0
- package/dist/translations/fr.js.map +1 -0
- package/dist/translations/id.js +52 -0
- package/dist/translations/id.js.map +1 -0
- package/dist/translations/index.d.ts +24 -0
- package/dist/translations/index.js +49 -0
- package/dist/translations/index.js.map +1 -0
- package/dist/translations/keys.d.ts +56 -0
- package/dist/translations/keys.js +56 -0
- package/dist/translations/keys.js.map +1 -0
- package/dist/translations/ko.js +52 -0
- package/dist/translations/ko.js.map +1 -0
- package/dist/translations/lookup.js +17 -0
- package/dist/translations/lookup.js.map +1 -0
- package/dist/translations/pt.js +52 -0
- package/dist/translations/pt.js.map +1 -0
- package/dist/translations/ru.js +52 -0
- package/dist/translations/ru.js.map +1 -0
- package/dist/translations/server.js +13 -0
- package/dist/translations/server.js.map +1 -0
- package/dist/translations/uk.js +52 -0
- package/dist/translations/uk.js.map +1 -0
- package/dist/translations/useTranslation.js +12 -0
- package/dist/translations/useTranslation.js.map +1 -0
- package/dist/translations/zh.js +52 -0
- package/dist/translations/zh.js.map +1 -0
- package/dist/types.d.ts +214 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/EndSessionMenuItem.d.ts +5 -0
- package/dist/ui/EndSessionMenuItem.js +45 -0
- package/dist/ui/EndSessionMenuItem.js.map +1 -0
- package/dist/ui/ImpersonateIcon.js +20 -0
- package/dist/ui/ImpersonateIcon.js.map +1 -0
- package/dist/ui/ImpersonationAction.d.ts +10 -0
- package/dist/ui/ImpersonationAction.js +48 -0
- package/dist/ui/ImpersonationAction.js.map +1 -0
- package/dist/ui/ImpersonationBar.d.ts +36 -0
- package/dist/ui/ImpersonationBar.js +357 -0
- package/dist/ui/ImpersonationBar.js.map +1 -0
- package/dist/ui/ImpersonationConfig.js +25 -0
- package/dist/ui/ImpersonationConfig.js.map +1 -0
- package/dist/ui/ImpersonationDocumentButton.d.ts +5 -0
- package/dist/ui/ImpersonationDocumentButton.js +85 -0
- package/dist/ui/ImpersonationDocumentButton.js.map +1 -0
- package/dist/ui/ImpersonationFrontendBanner.d.ts +41 -0
- package/dist/ui/ImpersonationFrontendBanner.js +58 -0
- package/dist/ui/ImpersonationFrontendBanner.js.map +1 -0
- package/dist/ui/ImpersonationFrontendExit.d.ts +17 -0
- package/dist/ui/ImpersonationFrontendExit.js +45 -0
- package/dist/ui/ImpersonationFrontendExit.js.map +1 -0
- package/dist/ui/ImpersonationProvider.d.ts +16 -0
- package/dist/ui/ImpersonationProvider.js +127 -0
- package/dist/ui/ImpersonationProvider.js.map +1 -0
- package/dist/ui/ImpersonationSwitcher.d.ts +21 -0
- package/dist/ui/ImpersonationSwitcher.js +251 -0
- package/dist/ui/ImpersonationSwitcher.js.map +1 -0
- package/dist/ui/ImpersonationUserCard.d.ts +31 -0
- package/dist/ui/ImpersonationUserCard.js +51 -0
- package/dist/ui/ImpersonationUserCard.js.map +1 -0
- package/dist/ui/StartConfirmModal.d.ts +28 -0
- package/dist/ui/StartConfirmModal.js +65 -0
- package/dist/ui/StartConfirmModal.js.map +1 -0
- package/dist/ui/api.js +66 -0
- package/dist/ui/api.js.map +1 -0
- package/dist/ui/barPlacement.js +107 -0
- package/dist/ui/barPlacement.js.map +1 -0
- package/dist/ui/frontendBanner.css +22 -0
- package/dist/ui/impersonation.css +244 -0
- package/dist/ui/useImpersonation.d.ts +43 -0
- package/dist/ui/useImpersonation.js +36 -0
- package/dist/ui/useImpersonation.js.map +1 -0
- package/package.json +126 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @10x-media/impersonation
|
|
2
|
+
|
|
3
|
+
## 0.1.0-beta.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Sessions collection accepts `collection.overrides`. `useImpersonation().targetFilters` is the filter map (the `targets` option is unchanged). `decorateRequests: false` still enforces `maxDuration`. The admin bar, switcher, and frontend banner pick up the review's layout and copy fixes.
|
|
8
|
+
|
|
9
|
+
- Show the sessions collection only when `ui.sessionsCollection` is on, hide End session on a closed row, and clear impersonation cookies when a session hits its absolute expiry.
|
|
10
|
+
|
|
11
|
+
- Replace the switcher row with your own card and end a session from the document controls.
|
|
12
|
+
|
|
13
|
+
- Initial beta of `@10x-media/impersonation`: an authorised account can sign in as another user without their password. After start, `req.user` is the target. Every session is a closed-never-deleted row. Exit, logout, or remote terminate ends it.
|
|
14
|
+
|
|
15
|
+
- **Mandatory `access.impersonate`**: only the literal `true` allows. A Payload `Where` object is deny. Start also reads the target with `overrideAccess: false`.
|
|
16
|
+
- **Minted target session**: `db.findOne` + `addSessionToUser` + `jwtSign` + cookie. Active means an open row whose `targetSid` matches `req.user._sid`, so Payload refresh cannot hide the bar.
|
|
17
|
+
- **Plugin-owned origin gate** on cookie POSTs. JWT/Bearer skip it only when `jwtOrder` ranks that scheme before `cookie`.
|
|
18
|
+
- **Swap or parallel**: parallel only when `@10x-media/dual-session` isolated the target. Hint cookie is never trusted alone.
|
|
19
|
+
- **Admin UI**: Switch to user, Acting as bar (including `/admin/unauthorized`), document action, End session on the record.
|
|
20
|
+
|
|
21
|
+
Requires Payload `^3.83.0`. Dual-session is an optional peer.
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- Keep the impersonation bar beside the nav, and collapse it to a chip that snaps to the edges of the content area.
|
|
26
|
+
|
|
27
|
+
- Admin impersonation UI uses Payload's native drawer, confirmation modal, inputs, and theme tokens.
|
|
28
|
+
|
|
29
|
+
- Rebuild the Impersonate UI (Payload SearchFilter, compact Button margins, card drawer/document actions, pagination, nav-aware bar, countdown), always register the provider with `useImpersonation`, add `filterTargets`, a frontend banner, opt-in retention, and skip session lookups on ordinary traffic.
|
|
30
|
+
|
|
31
|
+
- Restore the impersonator's tenant cookie on exit, and set the target's when they belong to exactly one tenant.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 10x Media GmbH
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# @10x-media/impersonation
|
|
4
|
+
|
|
5
|
+
Let an authorised account sign in as another user without their password, then return. Every session is recorded. After start, `req.user` is the target.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@10x-media/impersonation)
|
|
8
|
+
|
|
9
|
+
Part of the [@10x-media Payload plugins](https://github.com/10x-media/payload-plugins) collection. In beta: published under the `beta` dist-tag until a stable 1.0.
|
|
10
|
+
|
|
11
|
+
> [!WARNING]
|
|
12
|
+
> **Experimental.** This plugin mints and revokes sessions. That is not a place where a subtle bug announces itself. It is covered by unit, integration and end-to-end tests against Payload's real routing on Mongo and Postgres, but auth surfaces differ a lot between projects. Try it in staging first, and [report anything that looks off](https://github.com/10x-media/payload-plugins/issues).
|
|
13
|
+
|
|
14
|
+
## Quick start
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm add @10x-media/impersonation
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { impersonation } from '@10x-media/impersonation'
|
|
22
|
+
|
|
23
|
+
export default buildConfig({
|
|
24
|
+
plugins: [
|
|
25
|
+
impersonation({
|
|
26
|
+
access: {
|
|
27
|
+
impersonate: ({ req }) => req.user?.roles?.includes('admin') === true,
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
],
|
|
31
|
+
})
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`access.impersonate` is required. Only the literal `true` allows.
|
|
35
|
+
|
|
36
|
+
The admin header gets **Impersonate**. Confirm, and you are that user until you exit. A bar on every admin route (including `/admin/unauthorized`) shows **Acting as** and **Return to**.
|
|
37
|
+
|
|
38
|
+
## Documentation
|
|
39
|
+
|
|
40
|
+
Full documentation at [docs.10xmedia.de](https://docs.10xmedia.de/impersonation):
|
|
41
|
+
|
|
42
|
+
- [Overview](https://docs.10xmedia.de/impersonation)
|
|
43
|
+
- [Quick start](https://docs.10xmedia.de/impersonation/quick-start)
|
|
44
|
+
- [Configuration](https://docs.10xmedia.de/impersonation/configuration)
|
|
45
|
+
- [Security](https://docs.10xmedia.de/impersonation/security)
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
|
|
49
|
+
[MIT](./LICENSE). Copyright 10x Media GmbH.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//#region src/access/filterTargets.ts
|
|
2
|
+
const isWhere = (value) => Boolean(value) && typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the per-collection list filter for the current viewer. Collections
|
|
5
|
+
* that return `false` (or anything other than `true` / a Where) are omitted.
|
|
6
|
+
*/
|
|
7
|
+
const resolveTargetFilters = async ({ collections, options, req }) => {
|
|
8
|
+
const out = {};
|
|
9
|
+
for (const slug of collections) {
|
|
10
|
+
if (!options.access.filterTargets) {
|
|
11
|
+
out[slug] = true;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
let result;
|
|
15
|
+
try {
|
|
16
|
+
result = await options.access.filterTargets({
|
|
17
|
+
req,
|
|
18
|
+
targetCollection: slug
|
|
19
|
+
});
|
|
20
|
+
} catch (error) {
|
|
21
|
+
req.payload.logger.error({
|
|
22
|
+
err: error,
|
|
23
|
+
msg: `@10x-media/impersonation: access.filterTargets threw for "${slug}"`
|
|
24
|
+
});
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (result === true) {
|
|
28
|
+
out[slug] = true;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (result === false || !isWhere(result)) continue;
|
|
32
|
+
out[slug] = result;
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
};
|
|
36
|
+
const targetMatchesFilter = async ({ collection, filter, req, targetId }) => {
|
|
37
|
+
if (filter === true) return true;
|
|
38
|
+
return (await req.payload.find({
|
|
39
|
+
collection,
|
|
40
|
+
depth: 0,
|
|
41
|
+
limit: 1,
|
|
42
|
+
overrideAccess: true,
|
|
43
|
+
pagination: false,
|
|
44
|
+
req,
|
|
45
|
+
where: { and: [{ id: { equals: targetId } }, filter] }
|
|
46
|
+
})).docs.length > 0;
|
|
47
|
+
};
|
|
48
|
+
//#endregion
|
|
49
|
+
export { resolveTargetFilters, targetMatchesFilter };
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=filterTargets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filterTargets.js","names":[],"sources":["../../src/access/filterTargets.ts"],"sourcesContent":["import type { CollectionSlug, PayloadRequest, Where } from 'payload'\n\nimport type { ResolvedOptions } from '../types'\n\nexport type TargetFilterMap = Record<string, true | Where>\n\nconst isWhere = (value: unknown): value is Where =>\n\tBoolean(value) && typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/**\n * Resolve the per-collection list filter for the current viewer. Collections\n * that return `false` (or anything other than `true` / a Where) are omitted.\n */\nexport const resolveTargetFilters = async ({\n\tcollections,\n\toptions,\n\treq,\n}: {\n\tcollections: CollectionSlug[]\n\toptions: ResolvedOptions\n\treq: PayloadRequest\n}): Promise<TargetFilterMap> => {\n\tconst out: TargetFilterMap = {}\n\tfor (const slug of collections) {\n\t\tif (!options.access.filterTargets) {\n\t\t\tout[slug] = true\n\t\t\tcontinue\n\t\t}\n\t\tlet result: boolean | Where\n\t\ttry {\n\t\t\tresult = await options.access.filterTargets({ req, targetCollection: slug })\n\t\t} catch (error) {\n\t\t\treq.payload.logger.error({\n\t\t\t\terr: error,\n\t\t\t\tmsg: `@10x-media/impersonation: access.filterTargets threw for \"${slug}\"`,\n\t\t\t})\n\t\t\tcontinue\n\t\t}\n\t\tif (result === true) {\n\t\t\tout[slug] = true\n\t\t\tcontinue\n\t\t}\n\t\tif (result === false || !isWhere(result)) {\n\t\t\tcontinue\n\t\t}\n\t\tout[slug] = result\n\t}\n\treturn out\n}\n\nexport const targetMatchesFilter = async ({\n\tcollection,\n\tfilter,\n\treq,\n\ttargetId,\n}: {\n\tcollection: CollectionSlug\n\tfilter: true | Where\n\treq: PayloadRequest\n\ttargetId: number | string\n}): Promise<boolean> => {\n\tif (filter === true) {\n\t\treturn true\n\t}\n\tconst found = await req.payload.find({\n\t\tcollection,\n\t\tdepth: 0,\n\t\tlimit: 1,\n\t\toverrideAccess: true,\n\t\tpagination: false,\n\t\treq,\n\t\twhere: { and: [{ id: { equals: targetId } }, filter] },\n\t})\n\treturn found.docs.length > 0\n}\n"],"mappings":";AAMA,MAAM,WAAW,UAChB,QAAQ,KAAK,KAAK,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;;;;;AAMtF,MAAa,uBAAuB,OAAO,EAC1C,aACA,SACA,UAK+B;CAC/B,MAAM,MAAuB,CAAC;CAC9B,KAAK,MAAM,QAAQ,aAAa;EAC/B,IAAI,CAAC,QAAQ,OAAO,eAAe;GAClC,IAAI,QAAQ;GACZ;EACD;EACA,IAAI;EACJ,IAAI;GACH,SAAS,MAAM,QAAQ,OAAO,cAAc;IAAE;IAAK,kBAAkB;GAAK,CAAC;EAC5E,SAAS,OAAO;GACf,IAAI,QAAQ,OAAO,MAAM;IACxB,KAAK;IACL,KAAK,6DAA6D,KAAK;GACxE,CAAC;GACD;EACD;EACA,IAAI,WAAW,MAAM;GACpB,IAAI,QAAQ;GACZ;EACD;EACA,IAAI,WAAW,SAAS,CAAC,QAAQ,MAAM,GACtC;EAED,IAAI,QAAQ;CACb;CACA,OAAO;AACR;AAEA,MAAa,sBAAsB,OAAO,EACzC,YACA,QACA,KACA,eAMuB;CACvB,IAAI,WAAW,MACd,OAAO;CAWR,QAAO,MATa,IAAI,QAAQ,KAAK;EACpC;EACA,OAAO;EACP,OAAO;EACP,gBAAgB;EAChB,YAAY;EACZ;EACA,OAAO,EAAE,KAAK,CAAC,EAAE,IAAI,EAAE,QAAQ,SAAS,EAAE,GAAG,MAAM,EAAE;CACtD,CAAC,GACY,KAAK,SAAS;AAC5B"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { generateCookie, generateExpiredPayloadCookie, getCookieExpiration } from "payload/shared";
|
|
2
|
+
//#region src/auth/cookies.ts
|
|
3
|
+
const resolveSameSite = (sameSite) => {
|
|
4
|
+
if (typeof sameSite === "string") return sameSite;
|
|
5
|
+
return sameSite ? "Strict" : void 0;
|
|
6
|
+
};
|
|
7
|
+
const sharedCookieName = (cookiePrefix) => `${cookiePrefix}-token`;
|
|
8
|
+
const writeCookie = ({ authConfig, httpOnly = true, name, value }) => generateCookie({
|
|
9
|
+
name,
|
|
10
|
+
domain: authConfig.cookies.domain ?? void 0,
|
|
11
|
+
expires: getCookieExpiration({ seconds: authConfig.tokenExpiration }),
|
|
12
|
+
httpOnly,
|
|
13
|
+
path: "/",
|
|
14
|
+
returnCookieAsObject: false,
|
|
15
|
+
sameSite: resolveSameSite(authConfig.cookies.sameSite),
|
|
16
|
+
secure: authConfig.cookies.secure,
|
|
17
|
+
value
|
|
18
|
+
});
|
|
19
|
+
const generateHintCookie = ({ authConfig, name, value }) => writeCookie({
|
|
20
|
+
authConfig,
|
|
21
|
+
name,
|
|
22
|
+
value
|
|
23
|
+
});
|
|
24
|
+
const expireCookie = ({ authConfig, httpOnly = true, name }) => generateCookie({
|
|
25
|
+
name,
|
|
26
|
+
domain: authConfig.cookies.domain ?? void 0,
|
|
27
|
+
expires: /* @__PURE__ */ new Date(Date.now() - 1e3),
|
|
28
|
+
httpOnly,
|
|
29
|
+
path: "/",
|
|
30
|
+
returnCookieAsObject: false,
|
|
31
|
+
sameSite: resolveSameSite(authConfig.cookies.sameSite),
|
|
32
|
+
secure: authConfig.cookies.secure,
|
|
33
|
+
value: ""
|
|
34
|
+
});
|
|
35
|
+
const expirePayloadCookie = ({ authConfig, cookiePrefix }) => generateExpiredPayloadCookie({
|
|
36
|
+
collectionAuthConfig: authConfig,
|
|
37
|
+
cookiePrefix
|
|
38
|
+
});
|
|
39
|
+
const expireCookies = ({ authConfig, cookiePrefix, names }) => names.map((name) => name === sharedCookieName(cookiePrefix) ? expirePayloadCookie({
|
|
40
|
+
authConfig,
|
|
41
|
+
cookiePrefix
|
|
42
|
+
}) : expireCookie({
|
|
43
|
+
authConfig,
|
|
44
|
+
name
|
|
45
|
+
}));
|
|
46
|
+
//#endregion
|
|
47
|
+
export { expireCookie, expireCookies, expirePayloadCookie, generateHintCookie, sharedCookieName, writeCookie };
|
|
48
|
+
|
|
49
|
+
//# sourceMappingURL=cookies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookies.js","names":[],"sources":["../../src/auth/cookies.ts"],"sourcesContent":["import type { SanitizedCollectionConfig } from 'payload'\nimport { generateCookie, generateExpiredPayloadCookie, getCookieExpiration } from 'payload/shared'\n\ntype AuthConfig = SanitizedCollectionConfig['auth']\n\nconst resolveSameSite = (sameSite: AuthConfig['cookies']['sameSite']) => {\n\tif (typeof sameSite === 'string') {\n\t\treturn sameSite\n\t}\n\treturn sameSite ? ('Strict' as const) : undefined\n}\n\nexport const sharedCookieName = (cookiePrefix: string) => `${cookiePrefix}-token`\n\nexport const writeCookie = ({\n\tauthConfig,\n\thttpOnly = true,\n\tname,\n\tvalue,\n}: {\n\tauthConfig: AuthConfig\n\thttpOnly?: boolean\n\tname: string\n\tvalue: string\n}) =>\n\tgenerateCookie<false>({\n\t\tname,\n\t\tdomain: authConfig.cookies.domain ?? undefined,\n\t\texpires: getCookieExpiration({ seconds: authConfig.tokenExpiration }),\n\t\thttpOnly,\n\t\tpath: '/',\n\t\treturnCookieAsObject: false,\n\t\tsameSite: resolveSameSite(authConfig.cookies.sameSite),\n\t\tsecure: authConfig.cookies.secure,\n\t\tvalue,\n\t})\n\nexport const generateHintCookie = ({\n\tauthConfig,\n\tname,\n\tvalue,\n}: {\n\tauthConfig: AuthConfig\n\tname: string\n\tvalue: string\n}) => writeCookie({ authConfig, name, value })\n\nexport const expireCookie = ({\n\tauthConfig,\n\thttpOnly = true,\n\tname,\n}: {\n\tauthConfig: AuthConfig\n\thttpOnly?: boolean\n\tname: string\n}) =>\n\tgenerateCookie<false>({\n\t\tname,\n\t\tdomain: authConfig.cookies.domain ?? undefined,\n\t\texpires: new Date(Date.now() - 1000),\n\t\thttpOnly,\n\t\tpath: '/',\n\t\treturnCookieAsObject: false,\n\t\tsameSite: resolveSameSite(authConfig.cookies.sameSite),\n\t\tsecure: authConfig.cookies.secure,\n\t\tvalue: '',\n\t})\n\nexport const expirePayloadCookie = ({\n\tauthConfig,\n\tcookiePrefix,\n}: {\n\tauthConfig: AuthConfig\n\tcookiePrefix: string\n}) => generateExpiredPayloadCookie({ collectionAuthConfig: authConfig, cookiePrefix })\n\nexport const expireCookies = ({\n\tauthConfig,\n\tcookiePrefix,\n\tnames,\n}: {\n\tauthConfig: AuthConfig\n\tcookiePrefix: string\n\tnames: string[]\n}): string[] =>\n\tnames.map((name) =>\n\t\tname === sharedCookieName(cookiePrefix)\n\t\t\t? expirePayloadCookie({ authConfig, cookiePrefix })\n\t\t\t: expireCookie({ authConfig, name })\n\t)\n"],"mappings":";;AAKA,MAAM,mBAAmB,aAAgD;CACxE,IAAI,OAAO,aAAa,UACvB,OAAO;CAER,OAAO,WAAY,WAAqB,KAAA;AACzC;AAEA,MAAa,oBAAoB,iBAAyB,GAAG,aAAa;AAE1E,MAAa,eAAe,EAC3B,YACA,WAAW,MACX,MACA,YAOA,eAAsB;CACrB;CACA,QAAQ,WAAW,QAAQ,UAAU,KAAA;CACrC,SAAS,oBAAoB,EAAE,SAAS,WAAW,gBAAgB,CAAC;CACpE;CACA,MAAM;CACN,sBAAsB;CACtB,UAAU,gBAAgB,WAAW,QAAQ,QAAQ;CACrD,QAAQ,WAAW,QAAQ;CAC3B;AACD,CAAC;AAEF,MAAa,sBAAsB,EAClC,YACA,MACA,YAKK,YAAY;CAAE;CAAY;CAAM;AAAM,CAAC;AAE7C,MAAa,gBAAgB,EAC5B,YACA,WAAW,MACX,WAMA,eAAsB;CACrB;CACA,QAAQ,WAAW,QAAQ,UAAU,KAAA;CACrC,yBAAS,IAAI,KAAK,KAAK,IAAI,IAAI,GAAI;CACnC;CACA,MAAM;CACN,sBAAsB;CACtB,UAAU,gBAAgB,WAAW,QAAQ,QAAQ;CACrD,QAAQ,WAAW,QAAQ;CAC3B,OAAO;AACR,CAAC;AAEF,MAAa,uBAAuB,EACnC,YACA,mBAIK,6BAA6B;CAAE,sBAAsB;CAAY;AAAa,CAAC;AAErF,MAAa,iBAAiB,EAC7B,YACA,cACA,YAMA,MAAM,KAAK,SACV,SAAS,iBAAiB,YAAY,IACnC,oBAAoB;CAAE;CAAY;AAAa,CAAC,IAChD,aAAa;CAAE;CAAY;AAAK,CAAC,CACrC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { collectionBySlug } from "../ids.js";
|
|
2
|
+
import { relationOf } from "../session/resolve.js";
|
|
3
|
+
import { expireCookie, expirePayloadCookie } from "./cookies.js";
|
|
4
|
+
import { isolatedCookieNameFor } from "./mode.js";
|
|
5
|
+
//#region src/auth/expiryCookies.ts
|
|
6
|
+
/**
|
|
7
|
+
* Set-Cookie values for an absolute expiry. Parallel expiry does not clear the
|
|
8
|
+
* shared impersonator token. Swap expiry does, because start overwrote it.
|
|
9
|
+
*/
|
|
10
|
+
const absoluteExpiryCookies = async ({ options, payload, row, sid, user }) => {
|
|
11
|
+
const impersonator = relationOf(row.impersonator);
|
|
12
|
+
const target = relationOf(row.target);
|
|
13
|
+
const isImpersonatorRequest = sid === row.impersonatorSid && sid !== row.targetSid;
|
|
14
|
+
const targetAuth = target ? collectionBySlug(payload, target.collection)?.config.auth : void 0;
|
|
15
|
+
const impersonatorAuth = impersonator && collectionBySlug(payload, impersonator.collection)?.config.auth || collectionBySlug(payload, payload.config.admin.user)?.config.auth;
|
|
16
|
+
const cookieAuth = targetAuth ?? impersonatorAuth;
|
|
17
|
+
if (!cookieAuth) return [];
|
|
18
|
+
const cookies = [expireCookie({
|
|
19
|
+
authConfig: cookieAuth,
|
|
20
|
+
name: options.hintCookieName
|
|
21
|
+
})];
|
|
22
|
+
if (row.mode === "parallel" && target && targetAuth) {
|
|
23
|
+
const slotUser = isImpersonatorRequest ? await findSlotUser(payload, target) : user;
|
|
24
|
+
const isolatedName = await isolatedCookieNameFor({
|
|
25
|
+
collection: target.collection,
|
|
26
|
+
payload,
|
|
27
|
+
user: slotUser
|
|
28
|
+
});
|
|
29
|
+
if (isolatedName) cookies.push(expireCookie({
|
|
30
|
+
authConfig: targetAuth,
|
|
31
|
+
name: isolatedName
|
|
32
|
+
}));
|
|
33
|
+
return cookies;
|
|
34
|
+
}
|
|
35
|
+
if (impersonatorAuth) cookies.push(expirePayloadCookie({
|
|
36
|
+
authConfig: impersonatorAuth,
|
|
37
|
+
cookiePrefix: payload.config.cookiePrefix
|
|
38
|
+
}));
|
|
39
|
+
return cookies;
|
|
40
|
+
};
|
|
41
|
+
const findSlotUser = async (payload, target) => {
|
|
42
|
+
try {
|
|
43
|
+
return await payload.findByID({
|
|
44
|
+
id: target.id,
|
|
45
|
+
collection: target.collection,
|
|
46
|
+
depth: 0,
|
|
47
|
+
overrideAccess: true
|
|
48
|
+
});
|
|
49
|
+
} catch {
|
|
50
|
+
return {
|
|
51
|
+
collection: target.collection,
|
|
52
|
+
id: target.id
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
//#endregion
|
|
57
|
+
export { absoluteExpiryCookies };
|
|
58
|
+
|
|
59
|
+
//# sourceMappingURL=expiryCookies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expiryCookies.js","names":[],"sources":["../../src/auth/expiryCookies.ts"],"sourcesContent":["import type { CollectionSlug, Payload, TypedUser } from 'payload'\n\nimport { collectionBySlug } from '../ids'\nimport { relationOf } from '../session/resolve'\nimport type { ImpersonationRecord, ResolvedOptions } from '../types'\nimport { expireCookie, expirePayloadCookie } from './cookies'\nimport { isolatedCookieNameFor } from './mode'\n\n/**\n * Set-Cookie values for an absolute expiry. Parallel expiry does not clear the\n * shared impersonator token. Swap expiry does, because start overwrote it.\n */\nexport const absoluteExpiryCookies = async ({\n\toptions,\n\tpayload,\n\trow,\n\tsid,\n\tuser,\n}: {\n\toptions: ResolvedOptions\n\tpayload: Payload\n\trow: ImpersonationRecord\n\tsid: string\n\tuser: TypedUser\n}): Promise<string[]> => {\n\tconst impersonator = relationOf(row.impersonator)\n\tconst target = relationOf(row.target)\n\tconst isImpersonatorRequest = sid === row.impersonatorSid && sid !== row.targetSid\n\tconst targetAuth = target ? collectionBySlug(payload, target.collection)?.config.auth : undefined\n\tconst impersonatorAuth =\n\t\t(impersonator && collectionBySlug(payload, impersonator.collection)?.config.auth) ||\n\t\tcollectionBySlug(payload, payload.config.admin.user)?.config.auth\n\tconst cookieAuth = targetAuth ?? impersonatorAuth\n\tif (!cookieAuth) {\n\t\treturn []\n\t}\n\n\tconst cookies = [expireCookie({ authConfig: cookieAuth, name: options.hintCookieName })]\n\n\tif (row.mode === 'parallel' && target && targetAuth) {\n\t\tconst slotUser = isImpersonatorRequest ? await findSlotUser(payload, target) : user\n\t\tconst isolatedName = await isolatedCookieNameFor({\n\t\t\tcollection: target.collection as CollectionSlug,\n\t\t\tpayload,\n\t\t\tuser: slotUser,\n\t\t})\n\t\tif (isolatedName) {\n\t\t\tcookies.push(expireCookie({ authConfig: targetAuth, name: isolatedName }))\n\t\t}\n\t\treturn cookies\n\t}\n\n\tif (impersonatorAuth) {\n\t\tcookies.push(\n\t\t\texpirePayloadCookie({\n\t\t\t\tauthConfig: impersonatorAuth,\n\t\t\t\tcookiePrefix: payload.config.cookiePrefix,\n\t\t\t})\n\t\t)\n\t}\n\treturn cookies\n}\n\nconst findSlotUser = async (\n\tpayload: Payload,\n\ttarget: { collection: string; id: number | string }\n): Promise<TypedUser> => {\n\ttry {\n\t\treturn (await payload.findByID({\n\t\t\tid: target.id,\n\t\t\tcollection: target.collection as CollectionSlug,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t})) as TypedUser\n\t} catch {\n\t\treturn { collection: target.collection, id: target.id } as TypedUser\n\t}\n}\n"],"mappings":";;;;;;;;;AAYA,MAAa,wBAAwB,OAAO,EAC3C,SACA,SACA,KACA,KACA,WAOwB;CACxB,MAAM,eAAe,WAAW,IAAI,YAAY;CAChD,MAAM,SAAS,WAAW,IAAI,MAAM;CACpC,MAAM,wBAAwB,QAAQ,IAAI,mBAAmB,QAAQ,IAAI;CACzE,MAAM,aAAa,SAAS,iBAAiB,SAAS,OAAO,UAAU,GAAG,OAAO,OAAO,KAAA;CACxF,MAAM,mBACJ,gBAAgB,iBAAiB,SAAS,aAAa,UAAU,GAAG,OAAO,QAC5E,iBAAiB,SAAS,QAAQ,OAAO,MAAM,IAAI,GAAG,OAAO;CAC9D,MAAM,aAAa,cAAc;CACjC,IAAI,CAAC,YACJ,OAAO,CAAC;CAGT,MAAM,UAAU,CAAC,aAAa;EAAE,YAAY;EAAY,MAAM,QAAQ;CAAe,CAAC,CAAC;CAEvF,IAAI,IAAI,SAAS,cAAc,UAAU,YAAY;EACpD,MAAM,WAAW,wBAAwB,MAAM,aAAa,SAAS,MAAM,IAAI;EAC/E,MAAM,eAAe,MAAM,sBAAsB;GAChD,YAAY,OAAO;GACnB;GACA,MAAM;EACP,CAAC;EACD,IAAI,cACH,QAAQ,KAAK,aAAa;GAAE,YAAY;GAAY,MAAM;EAAa,CAAC,CAAC;EAE1E,OAAO;CACR;CAEA,IAAI,kBACH,QAAQ,KACP,oBAAoB;EACnB,YAAY;EACZ,cAAc,QAAQ,OAAO;CAC9B,CAAC,CACF;CAED,OAAO;AACR;AAEA,MAAM,eAAe,OACpB,SACA,WACwB;CACxB,IAAI;EACH,OAAQ,MAAM,QAAQ,SAAS;GAC9B,IAAI,OAAO;GACX,YAAY,OAAO;GACnB,OAAO;GACP,gBAAgB;EACjB,CAAC;CACF,QAAQ;EACP,OAAO;GAAE,YAAY,OAAO;GAAY,IAAI,OAAO;EAAG;CACvD;AACD"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { sharedCookieName } from "./cookies.js";
|
|
2
|
+
import { isolatedAuthCookie, resolveMode } from "./mode.js";
|
|
3
|
+
import { IMPERSONATION_SID_PREFIX } from "../plugin/constants.js";
|
|
4
|
+
import { getFieldsToSign, jwtSign } from "payload";
|
|
5
|
+
import { generatePayloadCookie } from "payload/shared";
|
|
6
|
+
//#region src/auth/issue.ts
|
|
7
|
+
const tokenExpirationOf = (auth) => {
|
|
8
|
+
if (auth === true || auth === false) return 7200;
|
|
9
|
+
return auth.tokenExpiration ?? 7200;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Mint a target session with a prefixed sid so decorateAuth can skip the DB
|
|
13
|
+
* on requests that cannot be impersonation.
|
|
14
|
+
*/
|
|
15
|
+
const mintPrefixedSession = async ({ collection, collectionConfig, payload, raw, req }) => {
|
|
16
|
+
if (typeof collectionConfig.auth === "object" && collectionConfig.auth.useSessions === false) throw new Error(`@10x-media/impersonation: "${collection}" did not mint a session id (useSessions is off)`);
|
|
17
|
+
const now = /* @__PURE__ */ new Date();
|
|
18
|
+
const sid = `${IMPERSONATION_SID_PREFIX}${crypto.randomUUID()}`;
|
|
19
|
+
const sessions = [...(raw.sessions ?? []).filter((session) => new Date(session.expiresAt) > now), {
|
|
20
|
+
createdAt: now.toISOString(),
|
|
21
|
+
expiresAt: new Date(now.getTime() + tokenExpirationOf(collectionConfig.auth) * 1e3).toISOString(),
|
|
22
|
+
id: sid
|
|
23
|
+
}];
|
|
24
|
+
await payload.db.updateOne({
|
|
25
|
+
collection,
|
|
26
|
+
data: { sessions },
|
|
27
|
+
id: raw.id,
|
|
28
|
+
req,
|
|
29
|
+
returning: false
|
|
30
|
+
});
|
|
31
|
+
raw.sessions = sessions;
|
|
32
|
+
return sid;
|
|
33
|
+
};
|
|
34
|
+
const issueSession = async ({ collection, payload, req, userId }) => {
|
|
35
|
+
const registered = payload.collections[collection];
|
|
36
|
+
if (!registered) throw new Error(`@10x-media/impersonation: collection "${collection}" is not registered`);
|
|
37
|
+
const collectionConfig = registered.config;
|
|
38
|
+
const raw = await payload.db.findOne({
|
|
39
|
+
collection,
|
|
40
|
+
req,
|
|
41
|
+
where: { id: { equals: userId } }
|
|
42
|
+
});
|
|
43
|
+
if (!raw) throw new Error(`@10x-media/impersonation: target ${collection}/${userId} was not found`);
|
|
44
|
+
raw.collection = collectionConfig.slug;
|
|
45
|
+
const sid = await mintPrefixedSession({
|
|
46
|
+
collection,
|
|
47
|
+
collectionConfig,
|
|
48
|
+
payload,
|
|
49
|
+
raw,
|
|
50
|
+
req
|
|
51
|
+
});
|
|
52
|
+
const { exp, token } = await jwtSign({
|
|
53
|
+
fieldsToSign: getFieldsToSign({
|
|
54
|
+
collectionConfig,
|
|
55
|
+
email: raw.email ?? "",
|
|
56
|
+
sid,
|
|
57
|
+
user: raw
|
|
58
|
+
}),
|
|
59
|
+
secret: payload.secret,
|
|
60
|
+
tokenExpiration: tokenExpirationOf(collectionConfig.auth)
|
|
61
|
+
});
|
|
62
|
+
const { cookieName, isolated } = await resolveMode({
|
|
63
|
+
collection,
|
|
64
|
+
payload,
|
|
65
|
+
user: raw
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
cookie: isolated ? await isolatedAuthCookie({
|
|
69
|
+
collection,
|
|
70
|
+
payload,
|
|
71
|
+
token,
|
|
72
|
+
user: raw
|
|
73
|
+
}) ?? generatePayloadCookie({
|
|
74
|
+
collectionAuthConfig: collectionConfig.auth,
|
|
75
|
+
cookiePrefix: payload.config.cookiePrefix,
|
|
76
|
+
token
|
|
77
|
+
}) : generatePayloadCookie({
|
|
78
|
+
collectionAuthConfig: collectionConfig.auth,
|
|
79
|
+
cookiePrefix: payload.config.cookiePrefix,
|
|
80
|
+
token
|
|
81
|
+
}),
|
|
82
|
+
cookieName: isolated ? cookieName : sharedCookieName(payload.config.cookiePrefix),
|
|
83
|
+
exp,
|
|
84
|
+
mode: isolated ? "parallel" : "swap",
|
|
85
|
+
sid,
|
|
86
|
+
token,
|
|
87
|
+
user: raw
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
const resignSession = async ({ collection, payload, req, sid, userId }) => {
|
|
91
|
+
const registered = payload.collections[collection];
|
|
92
|
+
if (!registered) return null;
|
|
93
|
+
const collectionConfig = registered.config;
|
|
94
|
+
const raw = await payload.db.findOne({
|
|
95
|
+
collection,
|
|
96
|
+
req,
|
|
97
|
+
where: { id: { equals: userId } }
|
|
98
|
+
});
|
|
99
|
+
if (!raw) return null;
|
|
100
|
+
const session = (raw.sessions ?? []).find((entry) => entry.id === sid);
|
|
101
|
+
if (!session) return null;
|
|
102
|
+
if (new Date(session.expiresAt).getTime() <= Date.now()) return null;
|
|
103
|
+
const { exp, token } = await jwtSign({
|
|
104
|
+
fieldsToSign: getFieldsToSign({
|
|
105
|
+
collectionConfig,
|
|
106
|
+
email: raw.email ?? "",
|
|
107
|
+
sid,
|
|
108
|
+
user: raw
|
|
109
|
+
}),
|
|
110
|
+
secret: payload.secret,
|
|
111
|
+
tokenExpiration: tokenExpirationOf(collectionConfig.auth)
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
cookie: generatePayloadCookie({
|
|
115
|
+
collectionAuthConfig: collectionConfig.auth,
|
|
116
|
+
cookiePrefix: payload.config.cookiePrefix,
|
|
117
|
+
token
|
|
118
|
+
}),
|
|
119
|
+
exp,
|
|
120
|
+
token
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
//#endregion
|
|
124
|
+
export { issueSession, resignSession };
|
|
125
|
+
|
|
126
|
+
//# sourceMappingURL=issue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"issue.js","names":[],"sources":["../../src/auth/issue.ts"],"sourcesContent":["import type { CollectionSlug, Payload, PayloadRequest, TypedUser } from 'payload'\nimport { getFieldsToSign, jwtSign } from 'payload'\nimport { generatePayloadCookie } from 'payload/shared'\n\nimport { IMPERSONATION_SID_PREFIX } from '../plugin/constants'\nimport type { IssuedSession, SessionIssueArgs } from '../types'\nimport { sharedCookieName } from './cookies'\nimport { isolatedAuthCookie, resolveMode } from './mode'\n\ntype RawUser = TypedUser & {\n\temail?: string\n\tsessions?: { createdAt?: null | string; expiresAt: string; id: string }[] | null\n}\n\nconst tokenExpirationOf = (auth: boolean | { tokenExpiration?: number; useSessions?: boolean }) => {\n\tif (auth === true || auth === false) {\n\t\treturn 7200\n\t}\n\treturn auth.tokenExpiration ?? 7200\n}\n\n/**\n * Mint a target session with a prefixed sid so decorateAuth can skip the DB\n * on requests that cannot be impersonation.\n */\nconst mintPrefixedSession = async ({\n\tcollection,\n\tcollectionConfig,\n\tpayload,\n\traw,\n\treq,\n}: {\n\tcollection: CollectionSlug\n\tcollectionConfig: { auth: boolean | { tokenExpiration?: number; useSessions?: boolean } }\n\tpayload: Payload\n\traw: RawUser\n\treq: PayloadRequest\n}): Promise<string> => {\n\tif (typeof collectionConfig.auth === 'object' && collectionConfig.auth.useSessions === false) {\n\t\tthrow new Error(\n\t\t\t`@10x-media/impersonation: \"${collection}\" did not mint a session id (useSessions is off)`\n\t\t)\n\t}\n\n\tconst now = new Date()\n\tconst sid = `${IMPERSONATION_SID_PREFIX}${crypto.randomUUID()}`\n\tconst sessions = [\n\t\t...(raw.sessions ?? []).filter((session) => new Date(session.expiresAt) > now),\n\t\t{\n\t\t\tcreatedAt: now.toISOString(),\n\t\t\texpiresAt: new Date(\n\t\t\t\tnow.getTime() + tokenExpirationOf(collectionConfig.auth) * 1000\n\t\t\t).toISOString(),\n\t\t\tid: sid,\n\t\t},\n\t]\n\tawait payload.db.updateOne({\n\t\tcollection,\n\t\tdata: { sessions },\n\t\tid: raw.id,\n\t\treq,\n\t\treturning: false,\n\t})\n\traw.sessions = sessions as RawUser['sessions']\n\treturn sid\n}\n\nexport const issueSession = async ({\n\tcollection,\n\tpayload,\n\treq,\n\tuserId,\n}: SessionIssueArgs): Promise<IssuedSession> => {\n\tconst registered = payload.collections[collection]\n\tif (!registered) {\n\t\tthrow new Error(`@10x-media/impersonation: collection \"${collection}\" is not registered`)\n\t}\n\n\tconst collectionConfig = registered.config\n\tconst raw = (await payload.db.findOne({\n\t\tcollection: collection as CollectionSlug,\n\t\treq,\n\t\twhere: { id: { equals: userId } },\n\t})) as null | RawUser\n\n\tif (!raw) {\n\t\tthrow new Error(`@10x-media/impersonation: target ${collection}/${userId} was not found`)\n\t}\n\n\traw.collection = collectionConfig.slug as TypedUser['collection']\n\tconst sid = await mintPrefixedSession({\n\t\tcollection: collection as CollectionSlug,\n\t\tcollectionConfig,\n\t\tpayload,\n\t\traw,\n\t\treq,\n\t})\n\n\tconst fieldsToSign = getFieldsToSign({\n\t\tcollectionConfig,\n\t\temail: raw.email ?? '',\n\t\tsid,\n\t\tuser: raw,\n\t})\n\tconst { exp, token } = await jwtSign({\n\t\tfieldsToSign,\n\t\tsecret: payload.secret,\n\t\ttokenExpiration: tokenExpirationOf(collectionConfig.auth),\n\t})\n\n\tconst { cookieName, isolated } = await resolveMode({ collection, payload, user: raw })\n\tconst cookie = isolated\n\t\t? ((await isolatedAuthCookie({ collection, payload, token, user: raw })) ??\n\t\t\tgeneratePayloadCookie({\n\t\t\t\tcollectionAuthConfig: collectionConfig.auth,\n\t\t\t\tcookiePrefix: payload.config.cookiePrefix,\n\t\t\t\ttoken,\n\t\t\t}))\n\t\t: generatePayloadCookie({\n\t\t\t\tcollectionAuthConfig: collectionConfig.auth,\n\t\t\t\tcookiePrefix: payload.config.cookiePrefix,\n\t\t\t\ttoken,\n\t\t\t})\n\n\treturn {\n\t\tcookie,\n\t\tcookieName: isolated ? cookieName : sharedCookieName(payload.config.cookiePrefix),\n\t\texp,\n\t\tmode: isolated ? 'parallel' : 'swap',\n\t\tsid,\n\t\ttoken,\n\t\tuser: raw,\n\t}\n}\n\nexport const resignSession = async ({\n\tcollection,\n\tpayload,\n\treq,\n\tsid,\n\tuserId,\n}: {\n\tcollection: CollectionSlug\n\tpayload: Payload\n\treq: PayloadRequest\n\tsid: string\n\tuserId: number | string\n}): Promise<{ cookie: string; exp: number; token: string } | null> => {\n\tconst registered = payload.collections[collection]\n\tif (!registered) {\n\t\treturn null\n\t}\n\n\tconst collectionConfig = registered.config\n\tconst raw = (await payload.db.findOne({\n\t\tcollection: collection as CollectionSlug,\n\t\treq,\n\t\twhere: { id: { equals: userId } },\n\t})) as null | RawUser\n\n\tif (!raw) {\n\t\treturn null\n\t}\n\n\tconst session = (raw.sessions ?? []).find((entry) => entry.id === sid)\n\tif (!session) {\n\t\treturn null\n\t}\n\n\tconst expiresAt = new Date(session.expiresAt as Date | string)\n\tif (expiresAt.getTime() <= Date.now()) {\n\t\treturn null\n\t}\n\n\tconst fieldsToSign = getFieldsToSign({\n\t\tcollectionConfig,\n\t\temail: raw.email ?? '',\n\t\tsid,\n\t\tuser: raw,\n\t})\n\tconst { exp, token } = await jwtSign({\n\t\tfieldsToSign,\n\t\tsecret: payload.secret,\n\t\ttokenExpiration: tokenExpirationOf(collectionConfig.auth),\n\t})\n\n\treturn {\n\t\tcookie: generatePayloadCookie({\n\t\t\tcollectionAuthConfig: collectionConfig.auth,\n\t\t\tcookiePrefix: payload.config.cookiePrefix,\n\t\t\ttoken,\n\t\t}),\n\t\texp,\n\t\ttoken,\n\t}\n}\n"],"mappings":";;;;;;AAcA,MAAM,qBAAqB,SAAwE;CAClG,IAAI,SAAS,QAAQ,SAAS,OAC7B,OAAO;CAER,OAAO,KAAK,mBAAmB;AAChC;;;;;AAMA,MAAM,sBAAsB,OAAO,EAClC,YACA,kBACA,SACA,KACA,UAOsB;CACtB,IAAI,OAAO,iBAAiB,SAAS,YAAY,iBAAiB,KAAK,gBAAgB,OACtF,MAAM,IAAI,MACT,8BAA8B,WAAW,iDAC1C;CAGD,MAAM,sBAAM,IAAI,KAAK;CACrB,MAAM,MAAM,GAAG,2BAA2B,OAAO,WAAW;CAC5D,MAAM,WAAW,CAChB,IAAI,IAAI,YAAY,CAAC,GAAG,QAAQ,YAAY,IAAI,KAAK,QAAQ,SAAS,IAAI,GAAG,GAC7E;EACC,WAAW,IAAI,YAAY;EAC3B,WAAW,IAAI,KACd,IAAI,QAAQ,IAAI,kBAAkB,iBAAiB,IAAI,IAAI,GAC5D,EAAE,YAAY;EACd,IAAI;CACL,CACD;CACA,MAAM,QAAQ,GAAG,UAAU;EAC1B;EACA,MAAM,EAAE,SAAS;EACjB,IAAI,IAAI;EACR;EACA,WAAW;CACZ,CAAC;CACD,IAAI,WAAW;CACf,OAAO;AACR;AAEA,MAAa,eAAe,OAAO,EAClC,YACA,SACA,KACA,aAC+C;CAC/C,MAAM,aAAa,QAAQ,YAAY;CACvC,IAAI,CAAC,YACJ,MAAM,IAAI,MAAM,yCAAyC,WAAW,oBAAoB;CAGzF,MAAM,mBAAmB,WAAW;CACpC,MAAM,MAAO,MAAM,QAAQ,GAAG,QAAQ;EACzB;EACZ;EACA,OAAO,EAAE,IAAI,EAAE,QAAQ,OAAO,EAAE;CACjC,CAAC;CAED,IAAI,CAAC,KACJ,MAAM,IAAI,MAAM,oCAAoC,WAAW,GAAG,OAAO,eAAe;CAGzF,IAAI,aAAa,iBAAiB;CAClC,MAAM,MAAM,MAAM,oBAAoB;EACzB;EACZ;EACA;EACA;EACA;CACD,CAAC;CAQD,MAAM,EAAE,KAAK,UAAU,MAAM,QAAQ;EACpC,cAPoB,gBAAgB;GACpC;GACA,OAAO,IAAI,SAAS;GACpB;GACA,MAAM;EACP,CAEY;EACX,QAAQ,QAAQ;EAChB,iBAAiB,kBAAkB,iBAAiB,IAAI;CACzD,CAAC;CAED,MAAM,EAAE,YAAY,aAAa,MAAM,YAAY;EAAE;EAAY;EAAS,MAAM;CAAI,CAAC;CAcrF,OAAO;EACN,QAdc,WACV,MAAM,mBAAmB;GAAE;GAAY;GAAS;GAAO,MAAM;EAAI,CAAC,KACrE,sBAAsB;GACrB,sBAAsB,iBAAiB;GACvC,cAAc,QAAQ,OAAO;GAC7B;EACD,CAAC,IACA,sBAAsB;GACtB,sBAAsB,iBAAiB;GACvC,cAAc,QAAQ,OAAO;GAC7B;EACD,CAAC;EAIF,YAAY,WAAW,aAAa,iBAAiB,QAAQ,OAAO,YAAY;EAChF;EACA,MAAM,WAAW,aAAa;EAC9B;EACA;EACA,MAAM;CACP;AACD;AAEA,MAAa,gBAAgB,OAAO,EACnC,YACA,SACA,KACA,KACA,aAOqE;CACrE,MAAM,aAAa,QAAQ,YAAY;CACvC,IAAI,CAAC,YACJ,OAAO;CAGR,MAAM,mBAAmB,WAAW;CACpC,MAAM,MAAO,MAAM,QAAQ,GAAG,QAAQ;EACzB;EACZ;EACA,OAAO,EAAE,IAAI,EAAE,QAAQ,OAAO,EAAE;CACjC,CAAC;CAED,IAAI,CAAC,KACJ,OAAO;CAGR,MAAM,WAAW,IAAI,YAAY,CAAC,GAAG,MAAM,UAAU,MAAM,OAAO,GAAG;CACrE,IAAI,CAAC,SACJ,OAAO;CAIR,IAAI,IADkB,KAAK,QAAQ,SACvB,EAAE,QAAQ,KAAK,KAAK,IAAI,GACnC,OAAO;CASR,MAAM,EAAE,KAAK,UAAU,MAAM,QAAQ;EACpC,cAPoB,gBAAgB;GACpC;GACA,OAAO,IAAI,SAAS;GACpB;GACA,MAAM;EACP,CAEY;EACX,QAAQ,QAAQ;EAChB,iBAAiB,kBAAkB,iBAAiB,IAAI;CACzD,CAAC;CAED,OAAO;EACN,QAAQ,sBAAsB;GAC7B,sBAAsB,iBAAiB;GACvC,cAAc,QAAQ,OAAO;GAC7B;EACD,CAAC;EACD;EACA;CACD;AACD"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { sharedCookieName } from "./cookies.js";
|
|
2
|
+
//#region src/auth/mode.ts
|
|
3
|
+
const loadDualSession = async () => {
|
|
4
|
+
try {
|
|
5
|
+
const mod = await import("@10x-media/dual-session");
|
|
6
|
+
return {
|
|
7
|
+
generateIsolatedAuthCookie: mod.generateIsolatedAuthCookie,
|
|
8
|
+
resolveIsolatedCookieName: mod.resolveIsolatedCookieName
|
|
9
|
+
};
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Parallel only when dual-session isolated this user onto a cookie other than
|
|
16
|
+
* the shared `${cookiePrefix}-token`. A role-split collection returns the shared
|
|
17
|
+
* name for some users, which is swap.
|
|
18
|
+
*/
|
|
19
|
+
const resolveMode = async ({ collection, payload, user }) => {
|
|
20
|
+
const shared = sharedCookieName(payload.config.cookiePrefix);
|
|
21
|
+
const dual = await loadDualSession();
|
|
22
|
+
if (!dual) return {
|
|
23
|
+
cookieName: shared,
|
|
24
|
+
isolated: false
|
|
25
|
+
};
|
|
26
|
+
try {
|
|
27
|
+
const name = dual.resolveIsolatedCookieName({
|
|
28
|
+
collection,
|
|
29
|
+
payload,
|
|
30
|
+
user
|
|
31
|
+
});
|
|
32
|
+
if (name && name !== shared) return {
|
|
33
|
+
cookieName: name,
|
|
34
|
+
isolated: true
|
|
35
|
+
};
|
|
36
|
+
} catch {
|
|
37
|
+
return {
|
|
38
|
+
cookieName: shared,
|
|
39
|
+
isolated: false
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
cookieName: shared,
|
|
44
|
+
isolated: false
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
/** Isolated cookie name for this user, or undefined when mode is swap. */
|
|
48
|
+
const isolatedCookieNameFor = async (args) => {
|
|
49
|
+
const { cookieName, isolated } = await resolveMode(args);
|
|
50
|
+
return isolated ? cookieName : void 0;
|
|
51
|
+
};
|
|
52
|
+
const isolatedAuthCookie = async ({ collection, payload, token, user }) => {
|
|
53
|
+
const dual = await loadDualSession();
|
|
54
|
+
if (!dual) return;
|
|
55
|
+
try {
|
|
56
|
+
return dual.generateIsolatedAuthCookie({
|
|
57
|
+
collection,
|
|
58
|
+
payload,
|
|
59
|
+
token,
|
|
60
|
+
user
|
|
61
|
+
});
|
|
62
|
+
} catch {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
//#endregion
|
|
67
|
+
export { isolatedAuthCookie, isolatedCookieNameFor, resolveMode };
|
|
68
|
+
|
|
69
|
+
//# sourceMappingURL=mode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mode.js","names":[],"sources":["../../src/auth/mode.ts"],"sourcesContent":["import type { CollectionSlug, Payload, TypedUser } from 'payload'\n\nimport { sharedCookieName } from './cookies'\n\nexport type DualSessionHelpers = {\n\tgenerateIsolatedAuthCookie: (args: {\n\t\tcollection: CollectionSlug\n\t\tpayload: Payload\n\t\ttoken: string\n\t\tuser?: null | TypedUser\n\t}) => string\n\tresolveIsolatedCookieName: (args: {\n\t\tcollection: CollectionSlug\n\t\tpayload: Payload\n\t\tuser?: null | TypedUser\n\t}) => string | undefined\n}\n\nconst loadDualSession = async (): Promise<DualSessionHelpers | null> => {\n\ttry {\n\t\tconst mod = await import('@10x-media/dual-session')\n\t\treturn {\n\t\t\tgenerateIsolatedAuthCookie: mod.generateIsolatedAuthCookie,\n\t\t\tresolveIsolatedCookieName: mod.resolveIsolatedCookieName,\n\t\t}\n\t} catch {\n\t\treturn null\n\t}\n}\n\n/**\n * Parallel only when dual-session isolated this user onto a cookie other than\n * the shared `${cookiePrefix}-token`. A role-split collection returns the shared\n * name for some users, which is swap.\n */\nexport const resolveMode = async ({\n\tcollection,\n\tpayload,\n\tuser,\n}: {\n\tcollection: CollectionSlug\n\tpayload: Payload\n\tuser: TypedUser\n}): Promise<{ cookieName: string; isolated: boolean }> => {\n\tconst shared = sharedCookieName(payload.config.cookiePrefix)\n\tconst dual = await loadDualSession()\n\tif (!dual) {\n\t\treturn { cookieName: shared, isolated: false }\n\t}\n\n\ttry {\n\t\tconst name = dual.resolveIsolatedCookieName({ collection, payload, user })\n\t\tif (name && name !== shared) {\n\t\t\treturn { cookieName: name, isolated: true }\n\t\t}\n\t} catch {\n\t\treturn { cookieName: shared, isolated: false }\n\t}\n\n\treturn { cookieName: shared, isolated: false }\n}\n\n/** Isolated cookie name for this user, or undefined when mode is swap. */\nexport const isolatedCookieNameFor = async (args: {\n\tcollection: CollectionSlug\n\tpayload: Payload\n\tuser: TypedUser\n}): Promise<string | undefined> => {\n\tconst { cookieName, isolated } = await resolveMode(args)\n\treturn isolated ? cookieName : undefined\n}\n\nexport const isolatedAuthCookie = async ({\n\tcollection,\n\tpayload,\n\ttoken,\n\tuser,\n}: {\n\tcollection: CollectionSlug\n\tpayload: Payload\n\ttoken: string\n\tuser: TypedUser\n}): Promise<string | undefined> => {\n\tconst dual = await loadDualSession()\n\tif (!dual) {\n\t\treturn undefined\n\t}\n\ttry {\n\t\treturn dual.generateIsolatedAuthCookie({ collection, payload, token, user })\n\t} catch {\n\t\treturn undefined\n\t}\n}\n"],"mappings":";;AAkBA,MAAM,kBAAkB,YAAgD;CACvE,IAAI;EACH,MAAM,MAAM,MAAM,OAAO;EACzB,OAAO;GACN,4BAA4B,IAAI;GAChC,2BAA2B,IAAI;EAChC;CACD,QAAQ;EACP,OAAO;CACR;AACD;;;;;;AAOA,MAAa,cAAc,OAAO,EACjC,YACA,SACA,WAKyD;CACzD,MAAM,SAAS,iBAAiB,QAAQ,OAAO,YAAY;CAC3D,MAAM,OAAO,MAAM,gBAAgB;CACnC,IAAI,CAAC,MACJ,OAAO;EAAE,YAAY;EAAQ,UAAU;CAAM;CAG9C,IAAI;EACH,MAAM,OAAO,KAAK,0BAA0B;GAAE;GAAY;GAAS;EAAK,CAAC;EACzE,IAAI,QAAQ,SAAS,QACpB,OAAO;GAAE,YAAY;GAAM,UAAU;EAAK;CAE5C,QAAQ;EACP,OAAO;GAAE,YAAY;GAAQ,UAAU;EAAM;CAC9C;CAEA,OAAO;EAAE,YAAY;EAAQ,UAAU;CAAM;AAC9C;;AAGA,MAAa,wBAAwB,OAAO,SAIT;CAClC,MAAM,EAAE,YAAY,aAAa,MAAM,YAAY,IAAI;CACvD,OAAO,WAAW,aAAa,KAAA;AAChC;AAEA,MAAa,qBAAqB,OAAO,EACxC,YACA,SACA,OACA,WAMkC;CAClC,MAAM,OAAO,MAAM,gBAAgB;CACnC,IAAI,CAAC,MACJ;CAED,IAAI;EACH,OAAO,KAAK,2BAA2B;GAAE;GAAY;GAAS;GAAO;EAAK,CAAC;CAC5E,QAAQ;EACP;CACD;AACD"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
//#region src/auth/origin.ts
|
|
2
|
+
/**
|
|
3
|
+
* Cookie POSTs that start, exit, or end a session must be same-origin. This is
|
|
4
|
+
* deliberately stricter than Payload's `extractJWT` cookie CSRF gate and than
|
|
5
|
+
* dual-session's `isCookieAuthAllowed`: `same-site` is denied so a subdomain XSS
|
|
6
|
+
* cannot start an impersonation. JWT/Bearer skip this gate only when that
|
|
7
|
+
* scheme ranks before `cookie` and the header carries a non-empty token. An
|
|
8
|
+
* empty `JWT ` / `Bearer ` prefix must not skip: Payload then authenticates
|
|
9
|
+
* the cookie.
|
|
10
|
+
*
|
|
11
|
+
* Never reads `X-Forwarded-Host` or `X-Forwarded-Proto`.
|
|
12
|
+
*/
|
|
13
|
+
const verifyMutationOrigin = ({ headers, options, payload }) => {
|
|
14
|
+
const authorization = headers.get("Authorization") ?? "";
|
|
15
|
+
const jwtOrder = payload.config.auth?.jwtOrder ?? [
|
|
16
|
+
"JWT",
|
|
17
|
+
"Bearer",
|
|
18
|
+
"cookie"
|
|
19
|
+
];
|
|
20
|
+
const cookieRank = jwtOrder.indexOf("cookie");
|
|
21
|
+
const ranksBeforeCookie = (scheme) => {
|
|
22
|
+
const rank = jwtOrder.indexOf(scheme);
|
|
23
|
+
return rank !== -1 && (cookieRank === -1 || rank < cookieRank);
|
|
24
|
+
};
|
|
25
|
+
const headerToken = (scheme) => {
|
|
26
|
+
const prefix = `${scheme} `;
|
|
27
|
+
if (!authorization.startsWith(prefix)) return "";
|
|
28
|
+
return authorization.slice(prefix.length).trim();
|
|
29
|
+
};
|
|
30
|
+
if (headerToken("JWT") && ranksBeforeCookie("JWT")) return true;
|
|
31
|
+
if (headerToken("Bearer") && ranksBeforeCookie("Bearer")) return true;
|
|
32
|
+
const secFetchSite = headers.get("Sec-Fetch-Site");
|
|
33
|
+
if (secFetchSite === "cross-site" || secFetchSite === "same-site") return false;
|
|
34
|
+
if (secFetchSite === "same-origin") return true;
|
|
35
|
+
const origin = headers.get("Origin");
|
|
36
|
+
if (!origin) return false;
|
|
37
|
+
const allowlist = [...new Set([
|
|
38
|
+
payload.config.serverURL,
|
|
39
|
+
...payload.config.csrf ?? [],
|
|
40
|
+
...options.security.trustedOrigins
|
|
41
|
+
].filter((value) => Boolean(value)))];
|
|
42
|
+
if (allowlist.length > 0) return allowlist.includes(origin);
|
|
43
|
+
try {
|
|
44
|
+
return new URL(origin).host === (headers.get("host") ?? "");
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const isJsonContentType = (headers) => {
|
|
50
|
+
return (headers.get("content-type") ?? "").toLowerCase().includes("application/json");
|
|
51
|
+
};
|
|
52
|
+
//#endregion
|
|
53
|
+
export { isJsonContentType, verifyMutationOrigin };
|
|
54
|
+
|
|
55
|
+
//# sourceMappingURL=origin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"origin.js","names":[],"sources":["../../src/auth/origin.ts"],"sourcesContent":["import type { Payload } from 'payload'\n\nimport type { ResolvedOptions } from '../types'\n\n/**\n * Cookie POSTs that start, exit, or end a session must be same-origin. This is\n * deliberately stricter than Payload's `extractJWT` cookie CSRF gate and than\n * dual-session's `isCookieAuthAllowed`: `same-site` is denied so a subdomain XSS\n * cannot start an impersonation. JWT/Bearer skip this gate only when that\n * scheme ranks before `cookie` and the header carries a non-empty token. An\n * empty `JWT ` / `Bearer ` prefix must not skip: Payload then authenticates\n * the cookie.\n *\n * Never reads `X-Forwarded-Host` or `X-Forwarded-Proto`.\n */\nexport const verifyMutationOrigin = ({\n\theaders,\n\toptions,\n\tpayload,\n}: {\n\theaders: Headers\n\toptions: Pick<ResolvedOptions, 'security'>\n\tpayload: Payload\n}): boolean => {\n\tconst authorization = headers.get('Authorization') ?? ''\n\tconst jwtOrder = payload.config.auth?.jwtOrder ?? ['JWT', 'Bearer', 'cookie']\n\tconst cookieRank = jwtOrder.indexOf('cookie')\n\tconst ranksBeforeCookie = (scheme: 'Bearer' | 'JWT') => {\n\t\tconst rank = jwtOrder.indexOf(scheme)\n\t\treturn rank !== -1 && (cookieRank === -1 || rank < cookieRank)\n\t}\n\tconst headerToken = (scheme: 'Bearer' | 'JWT') => {\n\t\tconst prefix = `${scheme} `\n\t\tif (!authorization.startsWith(prefix)) {\n\t\t\treturn ''\n\t\t}\n\t\treturn authorization.slice(prefix.length).trim()\n\t}\n\n\tif (headerToken('JWT') && ranksBeforeCookie('JWT')) {\n\t\treturn true\n\t}\n\tif (headerToken('Bearer') && ranksBeforeCookie('Bearer')) {\n\t\treturn true\n\t}\n\n\tconst secFetchSite = headers.get('Sec-Fetch-Site')\n\tif (secFetchSite === 'cross-site' || secFetchSite === 'same-site') {\n\t\treturn false\n\t}\n\tif (secFetchSite === 'same-origin') {\n\t\treturn true\n\t}\n\n\tconst origin = headers.get('Origin')\n\tif (!origin) {\n\t\treturn false\n\t}\n\n\tconst allowlist = [\n\t\t...new Set(\n\t\t\t[\n\t\t\t\tpayload.config.serverURL,\n\t\t\t\t...(payload.config.csrf ?? []),\n\t\t\t\t...options.security.trustedOrigins,\n\t\t\t].filter((value): value is string => Boolean(value))\n\t\t),\n\t]\n\n\tif (allowlist.length > 0) {\n\t\treturn allowlist.includes(origin)\n\t}\n\n\ttry {\n\t\treturn new URL(origin).host === (headers.get('host') ?? '')\n\t} catch {\n\t\treturn false\n\t}\n}\n\nexport const isJsonContentType = (headers: Headers): boolean => {\n\tconst value = headers.get('content-type') ?? ''\n\treturn value.toLowerCase().includes('application/json')\n}\n"],"mappings":";;;;;;;;;;;;AAeA,MAAa,wBAAwB,EACpC,SACA,SACA,cAKc;CACd,MAAM,gBAAgB,QAAQ,IAAI,eAAe,KAAK;CACtD,MAAM,WAAW,QAAQ,OAAO,MAAM,YAAY;EAAC;EAAO;EAAU;CAAQ;CAC5E,MAAM,aAAa,SAAS,QAAQ,QAAQ;CAC5C,MAAM,qBAAqB,WAA6B;EACvD,MAAM,OAAO,SAAS,QAAQ,MAAM;EACpC,OAAO,SAAS,OAAO,eAAe,MAAM,OAAO;CACpD;CACA,MAAM,eAAe,WAA6B;EACjD,MAAM,SAAS,GAAG,OAAO;EACzB,IAAI,CAAC,cAAc,WAAW,MAAM,GACnC,OAAO;EAER,OAAO,cAAc,MAAM,OAAO,MAAM,EAAE,KAAK;CAChD;CAEA,IAAI,YAAY,KAAK,KAAK,kBAAkB,KAAK,GAChD,OAAO;CAER,IAAI,YAAY,QAAQ,KAAK,kBAAkB,QAAQ,GACtD,OAAO;CAGR,MAAM,eAAe,QAAQ,IAAI,gBAAgB;CACjD,IAAI,iBAAiB,gBAAgB,iBAAiB,aACrD,OAAO;CAER,IAAI,iBAAiB,eACpB,OAAO;CAGR,MAAM,SAAS,QAAQ,IAAI,QAAQ;CACnC,IAAI,CAAC,QACJ,OAAO;CAGR,MAAM,YAAY,CACjB,GAAG,IAAI,IACN;EACC,QAAQ,OAAO;EACf,GAAI,QAAQ,OAAO,QAAQ,CAAC;EAC5B,GAAG,QAAQ,SAAS;CACrB,EAAE,QAAQ,UAA2B,QAAQ,KAAK,CAAC,CACpD,CACD;CAEA,IAAI,UAAU,SAAS,GACtB,OAAO,UAAU,SAAS,MAAM;CAGjC,IAAI;EACH,OAAO,IAAI,IAAI,MAAM,EAAE,UAAU,QAAQ,IAAI,MAAM,KAAK;CACzD,QAAQ;EACP,OAAO;CACR;AACD;AAEA,MAAa,qBAAqB,YAA8B;CAE/D,QADc,QAAQ,IAAI,cAAc,KAAK,IAChC,YAAY,EAAE,SAAS,kBAAkB;AACvD"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/auth/revoke.ts
|
|
2
|
+
const revokeSession = async ({ collection, payload, req, sid, userId }) => {
|
|
3
|
+
const raw = await payload.db.findOne({
|
|
4
|
+
collection,
|
|
5
|
+
req,
|
|
6
|
+
where: { id: { equals: userId } }
|
|
7
|
+
});
|
|
8
|
+
if (!raw?.sessions?.length) return;
|
|
9
|
+
const sessions = raw.sessions.filter((session) => session.id !== sid);
|
|
10
|
+
await payload.db.updateOne({
|
|
11
|
+
id: raw.id,
|
|
12
|
+
collection,
|
|
13
|
+
data: {
|
|
14
|
+
sessions,
|
|
15
|
+
updatedAt: null
|
|
16
|
+
},
|
|
17
|
+
req,
|
|
18
|
+
returning: false
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
//#endregion
|
|
22
|
+
export { revokeSession };
|
|
23
|
+
|
|
24
|
+
//# sourceMappingURL=revoke.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revoke.js","names":[],"sources":["../../src/auth/revoke.ts"],"sourcesContent":["import type { CollectionSlug, PayloadRequest } from 'payload'\n\nimport type { SessionRevokeArgs } from '../types'\n\ntype SessionEntry = { id: string }\n\nexport const revokeSession = async ({\n\tcollection,\n\tpayload,\n\treq,\n\tsid,\n\tuserId,\n}: SessionRevokeArgs): Promise<void> => {\n\tconst raw = (await payload.db.findOne({\n\t\tcollection: collection as CollectionSlug,\n\t\treq,\n\t\twhere: { id: { equals: userId } },\n\t})) as { id: number | string; sessions?: SessionEntry[] } | null\n\n\tif (!raw?.sessions?.length) {\n\t\treturn\n\t}\n\n\tconst sessions = raw.sessions.filter((session) => session.id !== sid)\n\tawait payload.db.updateOne({\n\t\tid: raw.id,\n\t\tcollection: collection as CollectionSlug,\n\t\tdata: { sessions, updatedAt: null },\n\t\treq: req as PayloadRequest,\n\t\treturning: false,\n\t})\n}\n"],"mappings":";AAMA,MAAa,gBAAgB,OAAO,EACnC,YACA,SACA,KACA,KACA,aACuC;CACvC,MAAM,MAAO,MAAM,QAAQ,GAAG,QAAQ;EACzB;EACZ;EACA,OAAO,EAAE,IAAI,EAAE,QAAQ,OAAO,EAAE;CACjC,CAAC;CAED,IAAI,CAAC,KAAK,UAAU,QACnB;CAGD,MAAM,WAAW,IAAI,SAAS,QAAQ,YAAY,QAAQ,OAAO,GAAG;CACpE,MAAM,QAAQ,GAAG,UAAU;EAC1B,IAAI,IAAI;EACI;EACZ,MAAM;GAAE;GAAU,WAAW;EAAK;EAC7B;EACL,WAAW;CACZ,CAAC;AACF"}
|