@3sixty/api-auth-entity-storage-service 0.0.0-stage → 0.20.0-next.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.
Files changed (133) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +24 -2
  3. package/dist/es/entities/authenticationAuditEntry.js +121 -0
  4. package/dist/es/entities/authenticationAuditEntry.js.map +1 -0
  5. package/dist/es/entities/authenticationRateEntry.js +37 -0
  6. package/dist/es/entities/authenticationRateEntry.js.map +1 -0
  7. package/dist/es/entities/authenticationUser.js +69 -0
  8. package/dist/es/entities/authenticationUser.js.map +1 -0
  9. package/dist/es/index.js +29 -0
  10. package/dist/es/index.js.map +1 -0
  11. package/dist/es/models/IAuthHeaderProcessorConfig.js +4 -0
  12. package/dist/es/models/IAuthHeaderProcessorConfig.js.map +1 -0
  13. package/dist/es/models/IAuthHeaderProcessorConstructorOptions.js +2 -0
  14. package/dist/es/models/IAuthHeaderProcessorConstructorOptions.js.map +1 -0
  15. package/dist/es/models/IAuthTokenContext.js +4 -0
  16. package/dist/es/models/IAuthTokenContext.js.map +1 -0
  17. package/dist/es/models/IEntityStorageAuthenticationAdminServiceConfig.js +4 -0
  18. package/dist/es/models/IEntityStorageAuthenticationAdminServiceConfig.js.map +1 -0
  19. package/dist/es/models/IEntityStorageAuthenticationAdminServiceConstructorOptions.js +2 -0
  20. package/dist/es/models/IEntityStorageAuthenticationAdminServiceConstructorOptions.js.map +1 -0
  21. package/dist/es/models/IEntityStorageAuthenticationAuditServiceConfig.js +4 -0
  22. package/dist/es/models/IEntityStorageAuthenticationAuditServiceConfig.js.map +1 -0
  23. package/dist/es/models/IEntityStorageAuthenticationAuditServiceConstructorOptions.js +2 -0
  24. package/dist/es/models/IEntityStorageAuthenticationAuditServiceConstructorOptions.js.map +1 -0
  25. package/dist/es/models/IEntityStorageAuthenticationRateServiceConfig.js +2 -0
  26. package/dist/es/models/IEntityStorageAuthenticationRateServiceConfig.js.map +1 -0
  27. package/dist/es/models/IEntityStorageAuthenticationRateServiceConstructorOptions.js +2 -0
  28. package/dist/es/models/IEntityStorageAuthenticationRateServiceConstructorOptions.js.map +1 -0
  29. package/dist/es/models/IEntityStorageAuthenticationServiceConfig.js +2 -0
  30. package/dist/es/models/IEntityStorageAuthenticationServiceConfig.js.map +1 -0
  31. package/dist/es/models/IEntityStorageAuthenticationServiceConstructorOptions.js +2 -0
  32. package/dist/es/models/IEntityStorageAuthenticationServiceConstructorOptions.js.map +1 -0
  33. package/dist/es/processors/authHeaderProcessor.js +277 -0
  34. package/dist/es/processors/authHeaderProcessor.js.map +1 -0
  35. package/dist/es/restEntryPoints.js +27 -0
  36. package/dist/es/restEntryPoints.js.map +1 -0
  37. package/dist/es/routes/entityStorageAuthenticationAdminRoutes.js +367 -0
  38. package/dist/es/routes/entityStorageAuthenticationAdminRoutes.js.map +1 -0
  39. package/dist/es/routes/entityStorageAuthenticationAuditRoutes.js +343 -0
  40. package/dist/es/routes/entityStorageAuthenticationAuditRoutes.js.map +1 -0
  41. package/dist/es/routes/entityStorageAuthenticationRoutes.js +247 -0
  42. package/dist/es/routes/entityStorageAuthenticationRoutes.js.map +1 -0
  43. package/dist/es/schema.js +15 -0
  44. package/dist/es/schema.js.map +1 -0
  45. package/dist/es/services/entityStorageAuthenticationAdminService.js +323 -0
  46. package/dist/es/services/entityStorageAuthenticationAdminService.js.map +1 -0
  47. package/dist/es/services/entityStorageAuthenticationAuditService.js +240 -0
  48. package/dist/es/services/entityStorageAuthenticationAuditService.js.map +1 -0
  49. package/dist/es/services/entityStorageAuthenticationRateService.js +213 -0
  50. package/dist/es/services/entityStorageAuthenticationRateService.js.map +1 -0
  51. package/dist/es/services/entityStorageAuthenticationService.js +326 -0
  52. package/dist/es/services/entityStorageAuthenticationService.js.map +1 -0
  53. package/dist/es/utils/passwordHelper.js +58 -0
  54. package/dist/es/utils/passwordHelper.js.map +1 -0
  55. package/dist/es/utils/tokenHelper.js +142 -0
  56. package/dist/es/utils/tokenHelper.js.map +1 -0
  57. package/dist/types/entities/authenticationAuditEntry.d.ts +49 -0
  58. package/dist/types/entities/authenticationRateEntry.d.ts +17 -0
  59. package/dist/types/entities/authenticationUser.d.ts +33 -0
  60. package/dist/types/index.d.ts +26 -0
  61. package/dist/types/models/IAuthHeaderProcessorConfig.d.ts +39 -0
  62. package/dist/types/models/IAuthHeaderProcessorConstructorOptions.d.ts +25 -0
  63. package/dist/types/models/IAuthTokenContext.d.ts +34 -0
  64. package/dist/types/models/IEntityStorageAuthenticationAdminServiceConfig.d.ts +14 -0
  65. package/dist/types/models/IEntityStorageAuthenticationAdminServiceConstructorOptions.d.ts +20 -0
  66. package/dist/types/models/IEntityStorageAuthenticationAuditServiceConfig.d.ts +9 -0
  67. package/dist/types/models/IEntityStorageAuthenticationAuditServiceConstructorOptions.d.ts +15 -0
  68. package/dist/types/models/IEntityStorageAuthenticationRateServiceConfig.d.ts +10 -0
  69. package/dist/types/models/IEntityStorageAuthenticationRateServiceConstructorOptions.d.ts +25 -0
  70. package/dist/types/models/IEntityStorageAuthenticationServiceConfig.d.ts +36 -0
  71. package/dist/types/models/IEntityStorageAuthenticationServiceConstructorOptions.d.ts +35 -0
  72. package/dist/types/processors/authHeaderProcessor.d.ts +57 -0
  73. package/dist/types/restEntryPoints.d.ts +5 -0
  74. package/dist/types/routes/entityStorageAuthenticationAdminRoutes.d.ts +62 -0
  75. package/dist/types/routes/entityStorageAuthenticationAuditRoutes.d.ts +54 -0
  76. package/dist/types/routes/entityStorageAuthenticationRoutes.d.ts +45 -0
  77. package/dist/types/schema.d.ts +4 -0
  78. package/dist/types/services/entityStorageAuthenticationAdminService.d.ts +65 -0
  79. package/dist/types/services/entityStorageAuthenticationAuditService.d.ts +72 -0
  80. package/dist/types/services/entityStorageAuthenticationRateService.d.ts +60 -0
  81. package/dist/types/services/entityStorageAuthenticationService.d.ts +65 -0
  82. package/dist/types/utils/passwordHelper.d.ts +24 -0
  83. package/dist/types/utils/tokenHelper.d.ts +60 -0
  84. package/docs/changelog.md +0 -0
  85. package/docs/examples.md +178 -0
  86. package/docs/open-api/spec.json +2122 -0
  87. package/docs/reference/classes/AuthHeaderProcessor.md +193 -0
  88. package/docs/reference/classes/AuthenticationAuditEntry.md +101 -0
  89. package/docs/reference/classes/AuthenticationRateEntry.md +37 -0
  90. package/docs/reference/classes/AuthenticationUser.md +69 -0
  91. package/docs/reference/classes/EntityStorageAuthenticationAdminService.md +229 -0
  92. package/docs/reference/classes/EntityStorageAuthenticationAuditService.md +241 -0
  93. package/docs/reference/classes/EntityStorageAuthenticationRateService.md +227 -0
  94. package/docs/reference/classes/EntityStorageAuthenticationService.md +221 -0
  95. package/docs/reference/classes/PasswordHelper.md +74 -0
  96. package/docs/reference/classes/TokenHelper.md +205 -0
  97. package/docs/reference/functions/authenticationAdminCreateUser.md +37 -0
  98. package/docs/reference/functions/authenticationAdminGetUser.md +31 -0
  99. package/docs/reference/functions/authenticationAdminGetUserByIdentity.md +31 -0
  100. package/docs/reference/functions/authenticationAdminRemoveUser.md +31 -0
  101. package/docs/reference/functions/authenticationAdminUpdateUser.md +31 -0
  102. package/docs/reference/functions/authenticationAdminUpdateUserPassword.md +31 -0
  103. package/docs/reference/functions/authenticationAuditCreate.md +37 -0
  104. package/docs/reference/functions/authenticationAuditGet.md +31 -0
  105. package/docs/reference/functions/authenticationAuditQuery.md +31 -0
  106. package/docs/reference/functions/authenticationAuditRemove.md +31 -0
  107. package/docs/reference/functions/authenticationAuditUpdate.md +31 -0
  108. package/docs/reference/functions/authenticationLogin.md +31 -0
  109. package/docs/reference/functions/authenticationLogout.md +31 -0
  110. package/docs/reference/functions/authenticationRefreshToken.md +31 -0
  111. package/docs/reference/functions/authenticationUpdatePassword.md +31 -0
  112. package/docs/reference/functions/generateRestRoutesAuthentication.md +25 -0
  113. package/docs/reference/functions/generateRestRoutesAuthenticationAdmin.md +25 -0
  114. package/docs/reference/functions/generateRestRoutesAuthenticationAudit.md +25 -0
  115. package/docs/reference/functions/initSchema.md +9 -0
  116. package/docs/reference/index.md +57 -0
  117. package/docs/reference/interfaces/IAuthHeaderProcessorConfig.md +86 -0
  118. package/docs/reference/interfaces/IAuthHeaderProcessorConstructorOptions.md +53 -0
  119. package/docs/reference/interfaces/IAuthTokenContext.md +60 -0
  120. package/docs/reference/interfaces/IEntityStorageAuthenticationAdminServiceConfig.md +25 -0
  121. package/docs/reference/interfaces/IEntityStorageAuthenticationAdminServiceConstructorOptions.md +39 -0
  122. package/docs/reference/interfaces/IEntityStorageAuthenticationAuditServiceConfig.md +11 -0
  123. package/docs/reference/interfaces/IEntityStorageAuthenticationAuditServiceConstructorOptions.md +25 -0
  124. package/docs/reference/interfaces/IEntityStorageAuthenticationRateServiceConfig.md +17 -0
  125. package/docs/reference/interfaces/IEntityStorageAuthenticationRateServiceConstructorOptions.md +53 -0
  126. package/docs/reference/interfaces/IEntityStorageAuthenticationServiceConfig.md +87 -0
  127. package/docs/reference/interfaces/IEntityStorageAuthenticationServiceConstructorOptions.md +81 -0
  128. package/docs/reference/variables/restEntryPoints.md +5 -0
  129. package/docs/reference/variables/tagsAuthentication.md +5 -0
  130. package/docs/reference/variables/tagsAuthenticationAdmin.md +5 -0
  131. package/docs/reference/variables/tagsAuthenticationAudit.md +5 -0
  132. package/locales/en.json +52 -0
  133. package/package.json +64 -5
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entityStorageAuthenticationAuditService.js","sourceRoot":"","sources":["../../../src/services/entityStorageAuthenticationAuditService.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type {\n\tAuthAuditEvent,\n\tIAuthenticationAuditComponent,\n\tIAuthenticationAuditEntry\n} from \"@3sixty/api-auth-entity-storage-models\";\nimport { HttpContextIdKeys } from \"@3sixty/api-models\";\nimport { ContextIdStore, ContextIdKeys } from \"@3sixty/context\";\nimport {\n\tConverter,\n\tGeneralError,\n\tGuards,\n\tIs,\n\tNotFoundError,\n\tRandomHelper,\n\tValidation,\n\ttype IValidationFailure\n} from \"@3sixty/core\";\nimport { Sha256 } from \"@3sixty/crypto\";\nimport { ComparisonOperator } from \"@3sixty/entity\";\nimport {\n\tEntityStorageConnectorFactory,\n\ttype IEntityStorageConnector\n} from \"@3sixty/entity-storage-models\";\nimport { nameof } from \"@3sixty/nameof\";\nimport type { AuthenticationAuditEntry } from \"../entities/authenticationAuditEntry.js\";\nimport type { IEntityStorageAuthenticationAuditServiceConstructorOptions } from \"../models/IEntityStorageAuthenticationAuditServiceConstructorOptions.js\";\n\n/**\n * Implementation of the authentication audit component using entity storage.\n */\nexport class EntityStorageAuthenticationAuditService implements IAuthenticationAuditComponent {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<EntityStorageAuthenticationAuditService>();\n\n\t/**\n\t * The entity storage for authentication audit entries.\n\t * @internal\n\t */\n\tprivate readonly _authenticationAuditEntryEntityStorage: IEntityStorageConnector<AuthenticationAuditEntry>;\n\n\t/**\n\t * The server-side salt for hashing IP addresses in audit logs, if configured.\n\t * @internal\n\t */\n\tprivate readonly _ipHashSalt?: string;\n\n\t/**\n\t * Create a new instance of EntityStorageAuthenticationAuditService.\n\t * @param options The dependencies for the identity connector.\n\t */\n\tconstructor(options?: IEntityStorageAuthenticationAuditServiceConstructorOptions) {\n\t\tthis._authenticationAuditEntryEntityStorage = EntityStorageConnectorFactory.get(\n\t\t\toptions?.authenticationAuditEntryStorageType ?? \"authentication-audit-entry\"\n\t\t);\n\t\tconst salt = options?.config?.ipHashSalt?.trim();\n\t\tif (Is.stringValue(salt)) {\n\t\t\tconst validationFailures: IValidationFailure[] = [];\n\t\t\tValidation.stringValue(\n\t\t\t\tnameof(options?.config?.ipHashSalt),\n\t\t\t\tsalt,\n\t\t\t\tvalidationFailures,\n\t\t\t\tundefined,\n\t\t\t\t{ minLength: 32 }\n\t\t\t);\n\t\t\tconst entropy = new Set(salt).size;\n\t\t\tif (entropy < 8) {\n\t\t\t\tvalidationFailures.push({\n\t\t\t\t\tproperty: nameof(options?.config?.ipHashSalt),\n\t\t\t\t\treason: \"validation.saltEntropyTooLow\"\n\t\t\t\t});\n\t\t\t}\n\t\t\tValidation.asValidationError(\n\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\tnameof(options?.config?.ipHashSalt),\n\t\t\t\tvalidationFailures\n\t\t\t);\n\n\t\t\tthis._ipHashSalt = salt;\n\t\t}\n\t}\n\n\t/**\n\t * Returns the class name of the component.\n\t * @returns The class name of the component.\n\t */\n\tpublic className(): string {\n\t\treturn EntityStorageAuthenticationAuditService.CLASS_NAME;\n\t}\n\n\t/**\n\t * Create a new audit entry.\n\t * @param entry The audit entry to be logged.\n\t * @returns The unique identifier of the created audit entry.\n\t */\n\tpublic async create(\n\t\tentry: Omit<IAuthenticationAuditEntry, \"id\" | \"dateCreated\">\n\t): Promise<string> {\n\t\tGuards.object<IAuthenticationAuditEntry>(\n\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\tnameof(entry),\n\t\t\tentry\n\t\t);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\tnameof(entry.event),\n\t\t\tentry.event\n\t\t);\n\n\t\tconst contextIds = await ContextIdStore.getContextIds();\n\n\t\tconst newAuditEntry: AuthenticationAuditEntry = {\n\t\t\tid: RandomHelper.generateUuidV7(\"compact\"),\n\t\t\tdateCreated: new Date().toISOString(),\n\t\t\tevent: entry.event,\n\t\t\tactorId: entry.actorId ?? contextIds?.[ContextIdKeys.User],\n\t\t\tnodeId: entry.nodeId ?? contextIds?.[ContextIdKeys.Node],\n\t\t\torganizationId: entry.organizationId ?? contextIds?.[ContextIdKeys.Organization],\n\t\t\ttenantId: entry.tenantId ?? contextIds?.[ContextIdKeys.Tenant],\n\t\t\tdata: entry.data,\n\t\t\tipAddressHashes: this.hashIpAddresses(contextIds?.[HttpContextIdKeys.IpAddress]?.split(\"|\")),\n\t\t\tuserAgent: contextIds?.[HttpContextIdKeys.UserAgent],\n\t\t\tcorrelationId: contextIds?.[HttpContextIdKeys.CorrelationId]\n\t\t};\n\n\t\ttry {\n\t\t\tawait this._authenticationAuditEntryEntityStorage.set(newAuditEntry);\n\t\t} catch {\n\t\t\t// Best-effort audit logging: do not interrupt auth/admin flows if persistence fails.\n\t\t}\n\n\t\treturn newAuditEntry.id;\n\t}\n\n\t/**\n\t * Get an audit entry by id.\n\t * @param id The unique identifier of the audit entry.\n\t * @returns The audit entry.\n\t */\n\tpublic async get(id: string): Promise<IAuthenticationAuditEntry> {\n\t\tGuards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, nameof(id), id);\n\n\t\ttry {\n\t\t\tconst entry = await this._authenticationAuditEntryEntityStorage.get(id);\n\t\t\tif (Is.empty(entry)) {\n\t\t\t\tthrow new NotFoundError(\n\t\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\t\"auditEntryNotFound\",\n\t\t\t\t\tid\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn entry;\n\t\t} catch (error) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\"getAuditEntryFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Update an audit entry.\n\t * @param id The unique identifier of the audit entry to update.\n\t * @param entry The fields to update on the audit entry.\n\t * @returns A promise that resolves when the audit entry has been updated.\n\t */\n\tpublic async update(\n\t\tid: string,\n\t\tentry: Partial<Omit<IAuthenticationAuditEntry, \"id\" | \"dateCreated\">>\n\t): Promise<void> {\n\t\tGuards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, nameof(id), id);\n\t\tGuards.object(EntityStorageAuthenticationAuditService.CLASS_NAME, nameof(entry), entry);\n\n\t\ttry {\n\t\t\tconst existing = await this._authenticationAuditEntryEntityStorage.get(id);\n\t\t\tif (Is.empty(existing)) {\n\t\t\t\tthrow new NotFoundError(\n\t\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\t\"auditEntryNotFound\",\n\t\t\t\t\tid\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tawait this._authenticationAuditEntryEntityStorage.set({\n\t\t\t\t...existing,\n\t\t\t\t...entry,\n\t\t\t\tid,\n\t\t\t\tdateCreated: existing.dateCreated\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\"updateAuditEntryFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Remove an audit entry.\n\t * @param id The unique identifier of the audit entry to remove.\n\t * @returns A promise that resolves when the audit entry has been removed.\n\t */\n\tpublic async remove(id: string): Promise<void> {\n\t\tGuards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, nameof(id), id);\n\n\t\ttry {\n\t\t\tconst existing = await this._authenticationAuditEntryEntityStorage.get(id);\n\t\t\tif (Is.empty(existing)) {\n\t\t\t\tthrow new NotFoundError(\n\t\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\t\"auditEntryNotFound\",\n\t\t\t\t\tid\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tawait this._authenticationAuditEntryEntityStorage.remove(id);\n\t\t} catch (error) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\"removeAuditEntryFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Query the audit entries.\n\t * @param options The query options.\n\t * @param options.actorId The actor identifier to filter the audit entries, optional.\n\t * @param options.organizationId The organization identifier to filter the audit entries, optional.\n\t * @param options.tenantId The tenant identifier to filter the audit entries, optional.\n\t * @param options.nodeId The node identifier to filter the audit entries, optional.\n\t * @param options.event The audit event to filter the audit entries, optional.\n\t * @param options.startDate The start date to filter the audit entries, optional.\n\t * @param options.endDate The end date to filter the audit entries, optional.\n\t * @param cursor The cursor for pagination.\n\t * @param limit The maximum number of entries to return.\n\t * @returns The audit entries.\n\t */\n\tpublic async query(\n\t\toptions?: {\n\t\t\tactorId?: string;\n\t\t\torganizationId?: string;\n\t\t\ttenantId?: string;\n\t\t\tnodeId?: string;\n\t\t\tevent?: AuthAuditEvent | string;\n\t\t\tstartDate?: string;\n\t\t\tendDate?: string;\n\t\t},\n\t\tcursor?: string,\n\t\tlimit?: number\n\t): Promise<{\n\t\tentries: IAuthenticationAuditEntry[];\n\t\tcursor?: string;\n\t}> {\n\t\tconst conditions: {\n\t\t\tproperty: string;\n\t\t\tvalue: string;\n\t\t\tcomparison: (typeof ComparisonOperator)[keyof typeof ComparisonOperator];\n\t\t}[] = [];\n\n\t\tif (Is.object(options)) {\n\t\t\tif (!Is.empty(options.actorId)) {\n\t\t\t\tGuards.stringValue(\n\t\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\tnameof(options.actorId),\n\t\t\t\t\toptions.actorId\n\t\t\t\t);\n\t\t\t\tconditions.push({\n\t\t\t\t\tproperty: \"actorId\",\n\t\t\t\t\tvalue: options.actorId,\n\t\t\t\t\tcomparison: ComparisonOperator.Equals\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (!Is.empty(options.organizationId)) {\n\t\t\t\tGuards.stringValue(\n\t\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\tnameof(options.organizationId),\n\t\t\t\t\toptions.organizationId\n\t\t\t\t);\n\t\t\t\tconditions.push({\n\t\t\t\t\tproperty: \"organizationId\",\n\t\t\t\t\tvalue: options.organizationId,\n\t\t\t\t\tcomparison: ComparisonOperator.Equals\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (!Is.empty(options.tenantId)) {\n\t\t\t\tGuards.stringValue(\n\t\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\tnameof(options.tenantId),\n\t\t\t\t\toptions.tenantId\n\t\t\t\t);\n\t\t\t\tconditions.push({\n\t\t\t\t\tproperty: \"tenantId\",\n\t\t\t\t\tvalue: options.tenantId,\n\t\t\t\t\tcomparison: ComparisonOperator.Equals\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (!Is.empty(options.nodeId)) {\n\t\t\t\tGuards.stringValue(\n\t\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\tnameof(options.nodeId),\n\t\t\t\t\toptions.nodeId\n\t\t\t\t);\n\t\t\t\tconditions.push({\n\t\t\t\t\tproperty: \"nodeId\",\n\t\t\t\t\tvalue: options.nodeId,\n\t\t\t\t\tcomparison: ComparisonOperator.Equals\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (!Is.empty(options.event)) {\n\t\t\t\tGuards.stringValue(\n\t\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\tnameof(options.event),\n\t\t\t\t\toptions.event\n\t\t\t\t);\n\t\t\t\tconditions.push({\n\t\t\t\t\tproperty: \"event\",\n\t\t\t\t\tvalue: options.event,\n\t\t\t\t\tcomparison: ComparisonOperator.Equals\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (!Is.empty(options.startDate)) {\n\t\t\t\tGuards.stringValue(\n\t\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\tnameof(options.startDate),\n\t\t\t\t\toptions.startDate\n\t\t\t\t);\n\t\t\t\tconditions.push({\n\t\t\t\t\tproperty: \"dateCreated\",\n\t\t\t\t\tvalue: options.startDate,\n\t\t\t\t\tcomparison: ComparisonOperator.GreaterThanOrEqual\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (!Is.empty(options.endDate)) {\n\t\t\t\tGuards.stringValue(\n\t\t\t\t\tEntityStorageAuthenticationAuditService.CLASS_NAME,\n\t\t\t\t\tnameof(options.endDate),\n\t\t\t\t\toptions.endDate\n\t\t\t\t);\n\t\t\t\tconditions.push({\n\t\t\t\t\tproperty: \"dateCreated\",\n\t\t\t\t\tvalue: options.endDate,\n\t\t\t\t\tcomparison: ComparisonOperator.LessThanOrEqual\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tconst result = await this._authenticationAuditEntryEntityStorage.query(\n\t\t\tconditions.length > 0 ? { conditions } : undefined,\n\t\t\tundefined,\n\t\t\tundefined,\n\t\t\tcursor,\n\t\t\tlimit\n\t\t);\n\n\t\treturn {\n\t\t\tentries: result.entities as IAuthenticationAuditEntry[],\n\t\t\tcursor: result.cursor\n\t\t};\n\t}\n\n\t/**\n\t * Hash a list of IP addresses using SHA-256.\n\t * @param ipAddresses The IP addresses to hash.\n\t * @returns The hexadecimal hashes of the salted IPs.\n\t * @internal\n\t */\n\tprivate hashIpAddresses(ipAddresses: string[] | undefined): string[] | undefined {\n\t\tif (!Is.stringValue(this._ipHashSalt) || !Is.array(ipAddresses)) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn ipAddresses.map(ip => {\n\t\t\tconst hash = Sha256.sum256(Converter.utf8ToBytes(`${this._ipHashSalt}:${ip}`));\n\t\t\treturn Converter.bytesToHex(hash);\n\t\t});\n\t}\n}\n"]}
@@ -0,0 +1,213 @@
1
+ import { TooManyRequestsError } from "@3sixty/api-models";
2
+ import { ComponentFactory, Converter, GeneralError, Guards, Is } from "@3sixty/core";
3
+ import { Sha256 } from "@3sixty/crypto";
4
+ import { ComparisonOperator } from "@3sixty/entity";
5
+ import { EntityStorageConnectorFactory } from "@3sixty/entity-storage-models";
6
+ /**
7
+ * Implementation of the authentication rate component using entity storage.
8
+ */
9
+ export class EntityStorageAuthenticationRateService {
10
+ /**
11
+ * Runtime name for the class.
12
+ */
13
+ static CLASS_NAME = "EntityStorageAuthenticationRateService";
14
+ /**
15
+ * Cleanup task id.
16
+ * @internal
17
+ */
18
+ static _CLEANUP_TASK_ID = "authentication-rate-cleanup";
19
+ /**
20
+ * Default cleanup interval in minutes.
21
+ * @internal
22
+ */
23
+ static _DEFAULT_CLEANUP_INTERVAL_MINUTES = 5;
24
+ /**
25
+ * Number of entries to retrieve in each cleanup page.
26
+ * @internal
27
+ */
28
+ static _CLEANUP_PAGE_SIZE = 250;
29
+ /**
30
+ * The entity storage for authentication rate entries.
31
+ * @internal
32
+ */
33
+ _authenticationRateEntryStorage;
34
+ /**
35
+ * The rate limits for each action.
36
+ * @internal
37
+ */
38
+ _actionConfigs;
39
+ /**
40
+ * The task scheduler.
41
+ * @internal
42
+ */
43
+ _taskScheduler;
44
+ /**
45
+ * The cleanup interval in minutes.
46
+ * @internal
47
+ */
48
+ _cleanupIntervalMinutes;
49
+ /**
50
+ * Platform component used to run the periodic cleanup with the correct tenant context.
51
+ * @internal
52
+ */
53
+ _platformComponent;
54
+ /**
55
+ * Create a new instance of EntityStorageAuthenticationRateService.
56
+ * @param options The constructor options.
57
+ */
58
+ constructor(options) {
59
+ this._authenticationRateEntryStorage = EntityStorageConnectorFactory.get(options?.authenticationRateEntryStorageType ?? "authentication-rate-entry");
60
+ this._actionConfigs = {};
61
+ this._taskScheduler = ComponentFactory.get(options?.taskSchedulerComponentType ?? "task-scheduler");
62
+ this._cleanupIntervalMinutes =
63
+ options?.config?.cleanupIntervalMinutes ??
64
+ EntityStorageAuthenticationRateService._DEFAULT_CLEANUP_INTERVAL_MINUTES;
65
+ this._platformComponent = ComponentFactory.get(options?.platformComponentType ?? "platform");
66
+ }
67
+ /**
68
+ * Register or update rate-limit configuration for an action.
69
+ * @param action The action name.
70
+ * @param config The action configuration.
71
+ * @returns A promise that resolves when the action configuration has been stored.
72
+ */
73
+ async registerAction(action, config) {
74
+ Guards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, "action", action);
75
+ Guards.object(EntityStorageAuthenticationRateService.CLASS_NAME, "config", config);
76
+ this._actionConfigs[action] = {
77
+ maxAttempts: config.maxAttempts,
78
+ windowMinutes: config.windowMinutes
79
+ };
80
+ }
81
+ /**
82
+ * Unregister rate-limit configuration for an action.
83
+ * @param action The action name.
84
+ * @returns A promise that resolves when the action configuration has been removed.
85
+ */
86
+ async unregisterAction(action) {
87
+ Guards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, "action", action);
88
+ delete this._actionConfigs[action];
89
+ }
90
+ /**
91
+ * Returns the class name of the component.
92
+ * @returns The class name of the component.
93
+ */
94
+ className() {
95
+ return EntityStorageAuthenticationRateService.CLASS_NAME;
96
+ }
97
+ /**
98
+ * The service needs to be started when the application is initialized.
99
+ * @param nodeLoggingComponentType The node logging component type.
100
+ * @returns A promise that resolves when the periodic cleanup task has been registered.
101
+ */
102
+ async start(nodeLoggingComponentType) {
103
+ await this._taskScheduler.addTask(EntityStorageAuthenticationRateService._CLEANUP_TASK_ID, [
104
+ {
105
+ intervalMinutes: this._cleanupIntervalMinutes
106
+ }
107
+ ], async () => this.cleanupExpiredEntries());
108
+ }
109
+ /**
110
+ * The component needs to be stopped when the node is closed.
111
+ * @param nodeLoggingComponentType The node logging component type.
112
+ * @returns A promise that resolves when the periodic cleanup task has been removed.
113
+ */
114
+ async stop(nodeLoggingComponentType) {
115
+ await this._taskScheduler.removeTask(EntityStorageAuthenticationRateService._CLEANUP_TASK_ID);
116
+ }
117
+ /**
118
+ * Check the authentication rate for a given action and identifier.
119
+ * @param action The action to be checked.
120
+ * @param identifier The identifier to be checked.
121
+ * @returns The rate entry id.
122
+ */
123
+ async check(action, identifier) {
124
+ Guards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, "action", action);
125
+ Guards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, "identifier", identifier);
126
+ const actionConfig = this._actionConfigs[action];
127
+ if (!Is.object(actionConfig)) {
128
+ throw new GeneralError(EntityStorageAuthenticationRateService.CLASS_NAME, "actionConfigMissing", {
129
+ action
130
+ });
131
+ }
132
+ const hashedIdentifier = Converter.bytesToHex(Sha256.sum256(Converter.utf8ToBytes(identifier)));
133
+ const compositeId = `|${action}|${hashedIdentifier}|`;
134
+ const now = Date.now();
135
+ const nowIso = new Date(now).toISOString();
136
+ const windowMs = actionConfig.windowMinutes * 60 * 1000;
137
+ const cutoff = now - windowMs;
138
+ // Resolve the existing rate entry for this action + identifier pair.
139
+ const existing = await this._authenticationRateEntryStorage.get(compositeId);
140
+ // Keep only attempts inside the configured sliding window.
141
+ const activeTimestamps = (existing?.timestamps ?? []).filter(timestamp => new Date(timestamp).getTime() > cutoff);
142
+ if (activeTimestamps.length >= actionConfig.maxAttempts) {
143
+ const oldestTimestamp = new Date(activeTimestamps[0]).getTime();
144
+ const nextRequestTime = new Date(oldestTimestamp + windowMs).toISOString();
145
+ const retryAfterSeconds = Math.ceil((oldestTimestamp + windowMs - now) / 1000);
146
+ throw new TooManyRequestsError(EntityStorageAuthenticationRateService.CLASS_NAME, "rateLimitExceeded", activeTimestamps.length, nextRequestTime, {
147
+ action,
148
+ retryAfterSeconds
149
+ });
150
+ }
151
+ activeTimestamps.push(nowIso);
152
+ await this._authenticationRateEntryStorage.set({
153
+ id: compositeId,
154
+ timestamps: activeTimestamps,
155
+ dateModified: nowIso
156
+ });
157
+ return compositeId;
158
+ }
159
+ /**
160
+ * Clear the authentication rate entry for the given action and identifier.
161
+ * @param action The action to clear.
162
+ * @param identifier The identifier to clear.
163
+ * @returns A promise that resolves when the rate entry has been removed.
164
+ */
165
+ async clear(action, identifier) {
166
+ Guards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, "action", action);
167
+ Guards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, "identifier", identifier);
168
+ const hashedIdentifier = Converter.bytesToHex(Sha256.sum256(Converter.utf8ToBytes(identifier)));
169
+ const compositeId = `|${action}|${hashedIdentifier}|`;
170
+ await this._authenticationRateEntryStorage.remove(compositeId);
171
+ }
172
+ /**
173
+ * Cleanup expired rate limit entries. Runs via the platform component's execute so the work runs with
174
+ * the correct context (once in single-tenant mode, once per tenant in multi-tenant mode).
175
+ * @returns A promise that resolves when all expired entries have been removed from storage.
176
+ * @internal
177
+ */
178
+ async cleanupExpiredEntries() {
179
+ // Run via the platform component's execute so the work runs with the correct context (once in
180
+ // single-tenant mode, once per tenant in multi-tenant mode).
181
+ await this._platformComponent.execute(async () => {
182
+ const now = Date.now();
183
+ for (const action of Object.keys(this._actionConfigs)) {
184
+ const actionConfig = this._actionConfigs[action];
185
+ const windowMs = actionConfig.windowMinutes * 60 * 1000;
186
+ const cutoffIso = new Date(now - windowMs).toISOString();
187
+ let cursor;
188
+ do {
189
+ // Query by composite rate entry id prefix and expiry window for this action.
190
+ const result = await this._authenticationRateEntryStorage.query({
191
+ conditions: [
192
+ {
193
+ property: "id",
194
+ value: `|${action}|`,
195
+ comparison: ComparisonOperator.Includes
196
+ },
197
+ {
198
+ property: "dateModified",
199
+ value: cutoffIso,
200
+ comparison: ComparisonOperator.LessThanOrEqual
201
+ }
202
+ ]
203
+ }, undefined, undefined, cursor, EntityStorageAuthenticationRateService._CLEANUP_PAGE_SIZE);
204
+ for (const entity of result.entities) {
205
+ await this._authenticationRateEntryStorage.remove(entity.id);
206
+ }
207
+ cursor = result.cursor;
208
+ } while (!Is.empty(cursor));
209
+ }
210
+ });
211
+ }
212
+ }
213
+ //# sourceMappingURL=entityStorageAuthenticationRateService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entityStorageAuthenticationRateService.js","sourceRoot":"","sources":["../../../src/services/entityStorageAuthenticationRateService.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type {\n\tIAuthenticationRateActionConfig,\n\tIAuthenticationRateComponent\n} from \"@3sixty/api-auth-entity-storage-models\";\nimport { TooManyRequestsError, type IPlatformComponent } from \"@3sixty/api-models\";\nimport type { ITaskSchedulerComponent } from \"@3sixty/background-task-models\";\nimport { ComponentFactory, Converter, GeneralError, Guards, Is } from \"@3sixty/core\";\nimport { Sha256 } from \"@3sixty/crypto\";\nimport { ComparisonOperator } from \"@3sixty/entity\";\nimport {\n\tEntityStorageConnectorFactory,\n\ttype IEntityStorageConnector\n} from \"@3sixty/entity-storage-models\";\nimport { nameof } from \"@3sixty/nameof\";\nimport type { AuthenticationRateEntry } from \"../entities/authenticationRateEntry.js\";\nimport type { IEntityStorageAuthenticationRateServiceConstructorOptions } from \"../models/IEntityStorageAuthenticationRateServiceConstructorOptions.js\";\n\n/**\n * Implementation of the authentication rate component using entity storage.\n */\nexport class EntityStorageAuthenticationRateService implements IAuthenticationRateComponent {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<EntityStorageAuthenticationRateService>();\n\n\t/**\n\t * Cleanup task id.\n\t * @internal\n\t */\n\tprivate static readonly _CLEANUP_TASK_ID = \"authentication-rate-cleanup\";\n\n\t/**\n\t * Default cleanup interval in minutes.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_CLEANUP_INTERVAL_MINUTES = 5;\n\n\t/**\n\t * Number of entries to retrieve in each cleanup page.\n\t * @internal\n\t */\n\tprivate static readonly _CLEANUP_PAGE_SIZE = 250;\n\n\t/**\n\t * The entity storage for authentication rate entries.\n\t * @internal\n\t */\n\tprivate readonly _authenticationRateEntryStorage: IEntityStorageConnector<AuthenticationRateEntry>;\n\n\t/**\n\t * The rate limits for each action.\n\t * @internal\n\t */\n\tprivate readonly _actionConfigs: { [action: string]: IAuthenticationRateActionConfig };\n\n\t/**\n\t * The task scheduler.\n\t * @internal\n\t */\n\tprivate readonly _taskScheduler: ITaskSchedulerComponent;\n\n\t/**\n\t * The cleanup interval in minutes.\n\t * @internal\n\t */\n\tprivate readonly _cleanupIntervalMinutes: number;\n\n\t/**\n\t * Platform component used to run the periodic cleanup with the correct tenant context.\n\t * @internal\n\t */\n\tprivate readonly _platformComponent: IPlatformComponent;\n\n\t/**\n\t * Create a new instance of EntityStorageAuthenticationRateService.\n\t * @param options The constructor options.\n\t */\n\tconstructor(options?: IEntityStorageAuthenticationRateServiceConstructorOptions) {\n\t\tthis._authenticationRateEntryStorage = EntityStorageConnectorFactory.get(\n\t\t\toptions?.authenticationRateEntryStorageType ?? \"authentication-rate-entry\"\n\t\t);\n\t\tthis._actionConfigs = {};\n\t\tthis._taskScheduler = ComponentFactory.get<ITaskSchedulerComponent>(\n\t\t\toptions?.taskSchedulerComponentType ?? \"task-scheduler\"\n\t\t);\n\t\tthis._cleanupIntervalMinutes =\n\t\t\toptions?.config?.cleanupIntervalMinutes ??\n\t\t\tEntityStorageAuthenticationRateService._DEFAULT_CLEANUP_INTERVAL_MINUTES;\n\t\tthis._platformComponent = ComponentFactory.get<IPlatformComponent>(\n\t\t\toptions?.platformComponentType ?? \"platform\"\n\t\t);\n\t}\n\n\t/**\n\t * Register or update rate-limit configuration for an action.\n\t * @param action The action name.\n\t * @param config The action configuration.\n\t * @returns A promise that resolves when the action configuration has been stored.\n\t */\n\tpublic async registerAction(\n\t\taction: string,\n\t\tconfig: IAuthenticationRateActionConfig\n\t): Promise<void> {\n\t\tGuards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, nameof(action), action);\n\t\tGuards.object<IAuthenticationRateActionConfig>(\n\t\t\tEntityStorageAuthenticationRateService.CLASS_NAME,\n\t\t\tnameof(config),\n\t\t\tconfig\n\t\t);\n\n\t\tthis._actionConfigs[action] = {\n\t\t\tmaxAttempts: config.maxAttempts,\n\t\t\twindowMinutes: config.windowMinutes\n\t\t};\n\t}\n\n\t/**\n\t * Unregister rate-limit configuration for an action.\n\t * @param action The action name.\n\t * @returns A promise that resolves when the action configuration has been removed.\n\t */\n\tpublic async unregisterAction(action: string): Promise<void> {\n\t\tGuards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, nameof(action), action);\n\n\t\tdelete this._actionConfigs[action];\n\t}\n\n\t/**\n\t * Returns the class name of the component.\n\t * @returns The class name of the component.\n\t */\n\tpublic className(): string {\n\t\treturn EntityStorageAuthenticationRateService.CLASS_NAME;\n\t}\n\n\t/**\n\t * The service needs to be started when the application is initialized.\n\t * @param nodeLoggingComponentType The node logging component type.\n\t * @returns A promise that resolves when the periodic cleanup task has been registered.\n\t */\n\tpublic async start(nodeLoggingComponentType?: string): Promise<void> {\n\t\tawait this._taskScheduler.addTask(\n\t\t\tEntityStorageAuthenticationRateService._CLEANUP_TASK_ID,\n\t\t\t[\n\t\t\t\t{\n\t\t\t\t\tintervalMinutes: this._cleanupIntervalMinutes\n\t\t\t\t}\n\t\t\t],\n\t\t\tasync () => this.cleanupExpiredEntries()\n\t\t);\n\t}\n\n\t/**\n\t * The component needs to be stopped when the node is closed.\n\t * @param nodeLoggingComponentType The node logging component type.\n\t * @returns A promise that resolves when the periodic cleanup task has been removed.\n\t */\n\tpublic async stop(nodeLoggingComponentType?: string): Promise<void> {\n\t\tawait this._taskScheduler.removeTask(EntityStorageAuthenticationRateService._CLEANUP_TASK_ID);\n\t}\n\n\t/**\n\t * Check the authentication rate for a given action and identifier.\n\t * @param action The action to be checked.\n\t * @param identifier The identifier to be checked.\n\t * @returns The rate entry id.\n\t */\n\tpublic async check(action: string, identifier: string): Promise<string> {\n\t\tGuards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, nameof(action), action);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationRateService.CLASS_NAME,\n\t\t\tnameof(identifier),\n\t\t\tidentifier\n\t\t);\n\n\t\tconst actionConfig = this._actionConfigs[action];\n\n\t\tif (!Is.object(actionConfig)) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationRateService.CLASS_NAME,\n\t\t\t\t\"actionConfigMissing\",\n\t\t\t\t{\n\t\t\t\t\taction\n\t\t\t\t}\n\t\t\t);\n\t\t}\n\n\t\tconst hashedIdentifier = Converter.bytesToHex(Sha256.sum256(Converter.utf8ToBytes(identifier)));\n\t\tconst compositeId = `|${action}|${hashedIdentifier}|`;\n\n\t\tconst now = Date.now();\n\t\tconst nowIso = new Date(now).toISOString();\n\t\tconst windowMs = actionConfig.windowMinutes * 60 * 1000;\n\t\tconst cutoff = now - windowMs;\n\n\t\t// Resolve the existing rate entry for this action + identifier pair.\n\t\tconst existing = await this._authenticationRateEntryStorage.get(compositeId);\n\t\t// Keep only attempts inside the configured sliding window.\n\t\tconst activeTimestamps = (existing?.timestamps ?? []).filter(\n\t\t\ttimestamp => new Date(timestamp).getTime() > cutoff\n\t\t);\n\n\t\tif (activeTimestamps.length >= actionConfig.maxAttempts) {\n\t\t\tconst oldestTimestamp = new Date(activeTimestamps[0]).getTime();\n\t\t\tconst nextRequestTime = new Date(oldestTimestamp + windowMs).toISOString();\n\t\t\tconst retryAfterSeconds = Math.ceil((oldestTimestamp + windowMs - now) / 1000);\n\n\t\t\tthrow new TooManyRequestsError(\n\t\t\t\tEntityStorageAuthenticationRateService.CLASS_NAME,\n\t\t\t\t\"rateLimitExceeded\",\n\t\t\t\tactiveTimestamps.length,\n\t\t\t\tnextRequestTime,\n\t\t\t\t{\n\t\t\t\t\taction,\n\t\t\t\t\tretryAfterSeconds\n\t\t\t\t}\n\t\t\t);\n\t\t}\n\n\t\tactiveTimestamps.push(nowIso);\n\n\t\tawait this._authenticationRateEntryStorage.set({\n\t\t\tid: compositeId,\n\t\t\ttimestamps: activeTimestamps,\n\t\t\tdateModified: nowIso\n\t\t});\n\n\t\treturn compositeId;\n\t}\n\n\t/**\n\t * Clear the authentication rate entry for the given action and identifier.\n\t * @param action The action to clear.\n\t * @param identifier The identifier to clear.\n\t * @returns A promise that resolves when the rate entry has been removed.\n\t */\n\tpublic async clear(action: string, identifier: string): Promise<void> {\n\t\tGuards.stringValue(EntityStorageAuthenticationRateService.CLASS_NAME, nameof(action), action);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationRateService.CLASS_NAME,\n\t\t\tnameof(identifier),\n\t\t\tidentifier\n\t\t);\n\n\t\tconst hashedIdentifier = Converter.bytesToHex(Sha256.sum256(Converter.utf8ToBytes(identifier)));\n\t\tconst compositeId = `|${action}|${hashedIdentifier}|`;\n\n\t\tawait this._authenticationRateEntryStorage.remove(compositeId);\n\t}\n\n\t/**\n\t * Cleanup expired rate limit entries. Runs via the platform component's execute so the work runs with\n\t * the correct context (once in single-tenant mode, once per tenant in multi-tenant mode).\n\t * @returns A promise that resolves when all expired entries have been removed from storage.\n\t * @internal\n\t */\n\tprivate async cleanupExpiredEntries(): Promise<void> {\n\t\t// Run via the platform component's execute so the work runs with the correct context (once in\n\t\t// single-tenant mode, once per tenant in multi-tenant mode).\n\t\tawait this._platformComponent.execute(async () => {\n\t\t\tconst now = Date.now();\n\n\t\t\tfor (const action of Object.keys(this._actionConfigs)) {\n\t\t\t\tconst actionConfig = this._actionConfigs[action];\n\t\t\t\tconst windowMs = actionConfig.windowMinutes * 60 * 1000;\n\t\t\t\tconst cutoffIso = new Date(now - windowMs).toISOString();\n\t\t\t\tlet cursor: string | undefined;\n\n\t\t\t\tdo {\n\t\t\t\t\t// Query by composite rate entry id prefix and expiry window for this action.\n\t\t\t\t\tconst result = await this._authenticationRateEntryStorage.query(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tconditions: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tproperty: \"id\",\n\t\t\t\t\t\t\t\t\tvalue: `|${action}|`,\n\t\t\t\t\t\t\t\t\tcomparison: ComparisonOperator.Includes\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tproperty: \"dateModified\",\n\t\t\t\t\t\t\t\t\tvalue: cutoffIso,\n\t\t\t\t\t\t\t\t\tcomparison: ComparisonOperator.LessThanOrEqual\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\tundefined,\n\t\t\t\t\t\tundefined,\n\t\t\t\t\t\tcursor,\n\t\t\t\t\t\tEntityStorageAuthenticationRateService._CLEANUP_PAGE_SIZE\n\t\t\t\t\t);\n\n\t\t\t\t\tfor (const entity of result.entities as AuthenticationRateEntry[]) {\n\t\t\t\t\t\tawait this._authenticationRateEntryStorage.remove(entity.id);\n\t\t\t\t\t}\n\n\t\t\t\t\tcursor = result.cursor;\n\t\t\t\t} while (!Is.empty(cursor));\n\t\t\t}\n\t\t});\n\t}\n}\n"]}
@@ -0,0 +1,326 @@
1
+ import { AuthAuditEvent } from "@3sixty/api-auth-entity-storage-models";
2
+ import { ScopeHelper } from "@3sixty/api-models";
3
+ import { ContextIdHelper, ContextIdKeys, ContextIdStore } from "@3sixty/context";
4
+ import { Coerce, ComponentFactory, Converter, GeneralError, Guards, Is, NotFoundError, UnauthorizedError } from "@3sixty/core";
5
+ import { PasswordGenerator, PasswordValidator } from "@3sixty/crypto";
6
+ import { EntityStorageConnectorFactory } from "@3sixty/entity-storage-models";
7
+ import { VaultConnectorFactory } from "@3sixty/vault-models";
8
+ import { PasswordHelper } from "../utils/passwordHelper.js";
9
+ import { TokenHelper } from "../utils/tokenHelper.js";
10
+ /**
11
+ * Implementation of the authentication component using entity storage.
12
+ */
13
+ export class EntityStorageAuthenticationService {
14
+ /**
15
+ * Runtime name for the class.
16
+ */
17
+ static CLASS_NAME = "EntityStorageAuthenticationService";
18
+ /**
19
+ * Default TTL in minutes.
20
+ * @internal
21
+ */
22
+ static _DEFAULT_TTL_MINUTES = 60;
23
+ /**
24
+ * Default maximum login attempts in a rate window.
25
+ * @internal
26
+ */
27
+ static _DEFAULT_LOGIN_RATE_MAX_ATTEMPTS = 5;
28
+ /**
29
+ * Default login rate window in minutes.
30
+ * @internal
31
+ */
32
+ static _DEFAULT_LOGIN_RATE_WINDOW_MINUTES = 15;
33
+ /**
34
+ * Default maximum password change attempts in a rate window.
35
+ * @internal
36
+ */
37
+ static _DEFAULT_PASSWORD_CHANGE_RATE_MAX_ATTEMPTS = 5;
38
+ /**
39
+ * Default password change rate window in minutes.
40
+ * @internal
41
+ */
42
+ static _DEFAULT_PASSWORD_CHANGE_RATE_WINDOW_MINUTES = 15;
43
+ /**
44
+ * Default maximum token refresh attempts in a rate window.
45
+ * @internal
46
+ */
47
+ static _DEFAULT_TOKEN_REFRESH_RATE_MAX_ATTEMPTS = 30;
48
+ /**
49
+ * Default token refresh rate window in minutes.
50
+ * @internal
51
+ */
52
+ static _DEFAULT_TOKEN_REFRESH_RATE_WINDOW_MINUTES = 60;
53
+ /**
54
+ * The audit service.
55
+ * @internal
56
+ */
57
+ _authenticationAuditService;
58
+ /**
59
+ * The rate service.
60
+ * @internal
61
+ */
62
+ _authenticationRateService;
63
+ /**
64
+ * The entity storage for users.
65
+ * @internal
66
+ */
67
+ _userEntityStorage;
68
+ /**
69
+ * The vault for the keys.
70
+ * @internal
71
+ */
72
+ _vaultConnector;
73
+ /**
74
+ * The name of the key to retrieve from the vault for signing JWT.
75
+ * @internal
76
+ */
77
+ _signingKeyName;
78
+ /**
79
+ * The default TTL for the token.
80
+ * @internal
81
+ */
82
+ _defaultTtlMinutes;
83
+ /**
84
+ * The minimum password length for validation.
85
+ * @internal
86
+ */
87
+ _minPasswordLength;
88
+ /**
89
+ * Rate limit configuration for login failures.
90
+ * @internal
91
+ */
92
+ _loginRateLimit;
93
+ /**
94
+ * Rate limit configuration for password changes.
95
+ * @internal
96
+ */
97
+ _passwordChangeRateLimit;
98
+ /**
99
+ * Rate limit configuration for token refresh.
100
+ * @internal
101
+ */
102
+ _tokenRefreshRateLimit;
103
+ /**
104
+ * The component to retrieve tenant information.
105
+ * @internal
106
+ */
107
+ _tenantAdminComponent;
108
+ /**
109
+ * The node identity.
110
+ * @internal
111
+ */
112
+ _nodeId;
113
+ /**
114
+ * Create a new instance of EntityStorageAuthentication.
115
+ * @param options The dependencies for the identity connector.
116
+ */
117
+ constructor(options) {
118
+ this._userEntityStorage = EntityStorageConnectorFactory.get(options?.userEntityStorageType ?? "authentication-user");
119
+ this._vaultConnector = VaultConnectorFactory.get(options?.vaultConnectorType ?? "vault");
120
+ this._authenticationAuditService = ComponentFactory.getIfExists(options?.authenticationAuditServiceType ?? "authentication-audit");
121
+ this._authenticationRateService = ComponentFactory.get(options?.authenticationRateServiceType ?? "authentication-rate");
122
+ this._tenantAdminComponent = ComponentFactory.getIfExists(options?.tenantAdminComponentType ?? "tenant-admin");
123
+ this._signingKeyName = options?.config?.signingKeyName ?? "auth-signing";
124
+ this._defaultTtlMinutes =
125
+ options?.config?.defaultTtlMinutes ?? EntityStorageAuthenticationService._DEFAULT_TTL_MINUTES;
126
+ this._minPasswordLength = options?.config?.minPasswordLength;
127
+ this._loginRateLimit = {
128
+ maxAttempts: options?.config?.loginRateLimit?.maxAttempts ??
129
+ EntityStorageAuthenticationService._DEFAULT_LOGIN_RATE_MAX_ATTEMPTS,
130
+ windowMinutes: options?.config?.loginRateLimit?.windowMinutes ??
131
+ EntityStorageAuthenticationService._DEFAULT_LOGIN_RATE_WINDOW_MINUTES
132
+ };
133
+ this._passwordChangeRateLimit = {
134
+ maxAttempts: options?.config?.passwordChangeRateLimit?.maxAttempts ??
135
+ EntityStorageAuthenticationService._DEFAULT_PASSWORD_CHANGE_RATE_MAX_ATTEMPTS,
136
+ windowMinutes: options?.config?.passwordChangeRateLimit?.windowMinutes ??
137
+ EntityStorageAuthenticationService._DEFAULT_PASSWORD_CHANGE_RATE_WINDOW_MINUTES
138
+ };
139
+ this._tokenRefreshRateLimit = {
140
+ maxAttempts: options?.config?.tokenRefreshRateLimit?.maxAttempts ??
141
+ EntityStorageAuthenticationService._DEFAULT_TOKEN_REFRESH_RATE_MAX_ATTEMPTS,
142
+ windowMinutes: options?.config?.tokenRefreshRateLimit?.windowMinutes ??
143
+ EntityStorageAuthenticationService._DEFAULT_TOKEN_REFRESH_RATE_WINDOW_MINUTES
144
+ };
145
+ }
146
+ /**
147
+ * Returns the class name of the component.
148
+ * @returns The class name of the component.
149
+ */
150
+ className() {
151
+ return EntityStorageAuthenticationService.CLASS_NAME;
152
+ }
153
+ /**
154
+ * The service needs to be started when the application is initialized.
155
+ * @param nodeLoggingComponentType The node logging component type.
156
+ * @returns A promise that resolves when rate-limit actions have been registered and the node identity cached.
157
+ */
158
+ async start(nodeLoggingComponentType) {
159
+ const contextIds = await ContextIdStore.getContextIds();
160
+ ContextIdHelper.guard(contextIds, ContextIdKeys.Node);
161
+ this._nodeId = contextIds[ContextIdKeys.Node];
162
+ await this._authenticationRateService.registerAction("login", this._loginRateLimit);
163
+ await this._authenticationRateService.registerAction("password-change", this._passwordChangeRateLimit);
164
+ await this._authenticationRateService.registerAction("token-refresh", this._tokenRefreshRateLimit);
165
+ }
166
+ /**
167
+ * The component needs to be stopped when the node is closed.
168
+ * @param nodeLoggingComponentType The node logging component type.
169
+ * @returns A promise that resolves when all rate-limit actions have been unregistered.
170
+ */
171
+ async stop(nodeLoggingComponentType) {
172
+ await this._authenticationRateService.unregisterAction("login");
173
+ await this._authenticationRateService.unregisterAction("password-change");
174
+ await this._authenticationRateService.unregisterAction("token-refresh");
175
+ }
176
+ /**
177
+ * Perform a login for the user.
178
+ * @param email The email address for the user.
179
+ * @param password The password for the user.
180
+ * @returns The authentication token for the user, if it uses a mechanism with public access.
181
+ */
182
+ async login(email, password) {
183
+ Guards.stringValue(EntityStorageAuthenticationService.CLASS_NAME, "email", email);
184
+ Guards.stringValue(EntityStorageAuthenticationService.CLASS_NAME, "password", password);
185
+ if (!Is.stringValue(this._nodeId)) {
186
+ throw new GeneralError(EntityStorageAuthenticationService.CLASS_NAME, "nodeIdNotSet");
187
+ }
188
+ let loginUser;
189
+ let loginTenantId;
190
+ let tokenAndExpiry;
191
+ try {
192
+ await this._authenticationRateService.check("login", email);
193
+ const user = await this._userEntityStorage.get(email);
194
+ if (!user) {
195
+ throw new GeneralError(EntityStorageAuthenticationService.CLASS_NAME, "userNotFound");
196
+ }
197
+ const saltBytes = Converter.base64ToBytes(user.salt);
198
+ const passwordBytes = Converter.utf8ToBytes(password);
199
+ const hashedPassword = await PasswordGenerator.hashPassword(passwordBytes, saltBytes);
200
+ if (!PasswordValidator.comparePasswordHashes(hashedPassword, user.password)) {
201
+ throw new GeneralError(EntityStorageAuthenticationService.CLASS_NAME, "passwordMismatch");
202
+ }
203
+ // This might be undefined if the login is performed in a single tenant context
204
+ // it is verified during the token processing, tenant id will be matched against
205
+ // the context
206
+ const contextIds = await ContextIdStore.getContextIds();
207
+ loginTenantId = contextIds?.[ContextIdKeys.Tenant];
208
+ tokenAndExpiry = await TokenHelper.createToken(this._vaultConnector, this._nodeId, this._signingKeyName, user.identity, user.organization, loginTenantId, this._defaultTtlMinutes, user.scope, user.passwordVersion ?? 0);
209
+ loginUser = user;
210
+ }
211
+ catch (error) {
212
+ await this._authenticationAuditService?.create({
213
+ actorId: email,
214
+ event: AuthAuditEvent.LoginFailure
215
+ });
216
+ throw new UnauthorizedError(EntityStorageAuthenticationService.CLASS_NAME, "loginFailed", undefined, error);
217
+ }
218
+ await this._authenticationRateService.clear("login", email);
219
+ await this._authenticationAuditService?.create({
220
+ actorId: email,
221
+ event: AuthAuditEvent.LoginSuccess,
222
+ data: {
223
+ userIdentity: loginUser.identity,
224
+ organizationIdentity: loginUser.organization,
225
+ tenantId: loginTenantId,
226
+ scope: ScopeHelper.toArray(loginUser.scope)
227
+ }
228
+ });
229
+ return tokenAndExpiry;
230
+ }
231
+ /**
232
+ * Logout the current user.
233
+ * @param token The token to logout, if it uses a mechanism with public access.
234
+ * @returns A promise that resolves when the logout audit entry has been recorded.
235
+ */
236
+ async logout(token) {
237
+ // Nothing to do here, as we are stateless.
238
+ // The cookie will be revoked by the REST route handling
239
+ const contextIds = await ContextIdStore.getContextIds();
240
+ const identifier = contextIds?.[ContextIdKeys.User];
241
+ if (Is.stringValue(identifier)) {
242
+ await this._authenticationAuditService?.create({
243
+ actorId: identifier,
244
+ event: AuthAuditEvent.Logout
245
+ });
246
+ }
247
+ }
248
+ /**
249
+ * Refresh the token.
250
+ * @param token The token to refresh, if it uses a mechanism with public access.
251
+ * @returns The refreshed token, if it uses a mechanism with public access.
252
+ */
253
+ async refresh(token) {
254
+ if (!Is.stringValue(this._nodeId)) {
255
+ throw new GeneralError(EntityStorageAuthenticationService.CLASS_NAME, "nodeIdNotSet");
256
+ }
257
+ let refreshPasswordVersion;
258
+ let refreshScope;
259
+ let tenantId;
260
+ // If the verify fails on the current token then it will throw an exception.
261
+ const headerAndPayload = await TokenHelper.verify(this._vaultConnector, this._nodeId, this._signingKeyName, token, undefined, async (sub, org, tid, passwordVersion) => {
262
+ const validParts = [];
263
+ tenantId = tid;
264
+ if (Is.stringValue(tenantId)) {
265
+ const tenant = await this._tenantAdminComponent?.get(tenantId);
266
+ if (tenant?.id === tenantId) {
267
+ validParts.push("tenant");
268
+ }
269
+ }
270
+ // We use the tenant id from the token, if the user is not in that
271
+ // partition then the get will fail. Only override the tenant key when
272
+ // tid is present - setting it to undefined would drop the active partition.
273
+ const baseContext = (await ContextIdStore.getContextIds()) ?? {};
274
+ const contextIdsForUserLookup = Is.stringValue(tid)
275
+ ? { ...baseContext, [ContextIdKeys.Tenant]: tid }
276
+ : baseContext;
277
+ // Wrap the user lookup in the request context so partitioned storage uses the correct tenant.
278
+ const user = await ContextIdStore.run(contextIdsForUserLookup, async () => this._userEntityStorage.get(sub, "identity"));
279
+ refreshPasswordVersion = user?.passwordVersion;
280
+ // The scope is read from storage rather than copied from the token being refreshed,
281
+ // so a scope change reaches a client that keeps refreshing.
282
+ refreshScope = user?.scope;
283
+ if (user?.identity === sub && (passwordVersion ?? 0) === (refreshPasswordVersion ?? 0)) {
284
+ validParts.push("user");
285
+ }
286
+ if (user?.organization === org) {
287
+ validParts.push("organization");
288
+ }
289
+ return validParts;
290
+ });
291
+ const refreshSub = headerAndPayload.payload.sub ?? "";
292
+ await this._authenticationRateService.check("token-refresh", refreshSub);
293
+ const payloadOrg = Coerce.string(headerAndPayload.payload.org);
294
+ const refreshTokenAndExpiry = await TokenHelper.createToken(this._vaultConnector, this._nodeId, this._signingKeyName, refreshSub, payloadOrg, tenantId, this._defaultTtlMinutes, refreshScope, refreshPasswordVersion ?? 0);
295
+ await this._authenticationAuditService?.create({
296
+ actorId: refreshSub,
297
+ event: AuthAuditEvent.TokenRefreshed,
298
+ data: {
299
+ organizationIdentity: payloadOrg,
300
+ tenantId,
301
+ scope: ScopeHelper.toArray(refreshScope),
302
+ version: refreshPasswordVersion ?? 0
303
+ }
304
+ });
305
+ return refreshTokenAndExpiry;
306
+ }
307
+ /**
308
+ * Update the user's password.
309
+ * @param currentPassword The current password for the user.
310
+ * @param newPassword The new password for the user.
311
+ * @returns A promise that resolves when the password has been updated and the rate limit cleared.
312
+ */
313
+ async updatePassword(currentPassword, newPassword) {
314
+ const contextIds = await ContextIdStore.getContextIds();
315
+ ContextIdHelper.guard(contextIds, ContextIdKeys.User);
316
+ const userIdentity = contextIds[ContextIdKeys.User];
317
+ await this._authenticationRateService.check("password-change", userIdentity);
318
+ const user = await this._userEntityStorage.get(userIdentity, "identity");
319
+ if (!Is.object(user)) {
320
+ throw new NotFoundError(EntityStorageAuthenticationService.CLASS_NAME, "userNotFound", userIdentity);
321
+ }
322
+ await PasswordHelper.updatePassword(this._userEntityStorage, this._authenticationAuditService, user, newPassword, currentPassword, this._minPasswordLength);
323
+ await this._authenticationRateService.clear("password-change", userIdentity);
324
+ }
325
+ }
326
+ //# sourceMappingURL=entityStorageAuthenticationService.js.map