@ballistix.digital/identity-connector 0.0.0-stage → 0.2.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 +6 -0
- package/README.md +630 -2
- 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 +138 -5
package/CHANGELOG.md
ADDED
package/README.md
CHANGED
|
@@ -1,3 +1,631 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ballistix.digital/identity-connector
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A NestJS module that signs a user in with an OIDC access token and provisions
|
|
4
|
+
users at one or more identity providers, behind provider-agnostic contracts.
|
|
5
|
+
The app does not know which provider is behind it, and one app can use more
|
|
6
|
+
than one at the same time. The providers are Zitadel and Entra, each behind
|
|
7
|
+
an entry of its own.
|
|
8
|
+
|
|
9
|
+
This file is the consumer guide and the public contract. The pages that
|
|
10
|
+
describe how the package is built live in the
|
|
11
|
+
[Ballistix wiki](https://github.com/layeredprints/ballistix-okf-wiki/blob/main/general/libraries/identity-connector/).
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @ballistix.digital/identity-connector @ballistix.digital/exception-mapper @ballistix.digital/exception-types class-validator class-transformer
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The providers verify tokens, and the Zitadel provider signs its service user's
|
|
20
|
+
assertion, with `jose`. Install it when the application imports `./zitadel` or
|
|
21
|
+
`./entra`:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install jose@^6
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`jose` ships ES modules only. Node 24 loads it from CommonJS by itself, but
|
|
28
|
+
Jest's CommonJS runtime does not: a Jest setup that loads a provider entry
|
|
29
|
+
transforms `jose` to CommonJS (`transformIgnorePatterns:
|
|
30
|
+
['/node_modules/(?!jose/)']`). Give `jose` a transformer of its own that only
|
|
31
|
+
rewrites the import syntax and keeps class fields as they are
|
|
32
|
+
(`useDefineForClassFields: true`): `jose`'s error classes break under the
|
|
33
|
+
`[[Set]]` semantics Ballistix packages compile with, and ts-jest keeps one
|
|
34
|
+
configuration per Jest project, so a second ts-jest entry does not help. This
|
|
35
|
+
repository's `test/module/common/esModule.transformer.js` is such a
|
|
36
|
+
transformer.
|
|
37
|
+
|
|
38
|
+
A browser build that reads the `./types` entry alone installs the package by
|
|
39
|
+
itself:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install @ballistix.digital/identity-connector
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The package needs Node 24 or later and has no dependencies of its own.
|
|
46
|
+
Every peer dependency is optional, so npm installs none of them for you:
|
|
47
|
+
install in the application what the entries it imports need.
|
|
48
|
+
|
|
49
|
+
## Entries
|
|
50
|
+
|
|
51
|
+
| Entry | Holds |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| `.` | The module, the runtime, the contracts, and `identityConnectorException` and `identityConnectorExceptionTypeOf` |
|
|
54
|
+
| `./zitadel` | The Zitadel provider |
|
|
55
|
+
| `./entra` | The Entra provider |
|
|
56
|
+
| `./testing` | The test doubles and the store contract |
|
|
57
|
+
| `./types` | The exception type enum, with no framework import |
|
|
58
|
+
|
|
59
|
+
## Signing in
|
|
60
|
+
|
|
61
|
+
An app writes three classes and registers them. Each one extends a class of
|
|
62
|
+
the package, which holds the logic.
|
|
63
|
+
|
|
64
|
+
1. Register `ExceptionModule` and the app's provider classes, one per identity
|
|
65
|
+
provider connection:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
@Module({
|
|
69
|
+
imports: [
|
|
70
|
+
ExceptionModule.forRoot(),
|
|
71
|
+
IdentityConnectorModule.forRoot({ identityProviders: [ComplianceZitadel] }),
|
|
72
|
+
],
|
|
73
|
+
})
|
|
74
|
+
export class AppModule {}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The module builds each provider once, with what its constructor injects,
|
|
78
|
+
so a global `ConfigModule` or a module in `imports` must provide it; every
|
|
79
|
+
guard and identity management then shares that instance. Two providers
|
|
80
|
+
with the same name stop the app at boot, since a store keeps an identity
|
|
81
|
+
under its provider's name, and so do two providers that list the same
|
|
82
|
+
issuer: a token names only its issuer, so with the same audience each would
|
|
83
|
+
accept the other's tokens. A missing `ExceptionModule` stops it too:
|
|
84
|
+
without its filter, every refusal would reach the client as a 500.
|
|
85
|
+
|
|
86
|
+
2. Write the user store over the app's own table. It implements `UserStore`,
|
|
87
|
+
and the library never touches the table itself:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
@Injectable()
|
|
91
|
+
export class UserStoreService extends UserStore<User> {
|
|
92
|
+
findByIdentityId(identityId, provider, transaction) { /* find by the column of that provider, on the transaction when given */ }
|
|
93
|
+
async linkByEmail(identity) { /* one match by email, conditional link */ }
|
|
94
|
+
async create(identity) { /* only with createOnSignIn: return the user the identity already has, or insert one */ }
|
|
95
|
+
async sync(user, identity) { /* write roles, email, names; return the value for request.user */ }
|
|
96
|
+
toProfile(user, provider) { /* throw identityConnectorException(NO_EMAIL) when the user has none */ }
|
|
97
|
+
link(user, identityId, provider, transaction) { /* conditional: no other identity, no other user, by primary key */ }
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
3. Extend the guard, pass it the providers whose tokens it accepts, and put it
|
|
102
|
+
on the controllers. `request.user` is what `sync()` returned:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
@Injectable()
|
|
106
|
+
export class AuthGuard extends IdentityAuthGuard<User> {
|
|
107
|
+
constructor(runtime: IdentityRuntime, store: UserStoreService, zitadel: ComplianceZitadel) {
|
|
108
|
+
super(runtime, store, [zitadel]);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@UseGuards(AuthGuard)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
A guard accepts the tokens of the providers it passes to `super()`, at least
|
|
116
|
+
one, injected by the class it registered in `IdentityConnectorModule.forRoot()`;
|
|
117
|
+
a guard for employee routes passes only the Entra provider. A token goes to
|
|
118
|
+
the one among them that lists its issuer, before anything is verified, and a
|
|
119
|
+
token of another registered provider is refused as `PROVIDER_NOT_ACCEPTED`. A
|
|
120
|
+
guard without providers, or with a provider the module does not register,
|
|
121
|
+
stops the app at boot. A provider the app registers only when it is
|
|
122
|
+
configured is injected with `@Optional()` and passed only when it is set:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
constructor(runtime: IdentityRuntime, store: UserStoreService, entra: ComplianceEntra, @Optional() zitadel?: ComplianceZitadel) {
|
|
126
|
+
super(runtime, store, zitadel ? [entra, zitadel] : [entra]);
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Provisioning users
|
|
131
|
+
|
|
132
|
+
Provisioning gives one of the app's users an identity at the identity
|
|
133
|
+
provider, for example when the app invites a customer: the identity provider's
|
|
134
|
+
identity with the user's email, or a new one, linked to the user through the
|
|
135
|
+
store's `link()`. The app then sends its own mail with a link where the user
|
|
136
|
+
sets up how they sign in, such as a password.
|
|
137
|
+
|
|
138
|
+
Extend `IdentityManagement` once per place the app manages identities, such as
|
|
139
|
+
customers and employees. It injects the store and the provider class, and
|
|
140
|
+
passes the management options of that place: their shape is the provider's,
|
|
141
|
+
for Zitadel the organization new users land in.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
@Injectable()
|
|
145
|
+
export class CustomerIdentityManagement extends IdentityManagement<User, ComplianceZitadel> {
|
|
146
|
+
constructor(runtime: IdentityRuntime, store: UserStoreService, zitadel: ComplianceZitadel, config: ConfigService) {
|
|
147
|
+
super(runtime, store, zitadel, {
|
|
148
|
+
organizationId: config.get('ZITADEL_CUSTOMERS_ORG_ID'),
|
|
149
|
+
federatedOrganizationIds: [config.get('ZITADEL_VDL_ORG_ID')],
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Inject it where the domain needs it:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
if (!(await this.customers.isFederated(user))) {
|
|
159
|
+
await this.customers.provision(user, transaction);
|
|
160
|
+
// queue the app's own mail
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// in the mail job, at send time, so the link is never stored
|
|
164
|
+
const link = await this.customers.createAccountSetupLink(user, { returnUrl });
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
- **`isFederated(user)`** is true when the user's email signs in elsewhere,
|
|
168
|
+
for example employees whose domain belongs to an organization that signs in
|
|
169
|
+
through Entra. **Call it before `provision()`**: `provision()` does not check
|
|
170
|
+
it, and would give such a user an identity of its own.
|
|
171
|
+
- **`provision(user, transaction?)`** does nothing for a user already linked to
|
|
172
|
+
the provider. Otherwise it finds the identity by email or creates one, and
|
|
173
|
+
links it with `link(user, identityId, provider, transaction)`. The store's
|
|
174
|
+
`link()` decides: it refuses a user that has another identity of the
|
|
175
|
+
provider, and an identity another user has. When it refuses,
|
|
176
|
+
`provision()` reads the owner with
|
|
177
|
+
`findByIdentityId(identityId, provider, transaction)` to throw
|
|
178
|
+
`IDENTITY_ALREADY_LINKED` or `LINK_REFUSED`. The transaction reaches both
|
|
179
|
+
unchanged, so the store runs them on the connection the transaction holds:
|
|
180
|
+
with a pool of one connection, a read outside it would wait forever. The
|
|
181
|
+
guard calls `findByIdentityId` without a transaction. `provision()` returns
|
|
182
|
+
nothing and throws on failure. When it fails after the identity was
|
|
183
|
+
created, the next call finds that identity by email and links it, rather
|
|
184
|
+
than creating a second one.
|
|
185
|
+
- **`createAccountSetupLink(user, { returnUrl })`** asks the provider for a
|
|
186
|
+
link where the identity sets up how it signs in, such as a password.
|
|
187
|
+
|
|
188
|
+
A sign-in refused just before the user is provisioned is not remembered after
|
|
189
|
+
`provision()` links the identity, so the user signs in at once. That holds
|
|
190
|
+
when the guard and the identity management inject the same store instance:
|
|
191
|
+
the guard remembers sign-ins per store instance, so two store classes are two
|
|
192
|
+
caches, even over one table, and so is one class registered in two modules.
|
|
193
|
+
Register the store once, as a singleton: a request-scoped store gets a new
|
|
194
|
+
cache on every request, which remembers nothing and does not run parallel
|
|
195
|
+
first sign-ins of one identity one at a time. Calling `provision()` again for
|
|
196
|
+
a user already linked clears the refusal as well.
|
|
197
|
+
|
|
198
|
+
Each method reads the email and the identity id through the store's
|
|
199
|
+
`toProfile(user, provider)`, so the app never handles an identity id. Two
|
|
200
|
+
identity managements may inject one provider class, each with its own
|
|
201
|
+
management options. An identity management whose provider class is not
|
|
202
|
+
registered in `IdentityConnectorModule.forRoot()` stops the app at boot.
|
|
203
|
+
|
|
204
|
+
What the methods throw; [Exceptions](#exceptions) lists every type with its code and field:
|
|
205
|
+
|
|
206
|
+
| Type | Status | Exception | When |
|
|
207
|
+
| --- | --- | --- | --- |
|
|
208
|
+
| `NO_EMAIL` | 409 | `PreconditionFailedException` | `toProfile()` found no email on the user |
|
|
209
|
+
| `IDENTITY_ALREADY_LINKED` | 409 | `PreconditionFailedException` | `provision()`: the store's `link()` refused, and another user owns the identity with the user's email |
|
|
210
|
+
| `LINK_REFUSED` | 409 | `PreconditionFailedException` | `provision()`: the store's `link()` refused for another reason, such as a user with another identity of the provider |
|
|
211
|
+
| `USER_NOT_PROVISIONED` | 409 | `PreconditionFailedException` | `createAccountSetupLink()`: the user has no identity at this provider |
|
|
212
|
+
| `NOT_CONFIGURED` | 503 | `GenericErrorException` | The provider holds no management credentials. Catch it to skip provisioning, for example on a local machine |
|
|
213
|
+
| `NOT_SUPPORTED` | 501 | `GenericErrorException` | The provider cannot perform the call |
|
|
214
|
+
| `PROVIDER_UNREACHABLE` | 503 | `GenericErrorException` | The provider could not be reached |
|
|
215
|
+
|
|
216
|
+
A refusal of the provider that retrying does not fix, such as a rejected key,
|
|
217
|
+
is a plain `Error` with the provider's answer; the provider's section says
|
|
218
|
+
which.
|
|
219
|
+
|
|
220
|
+
## Exceptions
|
|
221
|
+
|
|
222
|
+
The library has no exception class of its own. It throws the exceptions of
|
|
223
|
+
`@ballistix.digital/exception-types`, so the app's exception filter and its
|
|
224
|
+
frontends see the same classes and envelopes as for any other error. Each
|
|
225
|
+
type of `IdentityConnectorExceptionTypeEnum` reaches the client as:
|
|
226
|
+
|
|
227
|
+
| Type | Status | Code | Exception | Where the type sits | Thrown by |
|
|
228
|
+
| --- | --- | --- | --- | --- | --- |
|
|
229
|
+
| `NO_TOKEN` | 401 | `UNAUTHENTICATED` | `UnauthenticatedException` | `errors[0].message` | The guard: no `Bearer` token |
|
|
230
|
+
| `UNKNOWN_ISSUER` | 401 | `UNAUTHENTICATED` | `UnauthenticatedException` | `errors[0].message` | The guard: no registered provider lists the token's issuer |
|
|
231
|
+
| `PROVIDER_NOT_ACCEPTED` | 401 | `UNAUTHENTICATED` | `UnauthenticatedException` | `errors[0].message` | The guard: the provider that lists the token's issuer is registered, but not one the guard accepts |
|
|
232
|
+
| `INVALID_TOKEN` | 401 | `UNAUTHENTICATED` | `UnauthenticatedException` | `errors[0].message` | The guard and the providers: the token is not valid |
|
|
233
|
+
| `USER_NOT_LINKED` | 403 | `ACCESS_DENIED` | `AccessDeniedException` | `errors[0].message` | The guard: no user for the identity |
|
|
234
|
+
| `NO_EMAIL` | 409 | `PRECONDITION_FAILED` | `PreconditionFailedException` | `errors[0].condition` | The store's `toProfile()` |
|
|
235
|
+
| `IDENTITY_ALREADY_LINKED` | 409 | `PRECONDITION_FAILED` | `PreconditionFailedException` | `errors[0].condition` | `provision()` |
|
|
236
|
+
| `LINK_REFUSED` | 409 | `PRECONDITION_FAILED` | `PreconditionFailedException` | `errors[0].condition` | `provision()` |
|
|
237
|
+
| `USER_NOT_PROVISIONED` | 409 | `PRECONDITION_FAILED` | `PreconditionFailedException` | `errors[0].condition` | `createAccountSetupLink()` |
|
|
238
|
+
| `NOT_SUPPORTED` | 501 | `GENERIC_ERROR` | `GenericErrorException` | `errors[0].message` | A management call the provider cannot perform |
|
|
239
|
+
| `NOT_CONFIGURED` | 503 | `GENERIC_ERROR` | `GenericErrorException` | `errors[0].message` | A management call without credentials |
|
|
240
|
+
| `PROVIDER_UNREACHABLE` | 503 | `GENERIC_ERROR` | `GenericErrorException` | `errors[0].message` | A provider that cannot reach its identity provider |
|
|
241
|
+
|
|
242
|
+
A frontend that maps the library's refusals reads the type from the field the
|
|
243
|
+
table names; the code alone does not tell `USER_NOT_LINKED` apart from another
|
|
244
|
+
403, nor a 401 of the library from another `UNAUTHENTICATED`.
|
|
245
|
+
|
|
246
|
+
- **To react to one case**, read the type with `identityConnectorExceptionTypeOf(error)`
|
|
247
|
+
and compare it; never compare the message or the class. It answers only
|
|
248
|
+
for an exception with the code and status the type is thrown with, and
|
|
249
|
+
`undefined` for anything else.
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
try {
|
|
253
|
+
await this.customers.provision(user, transaction);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (identityConnectorExceptionTypeOf(error) !== IdentityConnectorExceptionTypeEnum.NOT_CONFIGURED) throw error;
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
- **To throw one**, from a store or from a provider the app writes, use
|
|
260
|
+
`identityConnectorException(type)`: it picks the class and status the type reaches
|
|
261
|
+
the client with, for example
|
|
262
|
+
`throw identityConnectorException(IdentityConnectorExceptionTypeEnum.NO_EMAIL)` in
|
|
263
|
+
`toProfile()`.
|
|
264
|
+
|
|
265
|
+
## Zitadel
|
|
266
|
+
|
|
267
|
+
`ZitadelProvider` from `./zitadel` signs users in with access tokens of one
|
|
268
|
+
Zitadel instance. The app extends it once per connection:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
@Injectable()
|
|
272
|
+
export class ComplianceZitadel extends ZitadelProvider {
|
|
273
|
+
constructor(config: ConfigService) {
|
|
274
|
+
super({
|
|
275
|
+
name: 'zitadel',
|
|
276
|
+
issuer: config.get('ZITADEL_ISSUER'),
|
|
277
|
+
projectId: config.get('ZITADEL_PROJECT_ID'),
|
|
278
|
+
organizationIds: [config.get('ZITADEL_VDL_ORG_ID')],
|
|
279
|
+
roleOrganizationIds: [config.get('ZITADEL_VDL_ORG_ID')],
|
|
280
|
+
linkByEmail: true,
|
|
281
|
+
serviceUserKey: config.get('ZITADEL_SERVICE_USER_KEY')
|
|
282
|
+
? JSON.parse(config.get('ZITADEL_SERVICE_USER_KEY'))
|
|
283
|
+
: undefined,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
| Option | Required | Meaning |
|
|
290
|
+
| --- | --- | --- |
|
|
291
|
+
| `name` | yes | The provider name; unique in the app |
|
|
292
|
+
| `issuer` | yes | The Zitadel instance URL: the one issuer it accepts and the base of its endpoints. A trailing slash is dropped |
|
|
293
|
+
| `projectId` | yes | The Zitadel project: the audience every token carries |
|
|
294
|
+
| `organizationIds` | yes, at least one | The organizations whose users may be linked or created at sign-in |
|
|
295
|
+
| `roleOrganizationIds` | yes, at least one | The organizations whose role grants count |
|
|
296
|
+
| `linkByEmail`, `createOnSignIn` | no, both off | As for every provider |
|
|
297
|
+
| `serviceUserKey` | no | The parsed key file of the service user that calls the management API. Left out, every management call throws `NOT_CONFIGURED`; `null` stops the app at boot |
|
|
298
|
+
|
|
299
|
+
Both organization lists are required and may not be empty. An empty list would
|
|
300
|
+
quietly let every organization in, or let every organization's role grants
|
|
301
|
+
count: another organization with a grant on the project could then hand its
|
|
302
|
+
users an elevated role. A connection with a missing or empty option stops the
|
|
303
|
+
app at boot with a message that names the provider and every failing option.
|
|
304
|
+
|
|
305
|
+
The app's Zitadel application must issue JWT access tokens. The provider
|
|
306
|
+
verifies each token against the instance's signing keys (RS256, this issuer,
|
|
307
|
+
this project as audience) and caches the keys, so a signed-in user costs no
|
|
308
|
+
call to Zitadel; an opaque access token cannot be verified and is refused as
|
|
309
|
+
not valid.
|
|
310
|
+
|
|
311
|
+
The keys are cached by `jose`'s `createRemoteJWKSet`. A token whose key is
|
|
312
|
+
cached is answered from the cache. Once the keys are ten minutes old, the next
|
|
313
|
+
token waits for a fresh set. A token with a key id the cache lacks fetches the
|
|
314
|
+
keys again when the last fetch that succeeded is 30 seconds old or more;
|
|
315
|
+
otherwise, it is refused as not valid (401). Every token waiting on a fetch
|
|
316
|
+
shares it. Tokens with made-up key ids therefore cost Zitadel at most one
|
|
317
|
+
request per 30 seconds and never lock out a user whose key is known. A key
|
|
318
|
+
Zitadel adds is accepted within 30 seconds, and a key it withdraws is refused
|
|
319
|
+
within ten minutes. A fetch gets 5 seconds; when the keys are needed and cannot
|
|
320
|
+
be fetched, the token fails with `PROVIDER_UNREACHABLE` (503) rather than as
|
|
321
|
+
not valid, so a user with a key Zitadel just added is not signed out. While
|
|
322
|
+
Zitadel's key endpoint is down, every sign-in fails this way once the keys are
|
|
323
|
+
ten minutes old.
|
|
324
|
+
|
|
325
|
+
On a first sign-in, when no user is linked yet, the provider calls Zitadel's
|
|
326
|
+
userinfo endpoint to complete the identity: email, whether Zitadel verified
|
|
327
|
+
it, and names. It refuses a user whose organization is not in
|
|
328
|
+
`organizationIds` with `USER_NOT_LINKED` (403). The frontend must request the
|
|
329
|
+
scope `urn:zitadel:iam:user:resourceowner` at sign-in: without it userinfo
|
|
330
|
+
leaves the organization out, and every first sign-in is refused. The provider
|
|
331
|
+
passes `emailVerified` on without checking it; a store that links by email
|
|
332
|
+
decides whether an unverified email may link.
|
|
333
|
+
|
|
334
|
+
### Managing Zitadel users
|
|
335
|
+
|
|
336
|
+
With `serviceUserKey`, an `IdentityManagement` on this provider provisions
|
|
337
|
+
users at Zitadel. Its management options:
|
|
338
|
+
|
|
339
|
+
| Management option | Required | Meaning |
|
|
340
|
+
| --- | --- | --- |
|
|
341
|
+
| `organizationId` | yes | The organization `provision()` searches by email and creates new users in |
|
|
342
|
+
| `federatedOrganizationIds` | no | Organizations whose users sign in elsewhere: `isFederated()` is true for an email on one of their domains |
|
|
343
|
+
|
|
344
|
+
The service user is a machine user with a key of type JSON. It needs to search
|
|
345
|
+
and create users and create password codes in `organizationId`, and to read the
|
|
346
|
+
domains of `federatedOrganizationIds`. The provider exchanges an assertion
|
|
347
|
+
signed with its key for an access token (Zitadel's JWT profile grant) and
|
|
348
|
+
reuses that token until a minute before it expires.
|
|
349
|
+
|
|
350
|
+
- **`findByEmail`** searches `organizationId` alone, without regard to case.
|
|
351
|
+
Two users there with one email are an error rather than a pick.
|
|
352
|
+
- **`create`** makes a human user with a verified email and no password.
|
|
353
|
+
- **`createAccountSetupLink`** returns a link to the password page of
|
|
354
|
+
Zitadel's login (v2), with a password code Zitadel returns instead of mailing
|
|
355
|
+
it. The code is valid as long as the instance's setting for password reset
|
|
356
|
+
codes says, so ask for the link when the mail is sent; every call creates a
|
|
357
|
+
new one. The page reads no return address, so `returnUrl` is ignored.
|
|
358
|
+
- **`isFederated`** compares the email's domain with the domains of
|
|
359
|
+
`federatedOrganizationIds`, exactly: a subdomain does not count. It caches
|
|
360
|
+
each organization's domains for ten minutes, so a domain an admin adds
|
|
361
|
+
counts within that time.
|
|
362
|
+
- **Every email is sent trimmed and in lowercase.** Zitadel matches uppercase
|
|
363
|
+
letters badly in its user search and its domain discovery (Zitadel issue
|
|
364
|
+
#12025).
|
|
365
|
+
|
|
366
|
+
The management options are checked at boot when `serviceUserKey` is given: a
|
|
367
|
+
missing `organizationId` stops the app with a message that names the identity
|
|
368
|
+
management. Without the key they go unchecked, so an app on a local machine
|
|
369
|
+
may leave both unset; every management call then throws `NOT_CONFIGURED`,
|
|
370
|
+
which the app catches to skip provisioning.
|
|
371
|
+
|
|
372
|
+
A call Zitadel cannot answer, through a network failure, a timeout of 5
|
|
373
|
+
seconds, a 5xx, a 408 or a 429, is `PROVIDER_UNREACHABLE` (503). So is a 401:
|
|
374
|
+
it may only mean Zitadel ended the service user's token early, so the provider
|
|
375
|
+
drops that token and the next call fetches a new one; the app tries again, as
|
|
376
|
+
for any 503. Any other refusal, such as a key Zitadel rejects when the provider
|
|
377
|
+
asks for a token or a service user without a permission, is a plain `Error`
|
|
378
|
+
with Zitadel's status and answer: a fault in the configuration that retrying
|
|
379
|
+
does not fix, and that an app skipping `NOT_CONFIGURED` must not skip
|
|
380
|
+
silently.
|
|
381
|
+
|
|
382
|
+
## Entra
|
|
383
|
+
|
|
384
|
+
`EntraProvider` from `./entra` signs users in with access tokens of one
|
|
385
|
+
Entra tenant, and with `acceptIdTokens` also with its ID tokens (see
|
|
386
|
+
[ID tokens](#id-tokens)); without that option an ID token is refused. The app
|
|
387
|
+
extends it once per tenant:
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
@Injectable()
|
|
391
|
+
export class ComplianceEntra extends EntraProvider {
|
|
392
|
+
constructor(config: ConfigService) {
|
|
393
|
+
super({
|
|
394
|
+
name: 'entra',
|
|
395
|
+
tenantId: config.get('AZURE_AD_TENANT_ID'),
|
|
396
|
+
// Without ?., a missing variable fails here instead of in the provider's boot check.
|
|
397
|
+
audience: config.get('AZURE_AD_AUDIENCE')?.split(','),
|
|
398
|
+
createOnSignIn: true,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
| Option | Required | Meaning |
|
|
405
|
+
| --- | --- | --- |
|
|
406
|
+
| `name` | yes | The provider name; unique in the app |
|
|
407
|
+
| `tenantId` | yes | The GUID of the tenant. It gives the issuers and the URL of the signing keys. `common` and `organizations` name no single tenant and stop the app at boot |
|
|
408
|
+
| `audience` | yes | The application ID URI or client id every token carries, or a list of them. Each is trimmed |
|
|
409
|
+
| `linkByEmail` | no, off | Leave it off; see below |
|
|
410
|
+
| `createOnSignIn` | no, off | As for every provider |
|
|
411
|
+
| `acceptIdTokens` | no, off | Also accept an ID token of the tenant; see [ID tokens](#id-tokens) |
|
|
412
|
+
|
|
413
|
+
**One provider per tenant.** Both of the tenant's issuers belong to the
|
|
414
|
+
provider, and two providers may not list one issuer, so a second provider for
|
|
415
|
+
the same tenant stops the app at boot. Several app registrations of the tenant
|
|
416
|
+
share one provider through a list of audiences.
|
|
417
|
+
|
|
418
|
+
The provider accepts both of Entra's token versions: a v2.0 token, issued by
|
|
419
|
+
`login.microsoftonline.com/<tenant>/v2.0`, and a v1.0 token, issued by
|
|
420
|
+
`sts.windows.net/<tenant>/`. Which one Entra issues depends on the app
|
|
421
|
+
registration; nothing there has to change. It verifies each token against the
|
|
422
|
+
tenant's signing keys (RS256, one of those issuers, one of the audiences) and
|
|
423
|
+
caches the keys, as the Zitadel provider does, with the same times. It reads the tenant's keys: an
|
|
424
|
+
app registration with a claims-mapping policy gets its tokens signed with a
|
|
425
|
+
key of its own, which those keys do not list, so every token of it is refused
|
|
426
|
+
as not valid.
|
|
427
|
+
|
|
428
|
+
- **`Identity.id` is the `oid` claim**, the identity's object id. `sub` differs
|
|
429
|
+
per app registration, so a store that keeps the `sub` finds no user once
|
|
430
|
+
another app registration signs the same person in.
|
|
431
|
+
- **A token without `scp` is refused** with `INVALID_TOKEN` (401), unless it is
|
|
432
|
+
an ID token and `acceptIdTokens` is on. Only an access token issued to an
|
|
433
|
+
identity carries scopes; an app's own token, from the client credentials
|
|
434
|
+
grant, has an `oid` and may carry app roles, but belongs to no identity. An
|
|
435
|
+
app that calls the API with such a token is refused.
|
|
436
|
+
- **The email is the `email` claim**, which Entra adds only when the app
|
|
437
|
+
registration asks for it as an optional claim; without it the email is null.
|
|
438
|
+
The names are `given_name` and `family_name`: a v1.0 access token carries
|
|
439
|
+
them, a v2.0 one only as optional claims. For an access token, those
|
|
440
|
+
optional claims are set on the app registration of the API, not of the
|
|
441
|
+
frontend that asks for the token.
|
|
442
|
+
`preferred_username` and `upn` look like an email but need not be one and
|
|
443
|
+
can change, so the provider does not read them. `emailVerified` is always
|
|
444
|
+
false: Entra's tokens do not say.
|
|
445
|
+
- **Leave `linkByEmail` off.** The `email` claim is the identity's mail
|
|
446
|
+
attribute, which an admin sets and Entra does not verify; on a guest it can
|
|
447
|
+
be any address. A store that links by email would hand the user with that
|
|
448
|
+
address to whoever carries it. Turn it on only with a store that trusts the
|
|
449
|
+
tenant's mail attributes, and expect `emailVerified` to be false.
|
|
450
|
+
- **The roles are the app roles** in the `roles` claim.
|
|
451
|
+
- **A first sign-in needs no call to Entra**: the token carries all the
|
|
452
|
+
provider knows, so `readProfile` returns the identity as it is.
|
|
453
|
+
|
|
454
|
+
### ID tokens
|
|
455
|
+
|
|
456
|
+
The proper setup is an access token for an API scope the app registration
|
|
457
|
+
exposes: the frontend asks for that scope, and the backend accepts what Entra
|
|
458
|
+
issued for it. Some frontends send their **ID token** as the bearer instead,
|
|
459
|
+
because they only ask for `openid profile email` and their access token is
|
|
460
|
+
one for Microsoft Graph. `acceptIdTokens: true` lets such a frontend keep
|
|
461
|
+
working while it moves to an API scope. An ID token's audience is the
|
|
462
|
+
frontend's client id, so `audience` lists that client id.
|
|
463
|
+
|
|
464
|
+
With the option on, a token without `scp` passes as an ID token only when both
|
|
465
|
+
hold:
|
|
466
|
+
|
|
467
|
+
- **It names no client.** Every Entra access token names the client it was
|
|
468
|
+
issued to, `azp` in a v2.0 token and `appid` in a v1.0 token, an app's own
|
|
469
|
+
token from the client credentials grant included. An ID token carries
|
|
470
|
+
neither. A token with `idtyp: "app"` is refused as well.
|
|
471
|
+
- **It names a person**, in `name`, `preferred_username` or `unique_name`. An
|
|
472
|
+
app's own token has none of them. An ID token with an `oid` does: Entra adds
|
|
473
|
+
the `oid` only with the `profile` scope, which brings them.
|
|
474
|
+
|
|
475
|
+
Signature, issuer, audience, lifetime and `oid` are checked as for an access
|
|
476
|
+
token, and the identity is read the same way. Delegated access tokens, with
|
|
477
|
+
`scp`, are still accepted.
|
|
478
|
+
|
|
479
|
+
What the option does not protect against:
|
|
480
|
+
|
|
481
|
+
- **An ID token is meant for the frontend, not for an API.** Anyone who gets
|
|
482
|
+
hold of a user's ID token for that client, from the browser's storage or a
|
|
483
|
+
log line, can call the API with it until it expires, usually after an hour.
|
|
484
|
+
It cannot be narrowed to this API, and Entra does not end it early when the
|
|
485
|
+
user signs out or loses access.
|
|
486
|
+
- **The rule rests on claims as Microsoft documents them today**, which it
|
|
487
|
+
does not promise to keep. Ask Entra for the `idtyp` optional claim on the
|
|
488
|
+
access tokens of the API's app registration: an app's own token then says
|
|
489
|
+
so explicitly.
|
|
490
|
+
|
|
491
|
+
Turn it off once the frontend sends an access token for the API's scope.
|
|
492
|
+
|
|
493
|
+
### Managing Entra users
|
|
494
|
+
|
|
495
|
+
The management calls are not available yet. `findByEmail` and `create` throw
|
|
496
|
+
`NOT_CONFIGURED`, which an app that provisions users catches to skip
|
|
497
|
+
provisioning. `createAccountSetupLink` and `isFederated` throw `NOT_SUPPORTED`
|
|
498
|
+
(501): the tenant's IT hands employees their password, and an Entra identity never
|
|
499
|
+
signs in elsewhere.
|
|
500
|
+
|
|
501
|
+
## Testing an app
|
|
502
|
+
|
|
503
|
+
`./testing` gives an app what it needs to test sign-in without an identity
|
|
504
|
+
provider:
|
|
505
|
+
|
|
506
|
+
```ts
|
|
507
|
+
const provider = new TestIdentityProvider({ name: 'zitadel', issuers: ['https://test-issuer'] });
|
|
508
|
+
|
|
509
|
+
Test.createTestingModule({ imports: [AppModule] })
|
|
510
|
+
.overrideProvider(ComplianceZitadel)
|
|
511
|
+
.useValue(provider);
|
|
512
|
+
|
|
513
|
+
const token = provider.createToken({ id: 'zitadel-1', roles: ['ADMIN'] });
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
Run the store contract against the app's own store, so a store that breaks a
|
|
517
|
+
rule the types cannot express fails in the app's CI:
|
|
518
|
+
|
|
519
|
+
```ts
|
|
520
|
+
for (const check of userStoreContract(() => ({ store, providers: ['zitadel'], createUser, userId: (user) => user.id }))) {
|
|
521
|
+
it(check.name, check.run);
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
## Good to know
|
|
526
|
+
|
|
527
|
+
- **A change at the provider takes up to `signInCacheSeconds` to arrive.** The
|
|
528
|
+
guard verifies the token on every request, but for an identity it saw
|
|
529
|
+
recently it answers from its cache, without the store and without `sync()`.
|
|
530
|
+
A revoked role therefore stays on `request.user` until the entry expires: 60
|
|
531
|
+
seconds by default. Set `signInCacheSeconds: 0` to apply such a change on the
|
|
532
|
+
next request, at the cost of a store call on every request.
|
|
533
|
+
- **A refused sign-in stays refused for up to `signInCacheSeconds`.** The
|
|
534
|
+
guard remembers a `USER_NOT_LINKED` refusal too. `provision()` drops it when
|
|
535
|
+
it links the identity. A user the app links by other means is still refused
|
|
536
|
+
until the entry expires, unless `signInCacheSeconds` is 0, and so is a
|
|
537
|
+
sign-in that arrives between `provision()` and the commit of the app's
|
|
538
|
+
transaction; calling `provision()` again after the commit clears it.
|
|
539
|
+
- **`link()` keeps one identity to one user.** The library cannot tell two
|
|
540
|
+
user objects apart, so the store compares users by primary key and checks
|
|
541
|
+
within the write. On PostgreSQL, a conditional update does both at once,
|
|
542
|
+
with a unique index on the identity column behind it. The example runs it
|
|
543
|
+
in a nested `transaction()`, which TypeORM 0.3.22 turns into a savepoint
|
|
544
|
+
inside the app's transaction; a version without nested transactions throws
|
|
545
|
+
`TransactionAlreadyStartedError` there and needs a savepoint of its own
|
|
546
|
+
through the query runner:
|
|
547
|
+
|
|
548
|
+
```ts
|
|
549
|
+
async link(user: User, identityId: string, provider: string, transaction?: EntityManager): Promise<boolean> {
|
|
550
|
+
try {
|
|
551
|
+
// Within the app's transaction this is a savepoint, so a unique violation undoes this update alone.
|
|
552
|
+
return await (transaction ?? this.manager).transaction(async (manager) => {
|
|
553
|
+
const result = await manager
|
|
554
|
+
.createQueryBuilder()
|
|
555
|
+
.update(User)
|
|
556
|
+
.set({ zitadelId: identityId })
|
|
557
|
+
.where('id = :userId', { userId: user.id })
|
|
558
|
+
.andWhere('("zitadelId" IS NULL OR "zitadelId" = :identityId)', { identityId })
|
|
559
|
+
.andWhere('NOT EXISTS (SELECT 1 FROM "user" other WHERE other."zitadelId" = :identityId AND other.id <> :userId)')
|
|
560
|
+
.execute();
|
|
561
|
+
|
|
562
|
+
return result.affected === 1;
|
|
563
|
+
});
|
|
564
|
+
} catch (error) {
|
|
565
|
+
// The identity column's own unique index: another transaction linked the identity at the same moment.
|
|
566
|
+
if (error instanceof QueryFailedError && error.driverError?.constraint === 'UQ_user_zitadelId') {
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
throw error;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
The unique index covers what the `NOT EXISTS` cannot: two transactions open
|
|
575
|
+
at the same time do not see each other's writes, so both could pass it.
|
|
576
|
+
Catching its violation turns it into a refusal rather than the database's
|
|
577
|
+
own error. Match the index by its name, so a violation of another unique
|
|
578
|
+
index on the table still surfaces as the data error it is; name it
|
|
579
|
+
yourself, such as `@Index('UQ_user_zitadelId', ['zitadelId'], { unique: true })`,
|
|
580
|
+
since TypeORM otherwise names it after a hash.
|
|
581
|
+
|
|
582
|
+
The savepoint keeps the app's transaction usable after a violation. Without
|
|
583
|
+
one, PostgreSQL refuses every further query of that transaction, so
|
|
584
|
+
`provision()`'s read of the owner fails, and `provision()` throws that
|
|
585
|
+
database error instead of `IDENTITY_ALREADY_LINKED`. A user object the app
|
|
586
|
+
loaded before another call linked that user links fine, since the update
|
|
587
|
+
checks the row, not the object.
|
|
588
|
+
|
|
589
|
+
MySQL refuses a subquery on the table an update writes, so a MySQL store
|
|
590
|
+
leaves out the `NOT EXISTS`, relies on the unique index alone and catches
|
|
591
|
+
its `ER_DUP_ENTRY` (errno 1062) instead of `23505`. The quoted table and
|
|
592
|
+
column names are the database's; match them to the entity's table and the
|
|
593
|
+
naming strategy.
|
|
594
|
+
- **Provision one user one call at a time.** Two `provision()` calls for the
|
|
595
|
+
same user that run at once can both miss the identity at the provider and
|
|
596
|
+
each create one. The first links its identity; the second gets
|
|
597
|
+
`LINK_REFUSED`, and its identity stays at the provider, linked to no one.
|
|
598
|
+
Run provisioning for a user from one place, such as the job that invites
|
|
599
|
+
them.
|
|
600
|
+
- **`create()` must be safe to run twice for one identity.** Within one app
|
|
601
|
+
instance the guard runs one sign-in per identity at a time, so parallel
|
|
602
|
+
requests of a new user wait for a single `create()`. Two app instances can
|
|
603
|
+
still each sign the same new identity in. Give the identity column a unique
|
|
604
|
+
index and return the existing user on a conflict; the store contract checks
|
|
605
|
+
it.
|
|
606
|
+
- **Treat `request.user` as read-only.** While an identity is cached, all its
|
|
607
|
+
requests share one `request.user` object. Keep per-request data elsewhere on
|
|
608
|
+
the request.
|
|
609
|
+
- **A guard injects its providers by the class the app registered.** It
|
|
610
|
+
accepts exactly the instances it passes to `super()`, so inject
|
|
611
|
+
`ComplianceZitadel`, the class listed in `forRoot()`, not a base class such as
|
|
612
|
+
`ZitadelProvider`, which Nest cannot inject. A test that replaces
|
|
613
|
+
`ComplianceZitadel` with `overrideProvider` replaces it in the guard too.
|
|
614
|
+
- **A provider tells an unreachable provider apart from a bad token.** The
|
|
615
|
+
guard passes an exception `identityConnectorException` made from
|
|
616
|
+
`authenticate()` on unchanged, so a provider throws
|
|
617
|
+
`identityConnectorException(PROVIDER_UNREACHABLE)` (503) when it cannot
|
|
618
|
+
reach its identity provider. Anything else `authenticate()` throws, another
|
|
619
|
+
exception of exception-types included, is a token that is not valid (401),
|
|
620
|
+
so a bad token never reaches the client as a 500 or a 404. This matters only
|
|
621
|
+
to an app that writes its own provider.
|
|
622
|
+
- **The guard links by email only an identity that has an email.** Whether the
|
|
623
|
+
email must be verified is the store's decision, in `linkByEmail()`.
|
|
624
|
+
- **A store implements what its guard's providers allow.** `linkByEmail()` and
|
|
625
|
+
`create()` return null unless the store overrides them, which refuses the
|
|
626
|
+
sign-in. When a provider the guard accepts has `linkByEmail` or
|
|
627
|
+
`createOnSignIn` and the store leaves that method out, the app stops at boot
|
|
628
|
+
with a message that names both. A guard that writes its own `onModuleInit`
|
|
629
|
+
calls `super.onModuleInit()`, or the check does not run.
|
|
630
|
+
- **The guard guards HTTP routes.** It reads the `Authorization` header of an
|
|
631
|
+
HTTP request.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare enum IdentityConnectorExceptionTypeEnum {
|
|
2
|
+
NO_TOKEN = "NO_TOKEN",
|
|
3
|
+
UNKNOWN_ISSUER = "UNKNOWN_ISSUER",
|
|
4
|
+
PROVIDER_NOT_ACCEPTED = "PROVIDER_NOT_ACCEPTED",
|
|
5
|
+
INVALID_TOKEN = "INVALID_TOKEN",
|
|
6
|
+
PROVIDER_UNREACHABLE = "PROVIDER_UNREACHABLE",
|
|
7
|
+
USER_NOT_LINKED = "USER_NOT_LINKED",
|
|
8
|
+
USER_NOT_PROVISIONED = "USER_NOT_PROVISIONED",
|
|
9
|
+
IDENTITY_ALREADY_LINKED = "IDENTITY_ALREADY_LINKED",
|
|
10
|
+
LINK_REFUSED = "LINK_REFUSED",
|
|
11
|
+
NO_EMAIL = "NO_EMAIL",
|
|
12
|
+
NOT_SUPPORTED = "NOT_SUPPORTED",
|
|
13
|
+
NOT_CONFIGURED = "NOT_CONFIGURED"
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IdentityConnectorExceptionTypeEnum = void 0;
|
|
4
|
+
var IdentityConnectorExceptionTypeEnum;
|
|
5
|
+
(function (IdentityConnectorExceptionTypeEnum) {
|
|
6
|
+
IdentityConnectorExceptionTypeEnum["NO_TOKEN"] = "NO_TOKEN";
|
|
7
|
+
IdentityConnectorExceptionTypeEnum["UNKNOWN_ISSUER"] = "UNKNOWN_ISSUER";
|
|
8
|
+
IdentityConnectorExceptionTypeEnum["PROVIDER_NOT_ACCEPTED"] = "PROVIDER_NOT_ACCEPTED";
|
|
9
|
+
IdentityConnectorExceptionTypeEnum["INVALID_TOKEN"] = "INVALID_TOKEN";
|
|
10
|
+
IdentityConnectorExceptionTypeEnum["PROVIDER_UNREACHABLE"] = "PROVIDER_UNREACHABLE";
|
|
11
|
+
IdentityConnectorExceptionTypeEnum["USER_NOT_LINKED"] = "USER_NOT_LINKED";
|
|
12
|
+
IdentityConnectorExceptionTypeEnum["USER_NOT_PROVISIONED"] = "USER_NOT_PROVISIONED";
|
|
13
|
+
IdentityConnectorExceptionTypeEnum["IDENTITY_ALREADY_LINKED"] = "IDENTITY_ALREADY_LINKED";
|
|
14
|
+
IdentityConnectorExceptionTypeEnum["LINK_REFUSED"] = "LINK_REFUSED";
|
|
15
|
+
IdentityConnectorExceptionTypeEnum["NO_EMAIL"] = "NO_EMAIL";
|
|
16
|
+
IdentityConnectorExceptionTypeEnum["NOT_SUPPORTED"] = "NOT_SUPPORTED";
|
|
17
|
+
IdentityConnectorExceptionTypeEnum["NOT_CONFIGURED"] = "NOT_CONFIGURED";
|
|
18
|
+
})(IdentityConnectorExceptionTypeEnum || (exports.IdentityConnectorExceptionTypeEnum = IdentityConnectorExceptionTypeEnum = {}));
|