@hanzo/cms-auth-iam 1.0.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/LICENSE.md +22 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/strategy.d.ts +16 -0
- package/dist/strategy.d.ts.map +1 -0
- package/dist/strategy.js +164 -0
- package/dist/strategy.js.map +1 -0
- package/dist/types.d.ts +53 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/package.json +36 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018-2025 Payload CMS, Inc. <info@payloadcms.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
'Software'), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
20
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
21
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
22
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Field } from '@hanzo/cms';
|
|
2
|
+
export { hanzoIAMStrategy } from './strategy.js';
|
|
3
|
+
export type { HanzoIAMStrategyConfig, IAMClaims } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Fields the IAM strategy needs on the auth collection to map + dedupe users.
|
|
6
|
+
* Spread these into your users collection `fields`.
|
|
7
|
+
*
|
|
8
|
+
* fields: [...iamAuthFields, ...yourFields]
|
|
9
|
+
*/
|
|
10
|
+
export declare const iamAuthFields: Field[];
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAEvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAChD,YAAY,EAAE,sBAAsB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAEnE;;;;;GAKG;AACH,eAAO,MAAM,aAAa,EAAE,KAAK,EA6BhC,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export { hanzoIAMStrategy } from './strategy.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fields the IAM strategy needs on the auth collection to map + dedupe users.
|
|
4
|
+
* Spread these into your users collection `fields`.
|
|
5
|
+
*
|
|
6
|
+
* fields: [...iamAuthFields, ...yourFields]
|
|
7
|
+
*/ export const iamAuthFields = [
|
|
8
|
+
{
|
|
9
|
+
// Auth collections with `disableLocalStrategy: true` do NOT get the email
|
|
10
|
+
// field auto-added (that comes from the local strategy), so add it here.
|
|
11
|
+
name: 'email',
|
|
12
|
+
type: 'email',
|
|
13
|
+
index: true,
|
|
14
|
+
label: 'Email'
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'iamSub',
|
|
18
|
+
type: 'text',
|
|
19
|
+
admin: {
|
|
20
|
+
description: 'Hanzo IAM subject (user id).',
|
|
21
|
+
readOnly: true
|
|
22
|
+
},
|
|
23
|
+
index: true,
|
|
24
|
+
label: 'IAM Subject',
|
|
25
|
+
unique: true
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'iamOrg',
|
|
29
|
+
type: 'text',
|
|
30
|
+
admin: {
|
|
31
|
+
description: 'Hanzo IAM org slug (== tenant).',
|
|
32
|
+
readOnly: true
|
|
33
|
+
},
|
|
34
|
+
index: true,
|
|
35
|
+
label: 'IAM Org'
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'username',
|
|
39
|
+
type: 'text',
|
|
40
|
+
label: 'Username'
|
|
41
|
+
}
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Field } from '@hanzo/cms'\n\nexport { hanzoIAMStrategy } from './strategy.js'\nexport type { HanzoIAMStrategyConfig, IAMClaims } from './types.js'\n\n/**\n * Fields the IAM strategy needs on the auth collection to map + dedupe users.\n * Spread these into your users collection `fields`.\n *\n * fields: [...iamAuthFields, ...yourFields]\n */\nexport const iamAuthFields: Field[] = [\n {\n // Auth collections with `disableLocalStrategy: true` do NOT get the email\n // field auto-added (that comes from the local strategy), so add it here.\n name: 'email',\n type: 'email',\n index: true,\n label: 'Email',\n },\n {\n name: 'iamSub',\n type: 'text',\n admin: { description: 'Hanzo IAM subject (user id).', readOnly: true },\n index: true,\n label: 'IAM Subject',\n unique: true,\n },\n {\n name: 'iamOrg',\n type: 'text',\n admin: { description: 'Hanzo IAM org slug (== tenant).', readOnly: true },\n index: true,\n label: 'IAM Org',\n },\n {\n name: 'username',\n type: 'text',\n label: 'Username',\n },\n]\n"],"names":["hanzoIAMStrategy","iamAuthFields","name","type","index","label","admin","description","readOnly","unique"],"mappings":"AAEA,SAASA,gBAAgB,QAAQ,gBAAe;AAGhD;;;;;CAKC,GACD,OAAO,MAAMC,gBAAyB;IACpC;QACE,0EAA0E;QAC1E,yEAAyE;QACzEC,MAAM;QACNC,MAAM;QACNC,OAAO;QACPC,OAAO;IACT;IACA;QACEH,MAAM;QACNC,MAAM;QACNG,OAAO;YAAEC,aAAa;YAAgCC,UAAU;QAAK;QACrEJ,OAAO;QACPC,OAAO;QACPI,QAAQ;IACV;IACA;QACEP,MAAM;QACNC,MAAM;QACNG,OAAO;YAAEC,aAAa;YAAmCC,UAAU;QAAK;QACxEJ,OAAO;QACPC,OAAO;IACT;IACA;QACEH,MAAM;QACNC,MAAM;QACNE,OAAO;IACT;CACD,CAAA"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AuthStrategy } from '@hanzo/cms';
|
|
2
|
+
import type { HanzoIAMStrategyConfig } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The Hanzo IAM SSO auth strategy.
|
|
5
|
+
*
|
|
6
|
+
* Validates a Bearer access token issued by Hanzo IAM (Casdoor, RS256) against
|
|
7
|
+
* the published JWKS — no shared secret, no per-request round-trip to IAM. Maps
|
|
8
|
+
* the verified claims to a CMS user (find-or-provision by IAM `sub`), links
|
|
9
|
+
* the user to the tenant derived from the IAM `owner` org (org == tenant), and
|
|
10
|
+
* sets the `cms-tenant` cookie so the multi-tenant plugin scopes every
|
|
11
|
+
* subsequent query to that org.
|
|
12
|
+
*
|
|
13
|
+
* IAM is the sole identity authority: there is no separate CMS login.
|
|
14
|
+
*/
|
|
15
|
+
export declare const hanzoIAMStrategy: (config?: HanzoIAMStrategyConfig) => AuthStrategy;
|
|
16
|
+
//# sourceMappingURL=strategy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strategy.d.ts","sourceRoot":"","sources":["../src/strategy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAqD,MAAM,YAAY,CAAA;AAIjG,OAAO,KAAK,EAAE,sBAAsB,EAAa,MAAM,YAAY,CAAA;AAoEnE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB,YAAY,sBAAsB,KAAQ,YAyFtE,CAAA"}
|
package/dist/strategy.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
2
|
+
const DEFAULT_ISSUER = 'https://hanzo.id';
|
|
3
|
+
const DEFAULT_JWKS = 'https://hanzo.id/v1/iam/.well-known/jwks';
|
|
4
|
+
/** Lazily-built, per-JWKS-URL cached remote key set (jose caches internally). */ const jwksCache = new Map();
|
|
5
|
+
const getJWKS = (uri)=>{
|
|
6
|
+
let set = jwksCache.get(uri);
|
|
7
|
+
if (!set) {
|
|
8
|
+
set = createRemoteJWKSet(new URL(uri));
|
|
9
|
+
jwksCache.set(uri, set);
|
|
10
|
+
}
|
|
11
|
+
return set;
|
|
12
|
+
};
|
|
13
|
+
const getBearer = (headers)=>{
|
|
14
|
+
const raw = headers.get('authorization') || headers.get('Authorization');
|
|
15
|
+
if (!raw) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const [scheme, token] = raw.split(' ');
|
|
19
|
+
if (!scheme || !token || scheme.toLowerCase() !== 'bearer') {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return token.trim();
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Provision-or-refresh the tenant doc for an IAM org (org == tenant), returning
|
|
26
|
+
* its id. Idempotent: keyed on the org slug.
|
|
27
|
+
*/ const ensureTenant = async (args)=>{
|
|
28
|
+
const { claims, cms, tenantsSlug } = args;
|
|
29
|
+
const slug = claims.owner;
|
|
30
|
+
if (!slug) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
// Only touch the collection if it actually exists in this config.
|
|
34
|
+
if (!cms.collections?.[tenantsSlug]) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
const existing = await cms.find({
|
|
38
|
+
collection: tenantsSlug,
|
|
39
|
+
depth: 0,
|
|
40
|
+
limit: 1,
|
|
41
|
+
where: {
|
|
42
|
+
slug: {
|
|
43
|
+
equals: slug
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
const existingDoc = existing.docs[0];
|
|
48
|
+
if (existingDoc) {
|
|
49
|
+
return existingDoc.id;
|
|
50
|
+
}
|
|
51
|
+
const created = await cms.create({
|
|
52
|
+
collection: tenantsSlug,
|
|
53
|
+
data: {
|
|
54
|
+
name: slug,
|
|
55
|
+
slug
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return created.id;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* The Hanzo IAM SSO auth strategy.
|
|
62
|
+
*
|
|
63
|
+
* Validates a Bearer access token issued by Hanzo IAM (Casdoor, RS256) against
|
|
64
|
+
* the published JWKS — no shared secret, no per-request round-trip to IAM. Maps
|
|
65
|
+
* the verified claims to a CMS user (find-or-provision by IAM `sub`), links
|
|
66
|
+
* the user to the tenant derived from the IAM `owner` org (org == tenant), and
|
|
67
|
+
* sets the `cms-tenant` cookie so the multi-tenant plugin scopes every
|
|
68
|
+
* subsequent query to that org.
|
|
69
|
+
*
|
|
70
|
+
* IAM is the sole identity authority: there is no separate CMS login.
|
|
71
|
+
*/ export const hanzoIAMStrategy = (config = {})=>{
|
|
72
|
+
const name = config.name || 'hanzo-iam';
|
|
73
|
+
const authSlug = config.authSlug || 'users';
|
|
74
|
+
const tenantsSlug = config.tenantsSlug || 'tenants';
|
|
75
|
+
const tenantsArrayField = config.tenantsArrayField || 'tenants';
|
|
76
|
+
const issuer = config.issuer || process.env.HANZO_IAM_ISSUER || DEFAULT_ISSUER;
|
|
77
|
+
const jwksUri = config.jwksUri || process.env.HANZO_IAM_JWKS_URI || DEFAULT_JWKS;
|
|
78
|
+
return {
|
|
79
|
+
name,
|
|
80
|
+
authenticate: async ({ canSetHeaders, headers, cms })=>{
|
|
81
|
+
const token = getBearer(headers);
|
|
82
|
+
if (!token) {
|
|
83
|
+
return {
|
|
84
|
+
user: null
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
let claims;
|
|
88
|
+
try {
|
|
89
|
+
const { payload: verified } = await jwtVerify(token, getJWKS(jwksUri), {
|
|
90
|
+
issuer
|
|
91
|
+
});
|
|
92
|
+
claims = verified;
|
|
93
|
+
} catch {
|
|
94
|
+
// invalid / expired / wrong-issuer token → anonymous
|
|
95
|
+
return {
|
|
96
|
+
user: null
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (!claims.sub) {
|
|
100
|
+
return {
|
|
101
|
+
user: null
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const tenantID = await ensureTenant({
|
|
105
|
+
claims,
|
|
106
|
+
cms,
|
|
107
|
+
tenantsSlug
|
|
108
|
+
});
|
|
109
|
+
// find-or-provision the user by IAM subject
|
|
110
|
+
const found = await cms.find({
|
|
111
|
+
collection: authSlug,
|
|
112
|
+
depth: 0,
|
|
113
|
+
limit: 1,
|
|
114
|
+
where: {
|
|
115
|
+
iamSub: {
|
|
116
|
+
equals: claims.sub
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
const baseData = {
|
|
121
|
+
email: claims.email || `${claims.sub}@iam.local`,
|
|
122
|
+
iamOrg: claims.owner,
|
|
123
|
+
iamSub: claims.sub,
|
|
124
|
+
username: claims.name,
|
|
125
|
+
...config.claimsToUser ? config.claimsToUser(claims) : {},
|
|
126
|
+
...tenantID !== undefined ? {
|
|
127
|
+
[tenantsArrayField]: [
|
|
128
|
+
{
|
|
129
|
+
tenant: tenantID
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
} : {}
|
|
133
|
+
};
|
|
134
|
+
let userDoc;
|
|
135
|
+
const foundUser = found.docs[0];
|
|
136
|
+
if (foundUser) {
|
|
137
|
+
userDoc = await cms.update({
|
|
138
|
+
id: foundUser.id,
|
|
139
|
+
collection: authSlug,
|
|
140
|
+
data: baseData
|
|
141
|
+
});
|
|
142
|
+
} else {
|
|
143
|
+
userDoc = await cms.create({
|
|
144
|
+
collection: authSlug,
|
|
145
|
+
data: baseData
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const responseHeaders = new Headers();
|
|
149
|
+
if (canSetHeaders && tenantID !== undefined) {
|
|
150
|
+
responseHeaders.append('Set-Cookie', `cms-tenant=${encodeURIComponent(String(tenantID))}; Path=/; SameSite=Lax; HttpOnly`);
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
responseHeaders: canSetHeaders ? responseHeaders : undefined,
|
|
154
|
+
user: {
|
|
155
|
+
...userDoc,
|
|
156
|
+
_strategy: name,
|
|
157
|
+
collection: authSlug
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
//# sourceMappingURL=strategy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/strategy.ts"],"sourcesContent":["import type { AuthStrategy, AuthStrategyFunctionArgs, AuthStrategyResult, CMS } from '@hanzo/cms'\n\nimport { createRemoteJWKSet, jwtVerify } from 'jose'\n\nimport type { HanzoIAMStrategyConfig, IAMClaims } from './types.js'\n\nconst DEFAULT_ISSUER = 'https://hanzo.id'\nconst DEFAULT_JWKS = 'https://hanzo.id/v1/iam/.well-known/jwks'\n\n/** Lazily-built, per-JWKS-URL cached remote key set (jose caches internally). */\nconst jwksCache = new Map<string, ReturnType<typeof createRemoteJWKSet>>()\n\nconst getJWKS = (uri: string) => {\n let set = jwksCache.get(uri)\n if (!set) {\n set = createRemoteJWKSet(new URL(uri))\n jwksCache.set(uri, set)\n }\n return set\n}\n\nconst getBearer = (headers: AuthStrategyFunctionArgs['headers']): null | string => {\n const raw = headers.get('authorization') || headers.get('Authorization')\n if (!raw) {\n return null\n }\n const [scheme, token] = raw.split(' ')\n if (!scheme || !token || scheme.toLowerCase() !== 'bearer') {\n return null\n }\n return token.trim()\n}\n\n/**\n * Provision-or-refresh the tenant doc for an IAM org (org == tenant), returning\n * its id. Idempotent: keyed on the org slug.\n */\nconst ensureTenant = async (args: {\n claims: IAMClaims\n cms: CMS\n tenantsSlug: string\n}): Promise<number | string | undefined> => {\n const { claims, cms, tenantsSlug } = args\n const slug = claims.owner\n if (!slug) {\n return undefined\n }\n\n // Only touch the collection if it actually exists in this config.\n if (!cms.collections?.[tenantsSlug]) {\n return undefined\n }\n\n const existing = await cms.find({\n collection: tenantsSlug,\n depth: 0,\n limit: 1,\n where: { slug: { equals: slug } },\n })\n\n const existingDoc = existing.docs[0]\n if (existingDoc) {\n return existingDoc.id as number | string\n }\n\n const created = await cms.create({\n collection: tenantsSlug,\n data: { name: slug, slug },\n })\n return created.id as number | string\n}\n\n/**\n * The Hanzo IAM SSO auth strategy.\n *\n * Validates a Bearer access token issued by Hanzo IAM (Casdoor, RS256) against\n * the published JWKS — no shared secret, no per-request round-trip to IAM. Maps\n * the verified claims to a CMS user (find-or-provision by IAM `sub`), links\n * the user to the tenant derived from the IAM `owner` org (org == tenant), and\n * sets the `cms-tenant` cookie so the multi-tenant plugin scopes every\n * subsequent query to that org.\n *\n * IAM is the sole identity authority: there is no separate CMS login.\n */\nexport const hanzoIAMStrategy = (config: HanzoIAMStrategyConfig = {}): AuthStrategy => {\n const name = config.name || 'hanzo-iam'\n const authSlug = config.authSlug || 'users'\n const tenantsSlug = config.tenantsSlug || 'tenants'\n const tenantsArrayField = config.tenantsArrayField || 'tenants'\n const issuer = config.issuer || process.env.HANZO_IAM_ISSUER || DEFAULT_ISSUER\n const jwksUri = config.jwksUri || process.env.HANZO_IAM_JWKS_URI || DEFAULT_JWKS\n\n return {\n name,\n authenticate: async ({\n canSetHeaders,\n headers,\n cms,\n }: AuthStrategyFunctionArgs): Promise<AuthStrategyResult> => {\n const token = getBearer(headers)\n if (!token) {\n return { user: null }\n }\n\n let claims: IAMClaims\n try {\n const { payload: verified } = await jwtVerify(token, getJWKS(jwksUri), {\n issuer,\n })\n claims = verified as IAMClaims\n } catch {\n // invalid / expired / wrong-issuer token → anonymous\n return { user: null }\n }\n\n if (!claims.sub) {\n return { user: null }\n }\n\n const tenantID = await ensureTenant({ claims, cms, tenantsSlug })\n\n // find-or-provision the user by IAM subject\n const found = await cms.find({\n collection: authSlug,\n depth: 0,\n limit: 1,\n where: { iamSub: { equals: claims.sub } },\n })\n\n const baseData = {\n email: claims.email || `${claims.sub}@iam.local`,\n iamOrg: claims.owner,\n iamSub: claims.sub,\n username: claims.name,\n ...(config.claimsToUser ? config.claimsToUser(claims) : {}),\n ...(tenantID !== undefined\n ? { [tenantsArrayField]: [{ tenant: tenantID }] }\n : {}),\n }\n\n let userDoc\n const foundUser = found.docs[0]\n if (foundUser) {\n userDoc = await cms.update({\n id: foundUser.id,\n collection: authSlug,\n data: baseData,\n })\n } else {\n userDoc = await cms.create({\n collection: authSlug,\n data: baseData,\n })\n }\n\n const responseHeaders = new Headers()\n if (canSetHeaders && tenantID !== undefined) {\n responseHeaders.append(\n 'Set-Cookie',\n `cms-tenant=${encodeURIComponent(String(tenantID))}; Path=/; SameSite=Lax; HttpOnly`,\n )\n }\n\n return {\n responseHeaders: canSetHeaders ? responseHeaders : undefined,\n user: {\n ...userDoc,\n _strategy: name,\n collection: authSlug,\n },\n }\n },\n }\n}\n"],"names":["createRemoteJWKSet","jwtVerify","DEFAULT_ISSUER","DEFAULT_JWKS","jwksCache","Map","getJWKS","uri","set","get","URL","getBearer","headers","raw","scheme","token","split","toLowerCase","trim","ensureTenant","args","claims","cms","tenantsSlug","slug","owner","undefined","collections","existing","find","collection","depth","limit","where","equals","existingDoc","docs","id","created","create","data","name","hanzoIAMStrategy","config","authSlug","tenantsArrayField","issuer","process","env","HANZO_IAM_ISSUER","jwksUri","HANZO_IAM_JWKS_URI","authenticate","canSetHeaders","user","payload","verified","sub","tenantID","found","iamSub","baseData","email","iamOrg","username","claimsToUser","tenant","userDoc","foundUser","update","responseHeaders","Headers","append","encodeURIComponent","String","_strategy"],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claims we rely on from a Hanzo IAM (Casdoor) access token.
|
|
3
|
+
* `owner` is the org slug — in Hanzo, org == tenant. There is no separate CMS
|
|
4
|
+
* tenant concept; the IAM org IS the tenancy boundary.
|
|
5
|
+
*/
|
|
6
|
+
export type IAMClaims = {
|
|
7
|
+
aud?: string | string[];
|
|
8
|
+
email?: string;
|
|
9
|
+
exp?: number;
|
|
10
|
+
iss?: string;
|
|
11
|
+
/** username */
|
|
12
|
+
name?: string;
|
|
13
|
+
/** org slug — the tenant */
|
|
14
|
+
owner?: string;
|
|
15
|
+
/** user id (uuid) */
|
|
16
|
+
sub?: string;
|
|
17
|
+
};
|
|
18
|
+
export type HanzoIAMStrategyConfig = {
|
|
19
|
+
/**
|
|
20
|
+
* Slug of the CMS auth collection users are mapped into (e.g. 'users').
|
|
21
|
+
*/
|
|
22
|
+
authSlug?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Extra fields to set/refresh on the mapped user each login. Receives the
|
|
25
|
+
* verified claims; returns a partial user document.
|
|
26
|
+
*/
|
|
27
|
+
claimsToUser?: (claims: IAMClaims) => Record<string, unknown>;
|
|
28
|
+
/**
|
|
29
|
+
* Expected token issuer. Defaults to HANZO_IAM_ISSUER or https://hanzo.id.
|
|
30
|
+
*/
|
|
31
|
+
issuer?: string;
|
|
32
|
+
/**
|
|
33
|
+
* JWKS URL. Defaults to HANZO_IAM_JWKS_URI or
|
|
34
|
+
* https://hanzo.id/v1/iam/.well-known/jwks.
|
|
35
|
+
*/
|
|
36
|
+
jwksUri?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Strategy name surfaced to CMS. Defaults to 'hanzo-iam'.
|
|
39
|
+
*/
|
|
40
|
+
name?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Name of the tenant field on the user (link user -> tenant). When the
|
|
43
|
+
* multi-tenant plugin is wired, this is the array field it manages.
|
|
44
|
+
* Defaults to 'tenants'.
|
|
45
|
+
*/
|
|
46
|
+
tenantsArrayField?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Slug of the tenants collection (org == tenant). Defaults to 'tenants'.
|
|
49
|
+
* A tenant doc is provisioned per IAM org on first login.
|
|
50
|
+
*/
|
|
51
|
+
tenantsSlug?: string;
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACvB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,eAAe;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,4BAA4B;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,qBAAqB;IACrB,GAAG,CAAC,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC7D;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["/**\n * Claims we rely on from a Hanzo IAM (Casdoor) access token.\n * `owner` is the org slug — in Hanzo, org == tenant. There is no separate CMS\n * tenant concept; the IAM org IS the tenancy boundary.\n */\nexport type IAMClaims = {\n aud?: string | string[]\n email?: string\n exp?: number\n iss?: string\n /** username */\n name?: string\n /** org slug — the tenant */\n owner?: string\n /** user id (uuid) */\n sub?: string\n}\n\nexport type HanzoIAMStrategyConfig = {\n /**\n * Slug of the CMS auth collection users are mapped into (e.g. 'users').\n */\n authSlug?: string\n /**\n * Extra fields to set/refresh on the mapped user each login. Receives the\n * verified claims; returns a partial user document.\n */\n claimsToUser?: (claims: IAMClaims) => Record<string, unknown>\n /**\n * Expected token issuer. Defaults to HANZO_IAM_ISSUER or https://hanzo.id.\n */\n issuer?: string\n /**\n * JWKS URL. Defaults to HANZO_IAM_JWKS_URI or\n * https://hanzo.id/v1/iam/.well-known/jwks.\n */\n jwksUri?: string\n /**\n * Strategy name surfaced to CMS. Defaults to 'hanzo-iam'.\n */\n name?: string\n /**\n * Name of the tenant field on the user (link user -> tenant). When the\n * multi-tenant plugin is wired, this is the array field it manages.\n * Defaults to 'tenants'.\n */\n tenantsArrayField?: string\n /**\n * Slug of the tenants collection (org == tenant). Defaults to 'tenants'.\n * A tenant doc is provisioned per IAM org on first login.\n */\n tenantsSlug?: string\n}\n"],"names":[],"mappings":"AAAA;;;;CAIC,GAcD,WAkCC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hanzo/cms-auth-iam",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Hanzo IAM SSO auth strategy for Hanzo CMS. Verifies IAM (Casdoor) access tokens via JWKS; org == tenant. No separate CMS login.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"jose": "5.10.0",
|
|
21
|
+
"@hanzo/cms": "3.85.2"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@hanzo/cms-eslint-config": "3.28.0"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
|
|
28
|
+
"build:debug": "pnpm build",
|
|
29
|
+
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
|
|
30
|
+
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
|
31
|
+
"clean": "rimraf -g {dist,*.tsbuildinfo}",
|
|
32
|
+
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
|
33
|
+
"lint": "eslint .",
|
|
34
|
+
"lint:fix": "eslint . --fix"
|
|
35
|
+
}
|
|
36
|
+
}
|