@tstdl/base 0.93.80 → 0.93.82

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.
Files changed (28) hide show
  1. package/api/server/gateway.js +1 -1
  2. package/authentication/authentication.api.d.ts +9 -0
  3. package/authentication/authentication.api.js +3 -0
  4. package/authentication/client/authentication.service.d.ts +23 -15
  5. package/authentication/client/authentication.service.js +30 -21
  6. package/authentication/index.d.ts +1 -0
  7. package/authentication/index.js +1 -0
  8. package/authentication/models/authentication-credentials.model.d.ts +2 -2
  9. package/authentication/models/authentication-credentials.model.js +5 -5
  10. package/authentication/models/authentication-session.model.d.ts +2 -2
  11. package/authentication/models/authentication-session.model.js +3 -3
  12. package/authentication/models/subject.model.js +5 -3
  13. package/authentication/models/token-payload-base.model.d.ts +5 -1
  14. package/authentication/models/token-payload-base.model.js +10 -2
  15. package/authentication/models/token.model.d.ts +9 -1
  16. package/authentication/server/authentication-ancillary.service.d.ts +12 -15
  17. package/authentication/server/authentication-ancillary.service.js +3 -0
  18. package/authentication/server/authentication.api-controller.js +5 -5
  19. package/authentication/server/authentication.audit.d.ts +7 -5
  20. package/authentication/server/authentication.service.d.ts +32 -22
  21. package/authentication/server/authentication.service.js +116 -65
  22. package/authentication/server/subject.service.d.ts +19 -3
  23. package/authentication/server/subject.service.js +126 -9
  24. package/authentication/types.d.ts +6 -0
  25. package/authentication/types.js +1 -0
  26. package/examples/api/authentication.js +3 -2
  27. package/examples/api/custom-authentication.js +11 -9
  28. package/package.json +1 -1
@@ -1,7 +1,8 @@
1
1
  import { Auditor } from '../../audit/index.js';
2
- import { type AfterResolve, afterResolve } from '../../injector/index.js';
2
+ import { afterResolve, type AfterResolve } from '../../injector/index.js';
3
3
  import type { BinaryData, Record } from '../../types/index.js';
4
- import { type RefreshToken, type SecretCheckResult, type SecretResetToken, type Token } from '../models/index.js';
4
+ import { Subject, type RefreshToken, type SecretCheckResult, type SecretResetToken, type Token } from '../models/index.js';
5
+ import type { SubjectInput } from '../types.js';
5
6
  import { type SecretTestResult } from './authentication-secret-requirements.validator.js';
6
7
  /**
7
8
  * Data for creating a token.
@@ -19,8 +20,8 @@ export type CreateTokenData<AdditionalTokenPayload extends Record> = {
19
20
  expiration?: number;
20
21
  /** Additional token payload */
21
22
  additionalTokenPayload: AdditionalTokenPayload;
22
- /** Subject of the token */
23
- subject: string;
23
+ /** Subject */
24
+ subject: Subject;
24
25
  /** Session id */
25
26
  sessionId: string;
26
27
  /** Impersonator subject */
@@ -70,7 +71,7 @@ export declare class AuthenticationServiceOptions {
70
71
  */
71
72
  export type AuthenticationResult = {
72
73
  success: true;
73
- subject: string;
74
+ subject: Subject;
74
75
  } | {
75
76
  success: false;
76
77
  subject?: undefined;
@@ -133,16 +134,16 @@ export declare class AuthenticationService<AdditionalTokenPayload extends Record
133
134
  #private;
134
135
  readonly hooks: {
135
136
  beforeLogin: import("../../utils/async-hook/async-hook.js").AsyncHook<{
136
- subject: string;
137
+ subject: Subject;
137
138
  }, never, unknown>;
138
139
  afterLogin: import("../../utils/async-hook/async-hook.js").AsyncHook<{
139
- subject: string;
140
+ subject: Subject;
140
141
  }, never, unknown>;
141
142
  beforeChangeSecret: import("../../utils/async-hook/async-hook.js").AsyncHook<{
142
- subject: string;
143
+ subject: Subject;
143
144
  }, never, unknown>;
144
145
  afterChangeSecret: import("../../utils/async-hook/async-hook.js").AsyncHook<{
145
- subject: string;
146
+ subject: Subject;
146
147
  }, never, unknown>;
147
148
  };
148
149
  private readonly tokenVersion;
@@ -168,14 +169,14 @@ export declare class AuthenticationService<AdditionalTokenPayload extends Record
168
169
  * @param secret The secret to set.
169
170
  * @param options Options for setting the credentials.
170
171
  */
171
- setCredentials(subject: string, secret: string, options?: SetCredentialsOptions): Promise<void>;
172
+ setCredentials(subject: Subject, secret: string, options?: SetCredentialsOptions): Promise<void>;
172
173
  /**
173
174
  * Authenticates a subject with a secret.
174
175
  * @param subject The subject to authenticate.
175
176
  * @param secret The secret to authenticate with.
176
177
  * @returns The result of the authentication.
177
178
  */
178
- authenticate(subject: string, secret: string): Promise<AuthenticationResult>;
179
+ authenticate(subject: SubjectInput, secret: string): Promise<AuthenticationResult>;
179
180
  /**
180
181
  * Gets a token for a subject.
181
182
  * @param subject The subject to get the token for.
@@ -183,18 +184,18 @@ export declare class AuthenticationService<AdditionalTokenPayload extends Record
183
184
  * @param options Options for getting the token.
184
185
  * @returns The token result.
185
186
  */
186
- getToken(subject: string, authenticationData: AuthenticationData, { impersonator }?: {
187
+ getToken(subject: Subject, authenticationData: AuthenticationData, { impersonator }?: {
187
188
  impersonator?: string;
188
189
  }): Promise<TokenResult<AdditionalTokenPayload>>;
189
190
  /**
190
191
  * Logs in a subject.
191
- * @param subject The subject to log in.
192
+ * @param subjectInput The subject to log in.
192
193
  * @param secret The secret to log in with.
193
194
  * @param data Additional authentication data.
194
195
  * @param auditor Auditor for auditing.
195
196
  * @returns Token
196
197
  */
197
- login(subject: string, secret: string, data: AuthenticationData, auditor: Auditor): Promise<TokenResult<AdditionalTokenPayload>>;
198
+ login(subjectInput: SubjectInput, secret: string, data: AuthenticationData, auditor: Auditor): Promise<TokenResult<AdditionalTokenPayload>>;
198
199
  /**
199
200
  * Ends a session.
200
201
  * @param sessionId The id of the session to end.
@@ -223,7 +224,7 @@ export declare class AuthenticationService<AdditionalTokenPayload extends Record
223
224
  * @returns The token result.
224
225
  * @throws {ForbiddenError} If impersonation is not allowed.
225
226
  */
226
- impersonate(impersonatorToken: string, impersonatorRefreshToken: string, subject: string, authenticationData: AuthenticationData, auditor: Auditor): Promise<TokenResult<AdditionalTokenPayload>>;
227
+ impersonate(impersonatorToken: string, impersonatorRefreshToken: string, subjectId: string, authenticationData: AuthenticationData, auditor: Auditor): Promise<TokenResult<AdditionalTokenPayload>>;
227
228
  /**
228
229
  * Unimpersonates a subject.
229
230
  * @param impersonatorRefreshToken The refresh token of the impersonator.
@@ -239,15 +240,15 @@ export declare class AuthenticationService<AdditionalTokenPayload extends Record
239
240
  * @param auditor Auditor for auditing.
240
241
  * @throws {NotImplementedError} If no ancillary service is registered.
241
242
  */
242
- initSecretReset(subject: string, data: AdditionalInitSecretResetData, auditor: Auditor): Promise<void>;
243
+ initSecretReset(subject: SubjectInput, data: AdditionalInitSecretResetData, auditor: Auditor): Promise<void>;
243
244
  /**
244
245
  * Changes a subject's secret.
245
- * @param subject The subject to change the secret for.
246
+ * @param subjectInput The subject to change the secret for.
246
247
  * @param currentSecret The current secret.
247
248
  * @param newSecret The new secret.
248
249
  * @param auditor Auditor for auditing.
249
250
  */
250
- changeSecret(subject: string, currentSecret: string, newSecret: string, auditor: Auditor): Promise<void>;
251
+ changeSecret(subjectInput: SubjectInput, currentSecret: string, newSecret: string, auditor: Auditor): Promise<void>;
251
252
  /**
252
253
  * Resets a secret.
253
254
  * @param tokenString The secret reset token.
@@ -297,11 +298,11 @@ export declare class AuthenticationService<AdditionalTokenPayload extends Record
297
298
  validateSecretResetToken(token: string): Promise<SecretResetToken>;
298
299
  /**
299
300
  * Tries to resolve a subject.
300
- * This method is safe to use in public facing APIs as it does not leak information about the existence of a subject.
301
301
  * @param subject The subject to resolve.
302
302
  * @returns The resolved subject or undefined if the subject could not be resolved.
303
303
  */
304
- tryResolveSubject(subject: string): Promise<string | undefined>;
304
+ tryResolveSubject(subject: SubjectInput): Promise<Subject | undefined>;
305
+ resolveSubjects(subjectInput: SubjectInput): Promise<Subject[]>;
305
306
  /**
306
307
  * Resolves the subject to the actual subject used for authentication.
307
308
  * This should *not* be used for public facing APIs, as it throws an error if the subject is not found that leaks if the subjects exists or not.
@@ -309,7 +310,7 @@ export declare class AuthenticationService<AdditionalTokenPayload extends Record
309
310
  * @param subject The subject to resolve.
310
311
  * @returns The resolved subject or the original subject if not found.
311
312
  */
312
- resolveSubject(subject: string): Promise<string>;
313
+ resolveSubject(subject: SubjectInput): Promise<Subject>;
313
314
  /**
314
315
  * Creates a token without session or refresh token and is not saved in database.
315
316
  * @param data Data for creating the token.
@@ -319,14 +320,23 @@ export declare class AuthenticationService<AdditionalTokenPayload extends Record
319
320
  /**
320
321
  * Creates a refresh token without session and is not saved in database.
321
322
  * @param subject The subject of the refresh token.
323
+ * @param tenantId The tenant id of the refresh token.
322
324
  * @param sessionId The session id of the refresh token.
323
325
  * @param expirationTimestamp The expiration timestamp of the refresh token.
324
326
  * @param options Options for creating the refresh token.
325
327
  * @returns The created refresh token.
326
328
  */
327
- createRefreshToken(subject: string, sessionId: string, expirationTimestamp: number, options?: {
329
+ createRefreshToken(subject: Subject, sessionId: string, expirationTimestamp: number, options?: {
328
330
  impersonator?: string;
329
331
  }): Promise<CreateRefreshTokenResult>;
332
+ defaultResolveSubjects({ tenantId, subject }: {
333
+ tenantId?: string;
334
+ subject: string;
335
+ }): Promise<Subject[]>;
336
+ defaultResolveSubject({ tenantId, subject }: {
337
+ tenantId?: string;
338
+ subject: string;
339
+ }): Promise<Subject>;
330
340
  private createSecretResetToken;
331
341
  private deriveSigningSecrets;
332
342
  private getHash;
@@ -16,6 +16,7 @@ import { afterResolve, inject, provide, Singleton } from '../../injector/index.j
16
16
  import { KeyValueStore } from '../../key-value-store/key-value.store.js';
17
17
  import { Logger } from '../../logger/logger.js';
18
18
  import { DatabaseConfig, injectRepository } from '../../orm/server/index.js';
19
+ import { getEntityIds } from '../../orm/utils.js';
19
20
  import { Alphabet } from '../../utils/alphabet.js';
20
21
  import { asyncHook } from '../../utils/async-hook/async-hook.js';
21
22
  import { decodeBase64, encodeBase64 } from '../../utils/base64.js';
@@ -24,9 +25,9 @@ import { currentTimestamp, timestampToTimestampSeconds } from '../../utils/date-
24
25
  import { timingSafeBinaryEquals } from '../../utils/equals.js';
25
26
  import { createJwtTokenString } from '../../utils/jwt.js';
26
27
  import { getRandomBytes, getRandomString } from '../../utils/random.js';
27
- import { isBinaryData, isString, isUndefined } from '../../utils/type-guards.js';
28
+ import { assertDefinedPass, isBinaryData, isString, isUndefined } from '../../utils/type-guards.js';
28
29
  import { millisecondsPerDay, millisecondsPerMinute } from '../../utils/units.js';
29
- import { AuthenticationCredentials, AuthenticationSession } from '../models/index.js';
30
+ import { AuthenticationCredentials, AuthenticationSession, Subject, User } from '../models/index.js';
30
31
  import { AuthenticationAncillaryService, GetTokenPayloadContextAction } from './authentication-ancillary.service.js';
31
32
  import { AuthenticationSecretRequirementsValidator } from './authentication-secret-requirements.validator.js';
32
33
  import { getRefreshTokenFromString, getSecretResetTokenFromString, getTokenFromString } from './helper.js';
@@ -83,6 +84,8 @@ const SIGNING_SECRETS_LENGTH = 64;
83
84
  let AuthenticationService = AuthenticationService_1 = class AuthenticationService {
84
85
  #credentialsRepository = injectRepository(AuthenticationCredentials);
85
86
  #sessionRepository = injectRepository(AuthenticationSession);
87
+ #subjectRepository = injectRepository(Subject);
88
+ #userRepository = injectRepository(User);
86
89
  #authenticationSecretRequirementsValidator = inject(AuthenticationSecretRequirementsValidator);
87
90
  #authenticationAncillaryService = inject(AuthenticationAncillaryService, undefined, { optional: true });
88
91
  #keyValueStore = inject((KeyValueStore), 'authentication');
@@ -130,7 +133,6 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
130
133
  */
131
134
  async setCredentials(subject, secret, options) {
132
135
  // We do not need to avoid information leakage here, as this is a non-public method that is only called by a public api if the secret reset token is valid.
133
- const actualSubject = await this.resolveSubject(subject);
134
136
  if (options?.skipValidation != true) {
135
137
  await this.#authenticationSecretRequirementsValidator.validateSecretRequirements(secret);
136
138
  }
@@ -138,13 +140,14 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
138
140
  const hash = await this.getHash(secret, salt);
139
141
  await this.#credentialsRepository.transaction(async (tx) => {
140
142
  await this.#credentialsRepository.withTransaction(tx).upsert('subject', {
141
- subject: actualSubject,
143
+ tenantId: subject.tenantId,
144
+ subject: subject.id,
142
145
  hashVersion: 1,
143
146
  salt,
144
147
  hash,
145
148
  });
146
149
  if (options?.skipSessionInvalidation != true) {
147
- await this.#sessionRepository.withTransaction(tx).updateManyByQuery({ subject: actualSubject }, { end: currentTimestamp() });
150
+ await this.#sessionRepository.withTransaction(tx).updateManyByQuery({ tenantId: subject.tenantId, subject: subject.id }, { end: currentTimestamp() });
148
151
  }
149
152
  });
150
153
  }
@@ -155,16 +158,16 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
155
158
  * @returns The result of the authentication.
156
159
  */
157
160
  async authenticate(subject, secret) {
158
- const actualSubject = await this.tryResolveSubject(subject) ?? subject;
159
- // Always try to load credentials, even if the subject is not resolved, to avoid information leakage.
161
+ const actualSubject = await this.tryResolveSubject(subject);
162
+ // Always try to load credentials, even if the subject is not resolved, to reduce information leakage.
160
163
  // If the subject is not resolved, we will create a new credentials entry with an empty salt and hash.
161
164
  // This way, we do not leak if the subject exists or not via timing attacks.
162
- const credentials = await this.#credentialsRepository.tryLoadByQuery({ subject: actualSubject })
165
+ const credentials = await this.#credentialsRepository.tryLoadByQuery({ tenantId: actualSubject?.tenantId ?? NIL_UUID, subject: actualSubject?.id ?? NIL_UUID })
163
166
  ?? { subject: actualSubject, salt: new Uint8Array(), hash: new Uint8Array() };
164
167
  const hash = await this.getHash(secret, credentials.salt);
165
168
  const valid = timingSafeBinaryEquals(hash, credentials.hash);
166
169
  if (valid) {
167
- return { success: true, subject: credentials.subject };
170
+ return { success: true, subject: assertDefinedPass(actualSubject, 'Subject should be defined if authentication is valid but it is not.') };
168
171
  }
169
172
  return { success: false };
170
173
  }
@@ -176,21 +179,21 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
176
179
  * @returns The token result.
177
180
  */
178
181
  async getToken(subject, authenticationData, { impersonator } = {}) {
179
- const actualSubject = await this.resolveSubject(subject);
180
182
  const now = currentTimestamp();
181
183
  const end = now + this.refreshTokenTimeToLive;
182
184
  return await this.#sessionRepository.transaction(async (tx) => {
183
185
  const session = await this.#sessionRepository.withTransaction(tx).insert({
184
- subject: actualSubject,
186
+ tenantId: subject.tenantId,
187
+ subject: subject.id,
185
188
  begin: now,
186
189
  end,
187
190
  refreshTokenHashVersion: 0,
188
191
  refreshTokenSalt: new Uint8Array(),
189
192
  refreshTokenHash: new Uint8Array(),
190
193
  });
191
- const tokenPayload = await this.#authenticationAncillaryService?.getTokenPayload(actualSubject, authenticationData, { action: GetTokenPayloadContextAction.GetToken });
192
- const { token, jsonToken } = await this.createToken({ additionalTokenPayload: tokenPayload, subject: actualSubject, impersonator, sessionId: session.id, refreshTokenExpiration: end, timestamp: now });
193
- const refreshToken = await this.createRefreshToken(actualSubject, session.id, end, { impersonator });
194
+ const tokenPayload = await this.#authenticationAncillaryService?.getTokenPayload(subject, authenticationData, { action: GetTokenPayloadContextAction.GetToken });
195
+ const { token, jsonToken } = await this.createToken({ additionalTokenPayload: tokenPayload, subject, impersonator, sessionId: session.id, refreshTokenExpiration: end, timestamp: now });
196
+ const refreshToken = await this.createRefreshToken(subject, session.id, end, { impersonator });
194
197
  await this.#sessionRepository.withTransaction(tx).update(session.id, {
195
198
  end,
196
199
  refreshTokenHashVersion: 1,
@@ -202,32 +205,34 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
202
205
  }
203
206
  /**
204
207
  * Logs in a subject.
205
- * @param subject The subject to log in.
208
+ * @param subjectInput The subject to log in.
206
209
  * @param secret The secret to log in with.
207
210
  * @param data Additional authentication data.
208
211
  * @param auditor Auditor for auditing.
209
212
  * @returns Token
210
213
  */
211
- async login(subject, secret, data, auditor) {
214
+ async login(subjectInput, secret, data, auditor) {
212
215
  const authAuditor = auditor.fork(AuthenticationService_1.name);
213
- const authenticationResult = await this.authenticate(subject, secret);
216
+ const authenticationResult = await this.authenticate(subjectInput, secret);
214
217
  if (!authenticationResult.success) {
215
- const actualSubject = await this.tryResolveSubject(subject) ?? subject;
218
+ const actualSubject = await this.tryResolveSubject(subjectInput);
216
219
  await authAuditor.warn('login-failure', {
217
- targetId: actualSubject,
220
+ tenantId: actualSubject?.tenantId ?? subjectInput.tenantId,
221
+ targetId: actualSubject?.id ?? NIL_UUID,
218
222
  targetType: 'User',
219
- details: { providedSubject: subject },
223
+ details: { subjectInput, subjectId: actualSubject?.id ?? null },
220
224
  });
221
225
  throw new InvalidCredentialsError();
222
226
  }
223
227
  await this.hooks.beforeLogin.trigger({ subject: authenticationResult.subject });
224
228
  const token = await this.getToken(authenticationResult.subject, data);
225
229
  await this.hooks.afterLogin.trigger({ subject: authenticationResult.subject });
226
- const sessionId = token.jsonToken.payload.sessionId;
230
+ const sessionId = token.jsonToken.payload.session;
227
231
  await authAuditor.info('login-success', {
228
- actor: authenticationResult.subject,
232
+ tenantId: authenticationResult.subject.tenantId,
233
+ actor: authenticationResult.subject.id,
229
234
  actorType: ActorType.User,
230
- targetId: authenticationResult.subject,
235
+ targetId: authenticationResult.subject.id,
231
236
  targetType: 'User',
232
237
  network: { sessionId },
233
238
  details: { sessionId },
@@ -249,6 +254,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
249
254
  const now = currentTimestamp();
250
255
  await this.#sessionRepository.update(sessionId, { end: now });
251
256
  await authAuditor.info('logout', {
257
+ tenantId: session.tenantId,
252
258
  actor: session.subject,
253
259
  actorType: ActorType.User,
254
260
  targetId: session.subject,
@@ -269,7 +275,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
269
275
  const authAuditor = auditor.fork(AuthenticationService_1.name);
270
276
  try {
271
277
  const validatedRefreshToken = await this.validateRefreshToken(refreshToken);
272
- const sessionId = validatedRefreshToken.payload.sessionId;
278
+ const sessionId = validatedRefreshToken.payload.session;
273
279
  const session = await this.#sessionRepository.load(sessionId);
274
280
  const hash = await this.getHash(validatedRefreshToken.payload.secret, session.refreshTokenSalt);
275
281
  if (session.end <= currentTimestamp()) {
@@ -281,9 +287,10 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
281
287
  const now = currentTimestamp();
282
288
  const impersonator = (options.omitImpersonator == true) ? undefined : validatedRefreshToken.payload.impersonator;
283
289
  const newEnd = now + this.refreshTokenTimeToLive;
284
- const tokenPayload = await this.#authenticationAncillaryService?.getTokenPayload(session.subject, authenticationData, { action: GetTokenPayloadContextAction.Refresh });
285
- const { token, jsonToken } = await this.createToken({ additionalTokenPayload: tokenPayload, subject: session.subject, sessionId, refreshTokenExpiration: newEnd, impersonator, timestamp: now });
286
- const newRefreshToken = await this.createRefreshToken(validatedRefreshToken.payload.subject, sessionId, newEnd, { impersonator });
290
+ const subject = await this.#subjectRepository.loadByQuery({ tenantId: session.tenantId, id: session.subject });
291
+ const tokenPayload = await this.#authenticationAncillaryService?.getTokenPayload(subject, authenticationData, { action: GetTokenPayloadContextAction.Refresh });
292
+ const { token, jsonToken } = await this.createToken({ additionalTokenPayload: tokenPayload, subject, sessionId, refreshTokenExpiration: newEnd, impersonator, timestamp: now });
293
+ const newRefreshToken = await this.createRefreshToken(subject, sessionId, newEnd, { impersonator });
287
294
  await this.#sessionRepository.update(sessionId, {
288
295
  end: newEnd,
289
296
  refreshTokenHashVersion: 1,
@@ -291,6 +298,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
291
298
  refreshTokenHash: newRefreshToken.hash,
292
299
  });
293
300
  await authAuditor.info('refresh-success', {
301
+ tenantId: session.tenantId,
294
302
  actor: session.subject,
295
303
  actorType: ActorType.User,
296
304
  targetId: session.subject,
@@ -318,32 +326,35 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
318
326
  * @returns The token result.
319
327
  * @throws {ForbiddenError} If impersonation is not allowed.
320
328
  */
321
- async impersonate(impersonatorToken, impersonatorRefreshToken, subject, authenticationData, auditor) {
329
+ async impersonate(impersonatorToken, impersonatorRefreshToken, subjectId, authenticationData, auditor) {
322
330
  const authAuditor = auditor.fork(AuthenticationService_1.name);
323
331
  const validatedImpersonatorToken = await this.validateToken(impersonatorToken);
324
332
  const validatedImpersonatorRefreshToken = await this.validateRefreshToken(impersonatorRefreshToken);
325
- const allowed = await this.#authenticationAncillaryService?.canImpersonate(validatedImpersonatorToken.payload, subject, authenticationData) ?? false;
333
+ const impersonatorSubject = await this.#subjectRepository.loadByQuery({ tenantId: validatedImpersonatorToken.payload.tenant, id: validatedImpersonatorToken.payload.subject });
334
+ const allowed = await this.#authenticationAncillaryService?.canImpersonate(validatedImpersonatorToken.payload, impersonatorSubject, authenticationData) ?? false;
326
335
  if (!allowed) {
327
- const impersonatedSubject = await this.tryResolveSubject(subject);
328
336
  await authAuditor.warn('impersonate-failure', {
329
337
  outcome: AuditOutcome.Denied,
330
- actor: validatedImpersonatorToken.payload.subject,
338
+ tenantId: impersonatorSubject.tenantId,
339
+ actor: impersonatorSubject.id,
331
340
  actorType: ActorType.User,
332
- targetId: impersonatedSubject ?? NIL_UUID,
341
+ targetId: subjectId,
333
342
  targetType: 'User',
334
- details: { impersonatedSubject: subject, reason: 'Impersonation forbidden.' },
343
+ details: { impersonatedSubjectId: subjectId },
335
344
  });
336
345
  throw new ForbiddenError('Impersonation forbidden.');
337
346
  }
347
+ const subject = await this.#subjectRepository.loadByQuery({ tenantId: validatedImpersonatorToken.payload.tenant, id: subjectId });
338
348
  const tokenResult = await this.getToken(subject, authenticationData, { impersonator: validatedImpersonatorToken.payload.subject });
339
349
  await authAuditor.info('impersonate-success', {
350
+ tenantId: subject.tenantId,
340
351
  actor: validatedImpersonatorToken.payload.subject,
341
352
  actorType: ActorType.User,
342
353
  impersonator: validatedImpersonatorToken.payload.subject,
343
354
  impersonatorType: ActorType.User,
344
355
  targetId: tokenResult.jsonToken.payload.subject,
345
356
  targetType: 'User',
346
- details: { impersonatedSubject: subject },
357
+ details: { impersonatedSubjectId: subjectId },
347
358
  });
348
359
  return {
349
360
  ...tokenResult,
@@ -362,9 +373,12 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
362
373
  const authAuditor = auditor.fork(AuthenticationService_1.name);
363
374
  const tokenResult = await this.refresh(impersonatorRefreshToken, authenticationData, { omitImpersonator: true }, auditor);
364
375
  await authAuditor.info('unimpersonate-success', {
376
+ tenantId: tokenResult.jsonToken.payload.tenant,
365
377
  actor: tokenResult.jsonToken.payload.subject,
366
378
  actorType: ActorType.User,
367
379
  targetId: tokenResult.jsonToken.payload.subject,
380
+ impersonatorType: ActorType.User,
381
+ impersonator: tokenResult.jsonToken.payload.impersonator,
368
382
  targetType: 'User',
369
383
  });
370
384
  return tokenResult;
@@ -383,7 +397,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
383
397
  const authAuditor = auditor.fork(AuthenticationService_1.name);
384
398
  const actualSubject = await this.tryResolveSubject(subject);
385
399
  if (isUndefined(actualSubject)) {
386
- this.#logger.warn(`Subject "${subject}" not found for secret reset.`);
400
+ this.#logger.warn(`Subject "${subject.subject}" not found for secret reset.`);
387
401
  /**
388
402
  * If the subject cannot be resolved, we do not throw an error here to avoid information leakage.
389
403
  * This is to prevent attackers from discovering valid subjects by trying to reset secrets.
@@ -393,40 +407,43 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
393
407
  }
394
408
  const secretResetToken = await this.createSecretResetToken(actualSubject, currentTimestamp() + this.secretResetTokenTimeToLive);
395
409
  const initSecretResetData = {
396
- subject: actualSubject,
410
+ subject: actualSubject.id,
397
411
  token: secretResetToken.token,
398
412
  ...data,
399
413
  };
400
414
  await this.#authenticationAncillaryService.handleInitSecretReset(initSecretResetData);
401
415
  await authAuditor.info('init-secret-reset', {
402
- targetId: actualSubject,
416
+ tenantId: actualSubject.tenantId,
417
+ targetId: actualSubject.id,
403
418
  targetType: 'User',
404
419
  });
405
420
  }
406
421
  /**
407
422
  * Changes a subject's secret.
408
- * @param subject The subject to change the secret for.
423
+ * @param subjectInput The subject to change the secret for.
409
424
  * @param currentSecret The current secret.
410
425
  * @param newSecret The new secret.
411
426
  * @param auditor Auditor for auditing.
412
427
  */
413
- async changeSecret(subject, currentSecret, newSecret, auditor) {
428
+ async changeSecret(subjectInput, currentSecret, newSecret, auditor) {
414
429
  const authAuditor = auditor.fork(AuthenticationService_1.name);
415
- const authenticationResult = await this.authenticate(subject, currentSecret);
430
+ const authenticationResult = await this.authenticate(subjectInput, currentSecret);
416
431
  if (!authenticationResult.success) {
417
- const resolvedSubject = await this.tryResolveSubject(subject);
432
+ const resolvedSubject = await this.tryResolveSubject(subjectInput);
418
433
  await authAuditor.warn('change-secret-failure', {
419
- targetId: resolvedSubject ?? NIL_UUID,
434
+ tenantId: resolvedSubject?.tenantId,
435
+ targetId: resolvedSubject?.id ?? NIL_UUID,
420
436
  targetType: 'User',
421
- details: { providedSubject: subject },
437
+ details: { subjectInput, subjectId: resolvedSubject?.id ?? null },
422
438
  });
423
439
  throw new InvalidCredentialsError();
424
440
  }
425
- await this.hooks.beforeChangeSecret.trigger({ subject });
426
- await this.setCredentials(subject, newSecret);
427
- await this.hooks.afterChangeSecret.trigger({ subject });
441
+ await this.hooks.beforeChangeSecret.trigger({ subject: authenticationResult.subject });
442
+ await this.setCredentials(authenticationResult.subject, newSecret);
443
+ await this.hooks.afterChangeSecret.trigger({ subject: authenticationResult.subject });
428
444
  await authAuditor.info('change-secret-success', {
429
- targetId: authenticationResult.subject,
445
+ tenantId: authenticationResult.subject.tenantId,
446
+ targetId: authenticationResult.subject.id,
430
447
  targetType: 'User',
431
448
  });
432
449
  }
@@ -441,8 +458,10 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
441
458
  const authAuditor = auditor.fork(AuthenticationService_1.name);
442
459
  try {
443
460
  const token = await this.validateSecretResetToken(tokenString);
444
- await this.setCredentials(token.payload.subject, newSecret);
461
+ const subject = await this.#subjectRepository.loadByQuery({ tenantId: token.payload.tenant, id: token.payload.subject });
462
+ await this.setCredentials(subject, newSecret);
445
463
  await authAuditor.info('reset-secret-success', {
464
+ tenantId: token.payload.tenant,
446
465
  targetId: token.payload.subject,
447
466
  targetType: 'User',
448
467
  });
@@ -509,19 +528,28 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
509
528
  }
510
529
  /**
511
530
  * Tries to resolve a subject.
512
- * This method is safe to use in public facing APIs as it does not leak information about the existence of a subject.
513
531
  * @param subject The subject to resolve.
514
532
  * @returns The resolved subject or undefined if the subject could not be resolved.
515
533
  */
516
534
  async tryResolveSubject(subject) {
517
535
  if (isUndefined(this.#authenticationAncillaryService)) {
518
- return subject;
536
+ const subjects = await this.defaultResolveSubjects(subject);
537
+ if (subjects.length > 1) {
538
+ throw new Error('Multiple subjects matched. Not supported yet.');
539
+ }
540
+ return subjects[0];
519
541
  }
520
- const result = await this.#authenticationAncillaryService.resolveSubject(subject);
521
- if (result.success) {
522
- return result.subject;
542
+ const subjects = await this.#authenticationAncillaryService.resolveSubjects(subject);
543
+ if (subjects.length > 1) {
544
+ throw new Error('Multiple subjects matched. Not supported yet.');
545
+ }
546
+ return subjects[0];
547
+ }
548
+ async resolveSubjects(subjectInput) {
549
+ if (isUndefined(this.#authenticationAncillaryService)) {
550
+ return await this.defaultResolveSubjects(subjectInput);
523
551
  }
524
- return undefined;
552
+ return await this.#authenticationAncillaryService.resolveSubjects(subjectInput);
525
553
  }
526
554
  /**
527
555
  * Resolves the subject to the actual subject used for authentication.
@@ -531,14 +559,14 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
531
559
  * @returns The resolved subject or the original subject if not found.
532
560
  */
533
561
  async resolveSubject(subject) {
534
- if (isUndefined(this.#authenticationAncillaryService)) {
535
- return subject;
562
+ const subjects = await this.resolveSubjects(subject);
563
+ if (subjects.length > 1) {
564
+ throw new Error('Multiple subjects matched. Not supported yet.');
536
565
  }
537
- const result = await this.#authenticationAncillaryService.resolveSubject(subject);
538
- if (result.success) {
539
- return result.subject;
566
+ if (subjects.length == 0) {
567
+ throw new NotFoundError(`Subject not found.`);
540
568
  }
541
- throw new NotFoundError(`Subject not found.`);
569
+ return subjects[0];
542
570
  }
543
571
  /**
544
572
  * Creates a token without session or refresh token and is not saved in database.
@@ -556,8 +584,9 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
556
584
  iat: issuedAt ?? timestampToTimestampSeconds(timestamp),
557
585
  exp: expiration ?? timestampToTimestampSeconds(timestamp + this.tokenTimeToLive),
558
586
  refreshTokenExp: timestampToTimestampSeconds(refreshTokenExpiration),
559
- sessionId,
560
- subject,
587
+ session: sessionId,
588
+ tenant: subject.tenantId,
589
+ subject: subject.id,
561
590
  impersonator: impersonatedBy,
562
591
  ...additionalTokenPayload,
563
592
  };
@@ -571,6 +600,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
571
600
  /**
572
601
  * Creates a refresh token without session and is not saved in database.
573
602
  * @param subject The subject of the refresh token.
603
+ * @param tenantId The tenant id of the refresh token.
574
604
  * @param sessionId The session id of the refresh token.
575
605
  * @param expirationTimestamp The expiration timestamp of the refresh token.
576
606
  * @param options Options for creating the refresh token.
@@ -587,15 +617,35 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
587
617
  },
588
618
  payload: {
589
619
  exp: timestampToTimestampSeconds(expirationTimestamp),
590
- subject,
620
+ subject: subject.id,
621
+ tenant: subject.tenantId,
591
622
  impersonator: options?.impersonator,
592
- sessionId,
623
+ session: sessionId,
593
624
  secret,
594
625
  },
595
626
  };
596
627
  const token = await createJwtTokenString(jsonToken, this.derivedRefreshTokenSigningSecret);
597
628
  return { token, jsonToken, salt, hash: new Uint8Array(hash) };
598
629
  }
630
+ async defaultResolveSubjects({ tenantId, subject }) {
631
+ const [subjectsById, usersByMail] = await Promise.all([
632
+ this.#subjectRepository.loadManyByQuery({ tenantId, id: subject }),
633
+ this.#userRepository.loadManyByQuery({ tenantId, email: subject }),
634
+ ]);
635
+ const userIds = getEntityIds(usersByMail);
636
+ const subjectsByUser = (userIds.length == 0) ? [] : await this.#subjectRepository.loadManyByQuery({ tenantId, userId: { $in: userIds } });
637
+ return [...subjectsById, ...subjectsByUser];
638
+ }
639
+ async defaultResolveSubject({ tenantId, subject }) {
640
+ const subjects = await this.defaultResolveSubjects({ tenantId, subject });
641
+ if (subjects.length > 1) {
642
+ throw new Error('Multiple subjects matched. Not supported yet.');
643
+ }
644
+ if (subjects.length == 0) {
645
+ throw new NotFoundError('Subject not found.');
646
+ }
647
+ return subjects[0];
648
+ }
599
649
  async createSecretResetToken(subject, expirationTimestamp) {
600
650
  const jsonToken = {
601
651
  header: {
@@ -604,7 +654,8 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
604
654
  },
605
655
  payload: {
606
656
  exp: timestampToTimestampSeconds(expirationTimestamp),
607
- subject,
657
+ subject: subject.id,
658
+ tenant: subject.tenantId,
608
659
  },
609
660
  };
610
661
  const token = await createJwtTokenString(jsonToken, this.derivedSecretResetTokenSigningSecret);
@@ -1,6 +1,22 @@
1
- import { Subject, SystemAccount } from '../models/index.js';
1
+ import { ServiceAccount, Subject, User } from '../models/index.js';
2
+ export type CreateUser = Pick<User, 'tenantId' | 'email' | 'firstName' | 'lastName'> & Partial<Pick<User, 'status'>>;
3
+ export type CreateServiceAccount = Pick<ServiceAccount, 'tenantId' | 'description' | 'parent'>;
2
4
  export declare class SubjectService {
3
- readonly subjectRepository: import("../../orm/server/index.js").EntityRepository<Subject>;
4
- readonly systemAccountRepository: import("../../orm/server/index.js").EntityRepository<SystemAccount>;
5
+ #private;
6
+ getSubject(id: string): Promise<Subject>;
7
+ tryGetSubject(id: string): Promise<Subject | undefined>;
5
8
  getSystemAccountSubject(tenantId: string, identifier: string): Promise<Subject>;
9
+ createUser(data: CreateUser): Promise<User>;
10
+ updateUser(tenantId: string, userId: string, data: Partial<Pick<User, 'firstName' | 'lastName' | 'status'>>): Promise<void>;
11
+ updateUserEmail(tenantId: string, userId: string, email: string): Promise<void>;
12
+ getUserSubject(tenantId: string, userId: string): Promise<Subject>;
13
+ getUserByEmail(tenantId: string, email: string): Promise<User>;
14
+ tryGetUserByEmail(tenantId: string, email: string): Promise<User | undefined>;
15
+ hasUserByEmail(tenantId: string, email: string): Promise<boolean>;
16
+ getUserBySubject(subject: Subject): Promise<User>;
17
+ loadManyUsersByEmails(tenantId: string, emails: string[]): Promise<User[]>;
18
+ createServiceAccount(data: CreateServiceAccount): Promise<ServiceAccount>;
19
+ updateServiceAccount(tenantId: string, serviceAccountId: string, data: Partial<Pick<ServiceAccount, 'description'>> & Partial<Pick<Subject, 'displayName'>>): Promise<void>;
20
+ getServiceAccountSubject(tenantId: string, serviceAccountId: string): Promise<Subject>;
21
+ getServiceAccountBySubject(subject: Subject): Promise<ServiceAccount>;
6
22
  }