@ballistix.digital/identity-connector 0.0.0-local
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 +6 -0
- package/README.md +631 -0
- package/dist/common/domain/enum/identityConnectorExceptionType.enum.d.ts +14 -0
- package/dist/common/domain/enum/identityConnectorExceptionType.enum.js +18 -0
- package/dist/common/domain/model/identity.d.ts +9 -0
- package/dist/common/domain/model/identity.js +2 -0
- package/dist/common/domain/model/identityProfile.d.ts +6 -0
- package/dist/common/domain/model/identityProfile.js +2 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +23 -0
- package/dist/module/entra/domain/model/entraProviderOptions.d.ts +6 -0
- package/dist/module/entra/domain/model/entraProviderOptions.js +42 -0
- package/dist/module/entra/index.d.ts +2 -0
- package/dist/module/entra/index.js +7 -0
- package/dist/module/entra/service/entra.provider.d.ts +23 -0
- package/dist/module/entra/service/entra.provider.js +72 -0
- package/dist/module/identity/domain/exception/identityConnectorException.d.ts +5 -0
- package/dist/module/identity/domain/exception/identityConnectorException.js +48 -0
- package/dist/module/identity/domain/model/identityConnectorModuleOptions.d.ts +7 -0
- package/dist/module/identity/domain/model/identityConnectorModuleOptions.js +2 -0
- package/dist/module/identity/domain/model/identityProvider.d.ts +22 -0
- package/dist/module/identity/domain/model/identityProvider.js +17 -0
- package/dist/module/identity/domain/model/identityProviderOptions.d.ts +5 -0
- package/dist/module/identity/domain/model/identityProviderOptions.js +34 -0
- package/dist/module/identity/domain/model/identityProvidersToken.d.ts +1 -0
- package/dist/module/identity/domain/model/identityProvidersToken.js +4 -0
- package/dist/module/identity/domain/model/userStore.d.ts +10 -0
- package/dist/module/identity/domain/model/userStore.js +12 -0
- package/dist/module/identity/domain/validation/default.decorator.d.ts +1 -0
- package/dist/module/identity/domain/validation/default.decorator.js +10 -0
- package/dist/module/identity/domain/validation/trimmed.decorator.d.ts +2 -0
- package/dist/module/identity/domain/validation/trimmed.decorator.js +17 -0
- package/dist/module/identity/domain/validation/validateOptions.d.ts +2 -0
- package/dist/module/identity/domain/validation/validateOptions.js +44 -0
- package/dist/module/identity/identityConnector.module.d.ts +6 -0
- package/dist/module/identity/identityConnector.module.js +47 -0
- package/dist/module/identity/service/identityAuth.guard.d.ts +20 -0
- package/dist/module/identity/service/identityAuth.guard.js +117 -0
- package/dist/module/identity/service/identityManagement.d.ts +16 -0
- package/dist/module/identity/service/identityManagement.js +54 -0
- package/dist/module/identity/service/identityRuntime.d.ts +2 -0
- package/dist/module/identity/service/identityRuntime.js +6 -0
- package/dist/module/identity/service/identityRuntimeInternals.d.ts +11 -0
- package/dist/module/identity/service/identityRuntimeInternals.js +52 -0
- package/dist/module/identity/service/signIn.cache.d.ts +20 -0
- package/dist/module/identity/service/signIn.cache.js +102 -0
- package/dist/module/jwt/service/signedToken.verifier.d.ts +12 -0
- package/dist/module/jwt/service/signedToken.verifier.js +56 -0
- package/dist/module/zitadel/domain/model/zitadelManagementOptions.d.ts +4 -0
- package/dist/module/zitadel/domain/model/zitadelManagementOptions.js +31 -0
- package/dist/module/zitadel/domain/model/zitadelProviderOptions.d.ts +10 -0
- package/dist/module/zitadel/domain/model/zitadelProviderOptions.js +48 -0
- package/dist/module/zitadel/domain/model/zitadelServiceUserKey.d.ts +6 -0
- package/dist/module/zitadel/domain/model/zitadelServiceUserKey.js +42 -0
- package/dist/module/zitadel/domain/validation/isRsaPrivateKey.decorator.d.ts +2 -0
- package/dist/module/zitadel/domain/validation/isRsaPrivateKey.decorator.js +26 -0
- package/dist/module/zitadel/domain/validation/organizationIdList.decorator.d.ts +1 -0
- package/dist/module/zitadel/domain/validation/organizationIdList.decorator.js +19 -0
- package/dist/module/zitadel/index.d.ts +4 -0
- package/dist/module/zitadel/index.js +11 -0
- package/dist/module/zitadel/service/zitadel.provider.d.ts +40 -0
- package/dist/module/zitadel/service/zitadel.provider.js +266 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +7 -0
- package/dist/testing/service/testIdentityProvider.mock.d.ts +42 -0
- package/dist/testing/service/testIdentityProvider.mock.js +118 -0
- package/dist/testing/service/userStore.contract.d.ts +17 -0
- package/dist/testing/service/userStore.contract.js +156 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +5 -0
- package/package.json +139 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OrganizationIdList = OrganizationIdList;
|
|
4
|
+
const class_validator_1 = require("class-validator");
|
|
5
|
+
const trimmed_decorator_1 = require("../../../identity/domain/validation/trimmed.decorator");
|
|
6
|
+
function OrganizationIdList() {
|
|
7
|
+
const stack = [
|
|
8
|
+
(0, trimmed_decorator_1.TrimmedList)(),
|
|
9
|
+
(0, class_validator_1.IsArray)(),
|
|
10
|
+
(0, class_validator_1.ArrayNotEmpty)({ message: '$property must list at least one organization id' }),
|
|
11
|
+
(0, class_validator_1.IsString)({ each: true }),
|
|
12
|
+
(0, class_validator_1.IsNotEmpty)({ each: true }),
|
|
13
|
+
];
|
|
14
|
+
return (target, property) => {
|
|
15
|
+
for (const decorator of [...stack].reverse()) {
|
|
16
|
+
decorator(target, property);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { ZitadelManagementOptions } from './domain/model/zitadelManagementOptions';
|
|
2
|
+
export { ZitadelProviderOptions } from './domain/model/zitadelProviderOptions';
|
|
3
|
+
export { ZitadelServiceUserKey } from './domain/model/zitadelServiceUserKey';
|
|
4
|
+
export { ZitadelProvider } from './service/zitadel.provider';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ZitadelProvider = exports.ZitadelServiceUserKey = exports.ZitadelProviderOptions = exports.ZitadelManagementOptions = void 0;
|
|
4
|
+
var zitadelManagementOptions_1 = require("./domain/model/zitadelManagementOptions");
|
|
5
|
+
Object.defineProperty(exports, "ZitadelManagementOptions", { enumerable: true, get: function () { return zitadelManagementOptions_1.ZitadelManagementOptions; } });
|
|
6
|
+
var zitadelProviderOptions_1 = require("./domain/model/zitadelProviderOptions");
|
|
7
|
+
Object.defineProperty(exports, "ZitadelProviderOptions", { enumerable: true, get: function () { return zitadelProviderOptions_1.ZitadelProviderOptions; } });
|
|
8
|
+
var zitadelServiceUserKey_1 = require("./domain/model/zitadelServiceUserKey");
|
|
9
|
+
Object.defineProperty(exports, "ZitadelServiceUserKey", { enumerable: true, get: function () { return zitadelServiceUserKey_1.ZitadelServiceUserKey; } });
|
|
10
|
+
var zitadel_provider_1 = require("./service/zitadel.provider");
|
|
11
|
+
Object.defineProperty(exports, "ZitadelProvider", { enumerable: true, get: function () { return zitadel_provider_1.ZitadelProvider; } });
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Type } from '@nestjs/common';
|
|
2
|
+
import { IdentityProfile } from '../../../common/domain/model/identityProfile';
|
|
3
|
+
import { IdentityProvider, ProviderIdentity } from '../../identity/domain/model/identityProvider';
|
|
4
|
+
import { ZitadelManagementOptions } from '../domain/model/zitadelManagementOptions';
|
|
5
|
+
import { ZitadelProviderOptions } from '../domain/model/zitadelProviderOptions';
|
|
6
|
+
export declare class ZitadelProvider extends IdentityProvider<ZitadelManagementOptions> {
|
|
7
|
+
readonly issuers: [string, ...string[]];
|
|
8
|
+
private readonly issuer;
|
|
9
|
+
private readonly projectId;
|
|
10
|
+
private readonly organizationIds;
|
|
11
|
+
private readonly roleOrganizationIds;
|
|
12
|
+
private readonly userinfoTimeoutMs;
|
|
13
|
+
private readonly serviceUserKey;
|
|
14
|
+
private readonly managementTimeoutMs;
|
|
15
|
+
private readonly verifier;
|
|
16
|
+
private accessToken;
|
|
17
|
+
private readonly organizationDomains;
|
|
18
|
+
constructor(options: ZitadelProviderOptions);
|
|
19
|
+
authenticate(accessToken: string): Promise<ProviderIdentity>;
|
|
20
|
+
private rolesOf;
|
|
21
|
+
readProfile(accessToken: string, identity: ProviderIdentity): Promise<ProviderIdentity>;
|
|
22
|
+
private fetchUserinfo;
|
|
23
|
+
get managementOptionsType(): Type<ZitadelManagementOptions> | undefined;
|
|
24
|
+
private request;
|
|
25
|
+
private serviceUserAccessToken;
|
|
26
|
+
private fetchServiceUserAccessToken;
|
|
27
|
+
private post;
|
|
28
|
+
private static normalizeEmail;
|
|
29
|
+
private static requireUserId;
|
|
30
|
+
private static isTemporary;
|
|
31
|
+
private static readAnswer;
|
|
32
|
+
findByEmail(email: string, managementOptions: ZitadelManagementOptions): Promise<string | null>;
|
|
33
|
+
create(profile: IdentityProfile, managementOptions: ZitadelManagementOptions): Promise<string>;
|
|
34
|
+
createAccountSetupLink(identityId: string, managementOptions: ZitadelManagementOptions, options?: {
|
|
35
|
+
returnUrl?: string;
|
|
36
|
+
}): Promise<string>;
|
|
37
|
+
isFederated(email: string, managementOptions: ZitadelManagementOptions): Promise<boolean>;
|
|
38
|
+
private domainsOf;
|
|
39
|
+
private fetchDomains;
|
|
40
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ZitadelProvider = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const jose_1 = require("jose");
|
|
6
|
+
const identityConnectorExceptionType_enum_1 = require("../../../common/domain/enum/identityConnectorExceptionType.enum");
|
|
7
|
+
const identityConnectorException_1 = require("../../identity/domain/exception/identityConnectorException");
|
|
8
|
+
const identityProvider_1 = require("../../identity/domain/model/identityProvider");
|
|
9
|
+
const validateOptions_1 = require("../../identity/domain/validation/validateOptions");
|
|
10
|
+
const signedToken_verifier_1 = require("../../jwt/service/signedToken.verifier");
|
|
11
|
+
const zitadelManagementOptions_1 = require("../domain/model/zitadelManagementOptions");
|
|
12
|
+
const zitadelProviderOptions_1 = require("../domain/model/zitadelProviderOptions");
|
|
13
|
+
const ROLES_CLAIM = 'urn:zitadel:iam:org:project:roles';
|
|
14
|
+
const ORGANIZATION_CLAIM = 'urn:zitadel:iam:user:resourceowner:id';
|
|
15
|
+
const USERINFO_TIMEOUT_MS = 5_000;
|
|
16
|
+
const MANAGEMENT_TIMEOUT_MS = 5_000;
|
|
17
|
+
const ACCESS_TOKEN_MARGIN_MS = 60_000;
|
|
18
|
+
const MANAGEMENT_SCOPE = 'openid urn:zitadel:iam:org:project:id:zitadel:aud';
|
|
19
|
+
const ERROR_BODY_LENGTH = 500;
|
|
20
|
+
const DOMAINS_TTL_MS = 10 * 60_000;
|
|
21
|
+
const ignoreFailure = () => undefined;
|
|
22
|
+
class ZitadelProvider extends identityProvider_1.IdentityProvider {
|
|
23
|
+
constructor(options) {
|
|
24
|
+
const checked = (0, validateOptions_1.validateOptions)(zitadelProviderOptions_1.ZitadelProviderOptions, options, `Zitadel provider "${options?.name}"`);
|
|
25
|
+
super(checked);
|
|
26
|
+
this.userinfoTimeoutMs = USERINFO_TIMEOUT_MS;
|
|
27
|
+
this.managementTimeoutMs = MANAGEMENT_TIMEOUT_MS;
|
|
28
|
+
this.accessToken = null;
|
|
29
|
+
this.organizationDomains = new Map();
|
|
30
|
+
this.issuer = checked.issuer;
|
|
31
|
+
this.issuers = [this.issuer];
|
|
32
|
+
this.projectId = checked.projectId;
|
|
33
|
+
this.organizationIds = checked.organizationIds;
|
|
34
|
+
this.roleOrganizationIds = checked.roleOrganizationIds;
|
|
35
|
+
this.serviceUserKey = checked.serviceUserKey ?? null;
|
|
36
|
+
this.verifier = new signedToken_verifier_1.SignedTokenVerifier({
|
|
37
|
+
keysUrl: `${this.issuer}/oauth/v2/keys`,
|
|
38
|
+
issuers: this.issuers,
|
|
39
|
+
audience: this.projectId,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
async authenticate(accessToken) {
|
|
43
|
+
const claims = await this.verifier.verify(accessToken);
|
|
44
|
+
if (typeof claims.sub !== 'string' || claims.sub === '') {
|
|
45
|
+
throw (0, identityConnectorException_1.identityConnectorException)(identityConnectorExceptionType_enum_1.IdentityConnectorExceptionTypeEnum.INVALID_TOKEN);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
id: claims.sub,
|
|
49
|
+
email: typeof claims.email === 'string' ? claims.email : null,
|
|
50
|
+
emailVerified: claims.email_verified === true,
|
|
51
|
+
givenName: typeof claims.given_name === 'string' ? claims.given_name : null,
|
|
52
|
+
familyName: typeof claims.family_name === 'string' ? claims.family_name : null,
|
|
53
|
+
roles: this.rolesOf(claims),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
rolesOf(claims) {
|
|
57
|
+
const grants = claims[ROLES_CLAIM];
|
|
58
|
+
if (!grants || typeof grants !== 'object' || Array.isArray(grants)) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
return Object.entries(grants)
|
|
62
|
+
.filter(([, organizations]) => organizations !== null &&
|
|
63
|
+
typeof organizations === 'object' &&
|
|
64
|
+
this.roleOrganizationIds.some((organizationId) => Object.hasOwn(organizations, organizationId)))
|
|
65
|
+
.map(([role]) => role);
|
|
66
|
+
}
|
|
67
|
+
async readProfile(accessToken, identity) {
|
|
68
|
+
const info = await this.fetchUserinfo(accessToken);
|
|
69
|
+
if (info.sub !== identity.id) {
|
|
70
|
+
throw (0, identityConnectorException_1.identityConnectorException)(identityConnectorExceptionType_enum_1.IdentityConnectorExceptionTypeEnum.INVALID_TOKEN);
|
|
71
|
+
}
|
|
72
|
+
const organizationId = info[ORGANIZATION_CLAIM];
|
|
73
|
+
if (typeof organizationId !== 'string' || !this.organizationIds.includes(organizationId)) {
|
|
74
|
+
throw (0, identityConnectorException_1.identityConnectorException)(identityConnectorExceptionType_enum_1.IdentityConnectorExceptionTypeEnum.USER_NOT_LINKED);
|
|
75
|
+
}
|
|
76
|
+
const fromUserinfo = typeof info.email === 'string';
|
|
77
|
+
return {
|
|
78
|
+
...identity,
|
|
79
|
+
email: fromUserinfo ? info.email : identity.email,
|
|
80
|
+
emailVerified: fromUserinfo ? info.email_verified === true : identity.emailVerified,
|
|
81
|
+
givenName: typeof info.given_name === 'string' ? info.given_name : identity.givenName,
|
|
82
|
+
familyName: typeof info.family_name === 'string' ? info.family_name : identity.familyName,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async fetchUserinfo(accessToken) {
|
|
86
|
+
let response;
|
|
87
|
+
try {
|
|
88
|
+
response = await fetch(`${this.issuer}/oidc/v1/userinfo`, {
|
|
89
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
90
|
+
signal: AbortSignal.timeout(this.userinfoTimeoutMs),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
throw (0, identityConnectorException_1.identityConnectorException)(identityConnectorExceptionType_enum_1.IdentityConnectorExceptionTypeEnum.PROVIDER_UNREACHABLE);
|
|
95
|
+
}
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
await response.body?.cancel().catch(ignoreFailure);
|
|
98
|
+
throw (0, identityConnectorException_1.identityConnectorException)(ZitadelProvider.isTemporary(response.status)
|
|
99
|
+
? identityConnectorExceptionType_enum_1.IdentityConnectorExceptionTypeEnum.PROVIDER_UNREACHABLE
|
|
100
|
+
: identityConnectorExceptionType_enum_1.IdentityConnectorExceptionTypeEnum.INVALID_TOKEN);
|
|
101
|
+
}
|
|
102
|
+
const info = await response.json().catch(() => undefined);
|
|
103
|
+
if (!info || typeof info !== 'object' || Array.isArray(info)) {
|
|
104
|
+
throw (0, identityConnectorException_1.identityConnectorException)(identityConnectorExceptionType_enum_1.IdentityConnectorExceptionTypeEnum.PROVIDER_UNREACHABLE);
|
|
105
|
+
}
|
|
106
|
+
return info;
|
|
107
|
+
}
|
|
108
|
+
get managementOptionsType() {
|
|
109
|
+
return this.serviceUserKey ? zitadelManagementOptions_1.ZitadelManagementOptions : undefined;
|
|
110
|
+
}
|
|
111
|
+
async request(path, body, description) {
|
|
112
|
+
const accessToken = await this.serviceUserAccessToken();
|
|
113
|
+
const response = await this.post(`${this.issuer}${path}`, {
|
|
114
|
+
headers: { authorization: `Bearer ${accessToken}`, 'content-type': 'application/json' },
|
|
115
|
+
body: JSON.stringify(body),
|
|
116
|
+
});
|
|
117
|
+
if (response.status === 401) {
|
|
118
|
+
this.accessToken = null;
|
|
119
|
+
await response.body?.cancel().catch(ignoreFailure);
|
|
120
|
+
throw (0, identityConnectorException_1.identityConnectorException)(identityConnectorExceptionType_enum_1.IdentityConnectorExceptionTypeEnum.PROVIDER_UNREACHABLE);
|
|
121
|
+
}
|
|
122
|
+
return ZitadelProvider.readAnswer(response, description);
|
|
123
|
+
}
|
|
124
|
+
async serviceUserAccessToken() {
|
|
125
|
+
if (!this.serviceUserKey) {
|
|
126
|
+
throw (0, identityConnectorException_1.identityConnectorException)(identityConnectorExceptionType_enum_1.IdentityConnectorExceptionTypeEnum.NOT_CONFIGURED);
|
|
127
|
+
}
|
|
128
|
+
if (this.accessToken && this.accessToken.renewAt > Date.now()) {
|
|
129
|
+
return this.accessToken.value;
|
|
130
|
+
}
|
|
131
|
+
return this.fetchServiceUserAccessToken(this.serviceUserKey);
|
|
132
|
+
}
|
|
133
|
+
async fetchServiceUserAccessToken({ key, keyId, userId }) {
|
|
134
|
+
const assertion = await new jose_1.SignJWT()
|
|
135
|
+
.setProtectedHeader({ alg: 'RS256', kid: keyId, typ: 'JWT' })
|
|
136
|
+
.setIssuer(userId)
|
|
137
|
+
.setSubject(userId)
|
|
138
|
+
.setAudience(this.issuer)
|
|
139
|
+
.setIssuedAt()
|
|
140
|
+
.setExpirationTime('1h')
|
|
141
|
+
.sign((0, crypto_1.createPrivateKey)(key));
|
|
142
|
+
const response = await this.post(`${this.issuer}/oauth/v2/token`, {
|
|
143
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
144
|
+
body: new URLSearchParams({
|
|
145
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
|
146
|
+
scope: MANAGEMENT_SCOPE,
|
|
147
|
+
assertion,
|
|
148
|
+
}).toString(),
|
|
149
|
+
});
|
|
150
|
+
const answer = await ZitadelProvider.readAnswer(response, 'issue the service user an access token');
|
|
151
|
+
if (typeof answer.access_token !== 'string' || typeof answer.expires_in !== 'number') {
|
|
152
|
+
throw (0, identityConnectorException_1.identityConnectorException)(identityConnectorExceptionType_enum_1.IdentityConnectorExceptionTypeEnum.PROVIDER_UNREACHABLE);
|
|
153
|
+
}
|
|
154
|
+
this.accessToken = {
|
|
155
|
+
value: answer.access_token,
|
|
156
|
+
renewAt: Date.now() + answer.expires_in * 1000 - ACCESS_TOKEN_MARGIN_MS,
|
|
157
|
+
};
|
|
158
|
+
return answer.access_token;
|
|
159
|
+
}
|
|
160
|
+
async post(url, init) {
|
|
161
|
+
try {
|
|
162
|
+
return await fetch(url, { method: 'POST', ...init, signal: AbortSignal.timeout(this.managementTimeoutMs) });
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
throw (0, identityConnectorException_1.identityConnectorException)(identityConnectorExceptionType_enum_1.IdentityConnectorExceptionTypeEnum.PROVIDER_UNREACHABLE);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
static normalizeEmail(email) {
|
|
169
|
+
return email.trim().toLowerCase();
|
|
170
|
+
}
|
|
171
|
+
static requireUserId(userId) {
|
|
172
|
+
if (typeof userId !== 'string' || userId === '') {
|
|
173
|
+
throw (0, identityConnectorException_1.identityConnectorException)(identityConnectorExceptionType_enum_1.IdentityConnectorExceptionTypeEnum.PROVIDER_UNREACHABLE);
|
|
174
|
+
}
|
|
175
|
+
return userId;
|
|
176
|
+
}
|
|
177
|
+
static isTemporary(status) {
|
|
178
|
+
return status >= 500 || status === 408 || status === 429;
|
|
179
|
+
}
|
|
180
|
+
static async readAnswer(response, description) {
|
|
181
|
+
if (ZitadelProvider.isTemporary(response.status)) {
|
|
182
|
+
await response.body?.cancel().catch(ignoreFailure);
|
|
183
|
+
throw (0, identityConnectorException_1.identityConnectorException)(identityConnectorExceptionType_enum_1.IdentityConnectorExceptionTypeEnum.PROVIDER_UNREACHABLE);
|
|
184
|
+
}
|
|
185
|
+
if (!response.ok) {
|
|
186
|
+
const text = (await response.text().catch(ignoreFailure)) ?? '';
|
|
187
|
+
throw new Error(`Zitadel refused to ${description}: ${response.status} ${text.slice(0, ERROR_BODY_LENGTH)}`.trim());
|
|
188
|
+
}
|
|
189
|
+
const answer = await response.json().catch(ignoreFailure);
|
|
190
|
+
if (!answer || typeof answer !== 'object' || Array.isArray(answer)) {
|
|
191
|
+
throw (0, identityConnectorException_1.identityConnectorException)(identityConnectorExceptionType_enum_1.IdentityConnectorExceptionTypeEnum.PROVIDER_UNREACHABLE);
|
|
192
|
+
}
|
|
193
|
+
return answer;
|
|
194
|
+
}
|
|
195
|
+
async findByEmail(email, managementOptions) {
|
|
196
|
+
const answer = await this.request('/v2/users', {
|
|
197
|
+
query: { limit: 2 },
|
|
198
|
+
queries: [
|
|
199
|
+
{ organizationIdQuery: { organizationId: managementOptions?.organizationId } },
|
|
200
|
+
{
|
|
201
|
+
emailQuery: {
|
|
202
|
+
emailAddress: ZitadelProvider.normalizeEmail(email),
|
|
203
|
+
method: 'TEXT_QUERY_METHOD_EQUALS_IGNORE_CASE',
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
}, 'search the users by email');
|
|
208
|
+
const users = answer.result ?? [];
|
|
209
|
+
if (users.length > 1) {
|
|
210
|
+
throw new Error(`Zitadel holds more than one user with this email in organization ${managementOptions.organizationId}.`);
|
|
211
|
+
}
|
|
212
|
+
return users.length === 0 ? null : ZitadelProvider.requireUserId(users[0].userId);
|
|
213
|
+
}
|
|
214
|
+
async create(profile, managementOptions) {
|
|
215
|
+
const answer = await this.request('/v2/users/human', {
|
|
216
|
+
organization: { orgId: managementOptions?.organizationId },
|
|
217
|
+
profile: { givenName: profile.givenName, familyName: profile.familyName },
|
|
218
|
+
email: { email: ZitadelProvider.normalizeEmail(profile.email), isVerified: true },
|
|
219
|
+
}, 'create the user');
|
|
220
|
+
return ZitadelProvider.requireUserId(answer.userId);
|
|
221
|
+
}
|
|
222
|
+
async createAccountSetupLink(identityId, managementOptions, options) {
|
|
223
|
+
const answer = await this.request(`/v2/users/${encodeURIComponent(identityId)}/password_reset`, { returnCode: {} }, 'create a password code');
|
|
224
|
+
if (typeof answer.verificationCode !== 'string' || answer.verificationCode === '') {
|
|
225
|
+
throw (0, identityConnectorException_1.identityConnectorException)(identityConnectorExceptionType_enum_1.IdentityConnectorExceptionTypeEnum.PROVIDER_UNREACHABLE);
|
|
226
|
+
}
|
|
227
|
+
const query = new URLSearchParams({
|
|
228
|
+
userId: identityId,
|
|
229
|
+
code: answer.verificationCode,
|
|
230
|
+
organization: managementOptions.organizationId,
|
|
231
|
+
});
|
|
232
|
+
return `${this.issuer}/ui/v2/login/password/set?${query}`;
|
|
233
|
+
}
|
|
234
|
+
async isFederated(email, managementOptions) {
|
|
235
|
+
if (!this.serviceUserKey) {
|
|
236
|
+
throw (0, identityConnectorException_1.identityConnectorException)(identityConnectorExceptionType_enum_1.IdentityConnectorExceptionTypeEnum.NOT_CONFIGURED);
|
|
237
|
+
}
|
|
238
|
+
const normalized = ZitadelProvider.normalizeEmail(email);
|
|
239
|
+
const at = normalized.lastIndexOf('@');
|
|
240
|
+
const organizationIds = managementOptions?.federatedOrganizationIds ?? [];
|
|
241
|
+
if (at < 0 || organizationIds.length === 0) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
const domain = normalized.slice(at + 1).replace(/\.$/, '');
|
|
245
|
+
const domainsPerOrganization = await Promise.all(organizationIds.map((id) => this.domainsOf(id)));
|
|
246
|
+
return domainsPerOrganization.some((domains) => domains.includes(domain));
|
|
247
|
+
}
|
|
248
|
+
async domainsOf(organizationId) {
|
|
249
|
+
const cached = this.organizationDomains.get(organizationId);
|
|
250
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
251
|
+
return cached.domains;
|
|
252
|
+
}
|
|
253
|
+
return this.fetchDomains(organizationId);
|
|
254
|
+
}
|
|
255
|
+
async fetchDomains(organizationId) {
|
|
256
|
+
const answer = await this.request(`/v2/organizations/${encodeURIComponent(organizationId)}/domains/search`, {}, `list the domains of organization ${organizationId}`);
|
|
257
|
+
const listed = answer.domains ?? [];
|
|
258
|
+
if (!Array.isArray(listed) || listed.some((entry) => typeof entry?.domain !== 'string')) {
|
|
259
|
+
throw (0, identityConnectorException_1.identityConnectorException)(identityConnectorExceptionType_enum_1.IdentityConnectorExceptionTypeEnum.PROVIDER_UNREACHABLE);
|
|
260
|
+
}
|
|
261
|
+
const domains = listed.map((entry) => entry.domain.toLowerCase());
|
|
262
|
+
this.organizationDomains.set(organizationId, { domains, expiresAt: Date.now() + DOMAINS_TTL_MS });
|
|
263
|
+
return domains;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
exports.ZitadelProvider = ZitadelProvider;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.userStoreContract = exports.TestIdentityProvider = void 0;
|
|
4
|
+
var testIdentityProvider_mock_1 = require("./service/testIdentityProvider.mock");
|
|
5
|
+
Object.defineProperty(exports, "TestIdentityProvider", { enumerable: true, get: function () { return testIdentityProvider_mock_1.TestIdentityProvider; } });
|
|
6
|
+
var userStore_contract_1 = require("./service/userStore.contract");
|
|
7
|
+
Object.defineProperty(exports, "userStoreContract", { enumerable: true, get: function () { return userStore_contract_1.userStoreContract; } });
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { IdentityProfile } from '../../common/domain/model/identityProfile';
|
|
2
|
+
import { IdentityProviderOptions } from '../../module/identity/domain/model/identityProviderOptions';
|
|
3
|
+
import { IdentityProvider, ProviderIdentity } from '../../module/identity/domain/model/identityProvider';
|
|
4
|
+
export interface TestIdentityProviderOptions extends IdentityProviderOptions {
|
|
5
|
+
issuers: string[];
|
|
6
|
+
federatedDomains?: string[];
|
|
7
|
+
}
|
|
8
|
+
export type TestTokenIdentity = Partial<ProviderIdentity> & Pick<ProviderIdentity, 'id'>;
|
|
9
|
+
export interface RecordedManagementCall<TManagementOptions> {
|
|
10
|
+
method: 'findByEmail' | 'create' | 'createAccountSetupLink' | 'isFederated';
|
|
11
|
+
args: unknown[];
|
|
12
|
+
managementOptions: TManagementOptions;
|
|
13
|
+
}
|
|
14
|
+
export declare class TestIdentityProvider<TManagementOptions = unknown> extends IdentityProvider<TManagementOptions> {
|
|
15
|
+
readonly issuers: string[];
|
|
16
|
+
readonly profiles: Map<string, Partial<ProviderIdentity>>;
|
|
17
|
+
readonly rejectedProfiles: Set<string>;
|
|
18
|
+
readonly identities: Map<string, string>;
|
|
19
|
+
readonly calls: RecordedManagementCall<TManagementOptions>[];
|
|
20
|
+
unreachable: boolean;
|
|
21
|
+
private readonly key;
|
|
22
|
+
private readonly federatedDomains;
|
|
23
|
+
private created;
|
|
24
|
+
constructor(options: TestIdentityProviderOptions);
|
|
25
|
+
createToken(identity: TestTokenIdentity, options?: {
|
|
26
|
+
issuer?: string;
|
|
27
|
+
expiresInSeconds?: number;
|
|
28
|
+
}): string;
|
|
29
|
+
authenticate(accessToken: string): Promise<ProviderIdentity>;
|
|
30
|
+
readProfile(accessToken: string, identity: ProviderIdentity): Promise<ProviderIdentity>;
|
|
31
|
+
findByEmail(email: string, managementOptions: TManagementOptions): Promise<string | null>;
|
|
32
|
+
create(profile: IdentityProfile, managementOptions: TManagementOptions): Promise<string>;
|
|
33
|
+
createAccountSetupLink(identityId: string, managementOptions: TManagementOptions, options?: {
|
|
34
|
+
returnUrl?: string;
|
|
35
|
+
}): Promise<string>;
|
|
36
|
+
isFederated(email: string, managementOptions: TManagementOptions): Promise<boolean>;
|
|
37
|
+
private static normalizeEmail;
|
|
38
|
+
reset(): void;
|
|
39
|
+
private sign;
|
|
40
|
+
private verify;
|
|
41
|
+
private assertReachable;
|
|
42
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TestIdentityProvider = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const identityConnectorExceptionType_enum_1 = require("../../common/domain/enum/identityConnectorExceptionType.enum");
|
|
6
|
+
const identityConnectorException_1 = require("../../module/identity/domain/exception/identityConnectorException");
|
|
7
|
+
const identityProvider_1 = require("../../module/identity/domain/model/identityProvider");
|
|
8
|
+
const encode = (value) => Buffer.from(JSON.stringify(value)).toString('base64url');
|
|
9
|
+
class TestIdentityProvider extends identityProvider_1.IdentityProvider {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
super(options);
|
|
12
|
+
this.profiles = new Map();
|
|
13
|
+
this.rejectedProfiles = new Set();
|
|
14
|
+
this.identities = new Map();
|
|
15
|
+
this.calls = [];
|
|
16
|
+
this.unreachable = false;
|
|
17
|
+
this.key = (0, crypto_1.randomBytes)(32);
|
|
18
|
+
this.created = 0;
|
|
19
|
+
this.issuers = options.issuers;
|
|
20
|
+
this.federatedDomains = (options.federatedDomains ?? []).map((domain) => domain.trim().toLowerCase());
|
|
21
|
+
}
|
|
22
|
+
createToken(identity, options = {}) {
|
|
23
|
+
const now = Math.floor(Date.now() / 1000);
|
|
24
|
+
const header = encode({ alg: 'HS256', typ: 'JWT' });
|
|
25
|
+
const payload = encode({
|
|
26
|
+
iss: options.issuer ?? this.issuers[0],
|
|
27
|
+
sub: identity.id,
|
|
28
|
+
iat: now,
|
|
29
|
+
exp: now + (options.expiresInSeconds ?? 3600),
|
|
30
|
+
email: identity.email ?? undefined,
|
|
31
|
+
email_verified: identity.emailVerified ?? undefined,
|
|
32
|
+
given_name: identity.givenName ?? undefined,
|
|
33
|
+
family_name: identity.familyName ?? undefined,
|
|
34
|
+
roles: identity.roles ?? undefined,
|
|
35
|
+
});
|
|
36
|
+
return `${header}.${payload}.${this.sign(`${header}.${payload}`)}`;
|
|
37
|
+
}
|
|
38
|
+
async authenticate(accessToken) {
|
|
39
|
+
this.assertReachable();
|
|
40
|
+
const claims = this.verify(accessToken);
|
|
41
|
+
return {
|
|
42
|
+
id: claims.sub,
|
|
43
|
+
email: claims.email ?? null,
|
|
44
|
+
emailVerified: claims.email_verified === true,
|
|
45
|
+
givenName: claims.given_name ?? null,
|
|
46
|
+
familyName: claims.family_name ?? null,
|
|
47
|
+
roles: Array.isArray(claims.roles) ? claims.roles : [],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async readProfile(accessToken, identity) {
|
|
51
|
+
this.assertReachable();
|
|
52
|
+
if (this.rejectedProfiles.has(identity.id)) {
|
|
53
|
+
throw (0, identityConnectorException_1.identityConnectorException)(identityConnectorExceptionType_enum_1.IdentityConnectorExceptionTypeEnum.USER_NOT_LINKED);
|
|
54
|
+
}
|
|
55
|
+
return { ...identity, ...this.profiles.get(identity.id) };
|
|
56
|
+
}
|
|
57
|
+
async findByEmail(email, managementOptions) {
|
|
58
|
+
this.calls.push({ method: 'findByEmail', args: [email], managementOptions });
|
|
59
|
+
const wanted = TestIdentityProvider.normalizeEmail(email);
|
|
60
|
+
const match = [...this.identities].find(([held]) => TestIdentityProvider.normalizeEmail(held) === wanted);
|
|
61
|
+
return match?.[1] ?? null;
|
|
62
|
+
}
|
|
63
|
+
async create(profile, managementOptions) {
|
|
64
|
+
this.calls.push({ method: 'create', args: [profile], managementOptions });
|
|
65
|
+
this.created += 1;
|
|
66
|
+
const identityId = `${this.name}-identity-${this.created}`;
|
|
67
|
+
this.identities.set(TestIdentityProvider.normalizeEmail(profile.email), identityId);
|
|
68
|
+
return identityId;
|
|
69
|
+
}
|
|
70
|
+
async createAccountSetupLink(identityId, managementOptions, options) {
|
|
71
|
+
this.calls.push({ method: 'createAccountSetupLink', args: [identityId, options], managementOptions });
|
|
72
|
+
const query = options?.returnUrl ? `?returnUrl=${encodeURIComponent(options.returnUrl)}` : '';
|
|
73
|
+
return `${this.issuers[0]}/account-setup/${identityId}${query}`;
|
|
74
|
+
}
|
|
75
|
+
async isFederated(email, managementOptions) {
|
|
76
|
+
this.calls.push({ method: 'isFederated', args: [email], managementOptions });
|
|
77
|
+
const normalized = TestIdentityProvider.normalizeEmail(email);
|
|
78
|
+
return this.federatedDomains.includes(normalized.slice(normalized.lastIndexOf('@') + 1));
|
|
79
|
+
}
|
|
80
|
+
static normalizeEmail(email) {
|
|
81
|
+
return email.trim().toLowerCase();
|
|
82
|
+
}
|
|
83
|
+
reset() {
|
|
84
|
+
this.profiles.clear();
|
|
85
|
+
this.rejectedProfiles.clear();
|
|
86
|
+
this.identities.clear();
|
|
87
|
+
this.calls.length = 0;
|
|
88
|
+
this.unreachable = false;
|
|
89
|
+
this.created = 0;
|
|
90
|
+
}
|
|
91
|
+
sign(content) {
|
|
92
|
+
return (0, crypto_1.createHmac)('sha256', this.key).update(content).digest('base64url');
|
|
93
|
+
}
|
|
94
|
+
verify(accessToken) {
|
|
95
|
+
const invalid = (0, identityConnectorException_1.identityConnectorException)(identityConnectorExceptionType_enum_1.IdentityConnectorExceptionTypeEnum.INVALID_TOKEN);
|
|
96
|
+
const [header, payload, signature, ...rest] = accessToken.split('.');
|
|
97
|
+
if (!header || !payload || !signature || rest.length > 0) {
|
|
98
|
+
throw invalid;
|
|
99
|
+
}
|
|
100
|
+
const expected = Buffer.from(this.sign(`${header}.${payload}`));
|
|
101
|
+
const actual = Buffer.from(signature);
|
|
102
|
+
if (expected.length !== actual.length || !(0, crypto_1.timingSafeEqual)(expected, actual)) {
|
|
103
|
+
throw invalid;
|
|
104
|
+
}
|
|
105
|
+
const claims = JSON.parse(Buffer.from(payload, 'base64url').toString());
|
|
106
|
+
const expired = claims.exp <= Math.floor(Date.now() / 1000);
|
|
107
|
+
if (!this.issuers.includes(claims.iss) || expired) {
|
|
108
|
+
throw invalid;
|
|
109
|
+
}
|
|
110
|
+
return claims;
|
|
111
|
+
}
|
|
112
|
+
assertReachable() {
|
|
113
|
+
if (this.unreachable) {
|
|
114
|
+
throw (0, identityConnectorException_1.identityConnectorException)(identityConnectorExceptionType_enum_1.IdentityConnectorExceptionTypeEnum.PROVIDER_UNREACHABLE);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
exports.TestIdentityProvider = TestIdentityProvider;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { UserStore } from '../../module/identity/domain/model/userStore';
|
|
2
|
+
export interface UserStoreContractFixture<TUser> {
|
|
3
|
+
store: UserStore<TUser>;
|
|
4
|
+
providers: string[];
|
|
5
|
+
createUser(values: {
|
|
6
|
+
email: string | null;
|
|
7
|
+
givenName: string;
|
|
8
|
+
familyName: string;
|
|
9
|
+
}): Promise<TUser>;
|
|
10
|
+
userId(user: TUser): unknown;
|
|
11
|
+
usersWithoutEmail?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface ContractCheck {
|
|
14
|
+
name: string;
|
|
15
|
+
run(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export declare function userStoreContract<TUser>(fixture: () => UserStoreContractFixture<TUser> | Promise<UserStoreContractFixture<TUser>>): ContractCheck[];
|