@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.
- package/api/server/gateway.js +1 -1
- package/authentication/authentication.api.d.ts +9 -0
- package/authentication/authentication.api.js +3 -0
- package/authentication/client/authentication.service.d.ts +23 -15
- package/authentication/client/authentication.service.js +30 -21
- package/authentication/index.d.ts +1 -0
- package/authentication/index.js +1 -0
- package/authentication/models/authentication-credentials.model.d.ts +2 -2
- package/authentication/models/authentication-credentials.model.js +5 -5
- package/authentication/models/authentication-session.model.d.ts +2 -2
- package/authentication/models/authentication-session.model.js +3 -3
- package/authentication/models/subject.model.js +5 -3
- package/authentication/models/token-payload-base.model.d.ts +5 -1
- package/authentication/models/token-payload-base.model.js +10 -2
- package/authentication/models/token.model.d.ts +9 -1
- package/authentication/server/authentication-ancillary.service.d.ts +12 -15
- package/authentication/server/authentication-ancillary.service.js +3 -0
- package/authentication/server/authentication.api-controller.js +5 -5
- package/authentication/server/authentication.audit.d.ts +7 -5
- package/authentication/server/authentication.service.d.ts +32 -22
- package/authentication/server/authentication.service.js +116 -65
- package/authentication/server/subject.service.d.ts +19 -3
- package/authentication/server/subject.service.js +126 -9
- package/authentication/types.d.ts +6 -0
- package/authentication/types.js +1 -0
- package/examples/api/authentication.js +3 -2
- package/examples/api/custom-authentication.js +11 -9
- package/package.json +1 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Auditor } from '../../audit/index.js';
|
|
2
|
-
import { type AfterResolve
|
|
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
|
|
23
|
-
subject:
|
|
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:
|
|
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:
|
|
137
|
+
subject: Subject;
|
|
137
138
|
}, never, unknown>;
|
|
138
139
|
afterLogin: import("../../utils/async-hook/async-hook.js").AsyncHook<{
|
|
139
|
-
subject:
|
|
140
|
+
subject: Subject;
|
|
140
141
|
}, never, unknown>;
|
|
141
142
|
beforeChangeSecret: import("../../utils/async-hook/async-hook.js").AsyncHook<{
|
|
142
|
-
subject:
|
|
143
|
+
subject: Subject;
|
|
143
144
|
}, never, unknown>;
|
|
144
145
|
afterChangeSecret: import("../../utils/async-hook/async-hook.js").AsyncHook<{
|
|
145
|
-
subject:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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(
|
|
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,
|
|
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:
|
|
243
|
+
initSecretReset(subject: SubjectInput, data: AdditionalInitSecretResetData, auditor: Auditor): Promise<void>;
|
|
243
244
|
/**
|
|
244
245
|
* Changes a subject's secret.
|
|
245
|
-
* @param
|
|
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(
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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)
|
|
159
|
-
// Always try to load credentials, even if the subject is not resolved, to
|
|
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:
|
|
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
|
-
|
|
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(
|
|
192
|
-
const { token, jsonToken } = await this.createToken({ additionalTokenPayload: tokenPayload, subject
|
|
193
|
-
const refreshToken = await this.createRefreshToken(
|
|
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
|
|
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(
|
|
214
|
+
async login(subjectInput, secret, data, auditor) {
|
|
212
215
|
const authAuditor = auditor.fork(AuthenticationService_1.name);
|
|
213
|
-
const authenticationResult = await this.authenticate(
|
|
216
|
+
const authenticationResult = await this.authenticate(subjectInput, secret);
|
|
214
217
|
if (!authenticationResult.success) {
|
|
215
|
-
const actualSubject = await this.tryResolveSubject(
|
|
218
|
+
const actualSubject = await this.tryResolveSubject(subjectInput);
|
|
216
219
|
await authAuditor.warn('login-failure', {
|
|
217
|
-
|
|
220
|
+
tenantId: actualSubject?.tenantId ?? subjectInput.tenantId,
|
|
221
|
+
targetId: actualSubject?.id ?? NIL_UUID,
|
|
218
222
|
targetType: 'User',
|
|
219
|
-
details: {
|
|
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.
|
|
230
|
+
const sessionId = token.jsonToken.payload.session;
|
|
227
231
|
await authAuditor.info('login-success', {
|
|
228
|
-
|
|
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.
|
|
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
|
|
285
|
-
const
|
|
286
|
-
const
|
|
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,
|
|
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
|
|
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
|
-
|
|
338
|
+
tenantId: impersonatorSubject.tenantId,
|
|
339
|
+
actor: impersonatorSubject.id,
|
|
331
340
|
actorType: ActorType.User,
|
|
332
|
-
targetId:
|
|
341
|
+
targetId: subjectId,
|
|
333
342
|
targetType: 'User',
|
|
334
|
-
details: {
|
|
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: {
|
|
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
|
-
|
|
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
|
|
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(
|
|
428
|
+
async changeSecret(subjectInput, currentSecret, newSecret, auditor) {
|
|
414
429
|
const authAuditor = auditor.fork(AuthenticationService_1.name);
|
|
415
|
-
const authenticationResult = await this.authenticate(
|
|
430
|
+
const authenticationResult = await this.authenticate(subjectInput, currentSecret);
|
|
416
431
|
if (!authenticationResult.success) {
|
|
417
|
-
const resolvedSubject = await this.tryResolveSubject(
|
|
432
|
+
const resolvedSubject = await this.tryResolveSubject(subjectInput);
|
|
418
433
|
await authAuditor.warn('change-secret-failure', {
|
|
419
|
-
|
|
434
|
+
tenantId: resolvedSubject?.tenantId,
|
|
435
|
+
targetId: resolvedSubject?.id ?? NIL_UUID,
|
|
420
436
|
targetType: 'User',
|
|
421
|
-
details: {
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
521
|
-
if (
|
|
522
|
-
|
|
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
|
|
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
|
-
|
|
535
|
-
|
|
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
|
-
|
|
538
|
-
|
|
539
|
-
return result.subject;
|
|
566
|
+
if (subjects.length == 0) {
|
|
567
|
+
throw new NotFoundError(`Subject not found.`);
|
|
540
568
|
}
|
|
541
|
-
|
|
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,
|
|
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
|
-
|
|
4
|
-
|
|
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
|
}
|