@twin.org/api-auth-entity-storage-service 0.0.3-next.51 → 0.0.3-next.53

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.
@@ -1 +1 @@
1
- {"version":3,"file":"IEntityStorageAuthenticationRateServiceConstructorOptions.js","sourceRoot":"","sources":["../../../src/models/IEntityStorageAuthenticationRateServiceConstructorOptions.ts"],"names":[],"mappings":"","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type { IEntityStorageAuthenticationRateServiceConfig } from \"./IEntityStorageAuthenticationRateServiceConfig.js\";\n\n/**\n * Options for the EntityStorageAuthenticationRateService constructor.\n */\nexport interface IEntityStorageAuthenticationRateServiceConstructorOptions {\n\t/**\n\t * The entity storage for authentication rate entries.\n\t * @default authentication-rate-entry\n\t */\n\tauthenticationRateEntryStorageType?: string;\n\n\t/**\n\t * The task scheduler component type.\n\t * @default task-scheduler\n\t */\n\ttaskSchedulerComponentType?: string;\n\n\t/**\n\t * The configuration for the authentication rate service.\n\t */\n\tconfig?: IEntityStorageAuthenticationRateServiceConfig;\n}\n"]}
1
+ {"version":3,"file":"IEntityStorageAuthenticationRateServiceConstructorOptions.js","sourceRoot":"","sources":["../../../src/models/IEntityStorageAuthenticationRateServiceConstructorOptions.ts"],"names":[],"mappings":"","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type { IEntityStorageAuthenticationRateServiceConfig } from \"./IEntityStorageAuthenticationRateServiceConfig.js\";\n\n/**\n * Options for the EntityStorageAuthenticationRateService constructor.\n */\nexport interface IEntityStorageAuthenticationRateServiceConstructorOptions {\n\t/**\n\t * The entity storage for authentication rate entries.\n\t * @default authentication-rate-entry\n\t */\n\tauthenticationRateEntryStorageType?: string;\n\n\t/**\n\t * The task scheduler component type.\n\t * @default task-scheduler\n\t */\n\ttaskSchedulerComponentType?: string;\n\n\t/**\n\t * The platform component type, used to run the periodic cleanup per tenant.\n\t * @default platform\n\t */\n\tplatformComponentType?: string;\n\n\t/**\n\t * The configuration for the authentication rate service.\n\t */\n\tconfig?: IEntityStorageAuthenticationRateServiceConfig;\n}\n"]}
@@ -2,7 +2,7 @@
2
2
  // SPDX-License-Identifier: Apache-2.0.
3
3
  import { HttpContextIdKeys, HttpErrorHelper } from "@twin.org/api-models";
4
4
  import { ContextIdHelper, ContextIdKeys, ContextIdStore } from "@twin.org/context";
5
- import { BaseError, ComponentFactory, Is } from "@twin.org/core";
5
+ import { BaseError, ComponentFactory, GeneralError, Is } from "@twin.org/core";
6
6
  import { EntityStorageConnectorFactory } from "@twin.org/entity-storage-models";
7
7
  import { VaultConnectorFactory } from "@twin.org/vault-models";
8
8
  import { CookieHelper, HeaderTypes, HttpStatusCode } from "@twin.org/web";
@@ -96,12 +96,15 @@ export class AuthHeaderProcessor {
96
96
  async pre(request, response, route, contextIds, processorState) {
97
97
  if (!Is.empty(route) && !(route.skipAuth ?? false)) {
98
98
  try {
99
+ if (!Is.stringValue(this._nodeId)) {
100
+ throw new GeneralError(AuthHeaderProcessor.CLASS_NAME, "nodeIdNotSet");
101
+ }
99
102
  const tokenAndLocation = TokenHelper.extractTokenFromHeaders(request.headers, this._cookieName);
100
103
  let user;
101
104
  let tenantId;
102
105
  let tenantOrganizationId;
103
106
  let tenantPublicOrigin;
104
- await TokenHelper.verify(this._vaultConnector, `${this._nodeId}/${this._signingKeyName}`, tokenAndLocation?.token, route.requiredScope, async (sub, org, tid, passwordVersion) => {
107
+ await TokenHelper.verify(this._vaultConnector, this._nodeId, this._signingKeyName, tokenAndLocation?.token, route.requiredScope, async (sub, org, tid, passwordVersion) => {
105
108
  const validParts = [];
106
109
  tenantId = tid;
107
110
  if (Is.stringValue(tenantId)) {
@@ -1 +1 @@
1
- {"version":3,"file":"authHeaderProcessor.js","sourceRoot":"","sources":["../../../src/processors/authHeaderProcessor.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport {\n\tHttpContextIdKeys,\n\tHttpErrorHelper,\n\ttype IBaseRoute,\n\ttype IBaseRouteProcessor,\n\ttype IHttpResponse,\n\ttype IHttpServerRequest,\n\ttype ITenantAdminComponent\n} from \"@twin.org/api-models\";\nimport {\n\tContextIdHelper,\n\tContextIdKeys,\n\tContextIdStore,\n\ttype IContextIds\n} from \"@twin.org/context\";\nimport { BaseError, ComponentFactory, Is } from \"@twin.org/core\";\nimport {\n\tEntityStorageConnectorFactory,\n\ttype IEntityStorageConnector\n} from \"@twin.org/entity-storage-models\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { VaultConnectorFactory, type IVaultConnector } from \"@twin.org/vault-models\";\nimport { CookieHelper, HeaderTypes, HttpStatusCode } from \"@twin.org/web\";\nimport type { AuthenticationUser } from \"../entities/authenticationUser.js\";\nimport type { IAuthHeaderProcessorConstructorOptions } from \"../models/IAuthHeaderProcessorConstructorOptions.js\";\nimport { TokenHelper } from \"../utils/tokenHelper.js\";\n\n/**\n * Handle a JWT token in the authorization header or cookies and validate it to populate request context identity.\n */\nexport class AuthHeaderProcessor implements IBaseRouteProcessor {\n\t/**\n\t * The default name for the access token as a cookie.\n\t * @internal\n\t */\n\tpublic static readonly DEFAULT_COOKIE_NAME: string = \"access_token\";\n\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<AuthHeaderProcessor>();\n\n\t/**\n\t * The vault for the keys.\n\t * @internal\n\t */\n\tprivate readonly _vaultConnector: IVaultConnector;\n\n\t/**\n\t * The component to retrieve tenant information.\n\t * @internal\n\t */\n\tprivate readonly _tenantAdminComponent?: ITenantAdminComponent;\n\n\t/**\n\t * The entity storage for users.\n\t * @internal\n\t */\n\tprivate readonly _userEntityStorage: IEntityStorageConnector<AuthenticationUser>;\n\n\t/**\n\t * The name of the key to retrieve from the vault for signing JWT.\n\t * @internal\n\t */\n\tprivate readonly _signingKeyName: string;\n\n\t/**\n\t * The name of the cookie to use for the token.\n\t * @internal\n\t */\n\tprivate readonly _cookieName: string;\n\n\t/**\n\t * The node identity.\n\t * @internal\n\t */\n\tprivate _nodeId?: string;\n\n\t/**\n\t * The organization ID for the single-tenant node, cached at startup.\n\t * @internal\n\t */\n\tprivate _nodeOrganizationId?: string;\n\n\t/**\n\t * Create a new instance of AuthHeaderProcessor.\n\t * @param options Options for the processor.\n\t */\n\tconstructor(options?: IAuthHeaderProcessorConstructorOptions) {\n\t\tthis._vaultConnector = VaultConnectorFactory.get(options?.vaultConnectorType ?? \"vault\");\n\t\tthis._userEntityStorage = EntityStorageConnectorFactory.get(\n\t\t\toptions?.userEntityStorageType ?? \"authentication-user\"\n\t\t);\n\t\tthis._tenantAdminComponent = ComponentFactory.getIfExists<ITenantAdminComponent>(\n\t\t\toptions?.tenantAdminComponentType ?? \"tenant-admin\"\n\t\t);\n\n\t\tthis._signingKeyName = options?.config?.signingKeyName ?? \"auth-signing\";\n\t\tthis._cookieName = options?.config?.cookieName ?? AuthHeaderProcessor.DEFAULT_COOKIE_NAME;\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 AuthHeaderProcessor.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 node identity and organization ID have been cached.\n\t */\n\tpublic async start(nodeLoggingComponentType?: string): Promise<void> {\n\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\tContextIdHelper.guard(contextIds, ContextIdKeys.Node);\n\t\tthis._nodeId = contextIds[ContextIdKeys.Node];\n\t\tthis._nodeOrganizationId = contextIds[ContextIdKeys.Organization];\n\t}\n\n\t/**\n\t * Pre process the REST request for the specified route.\n\t * @param request The incoming request.\n\t * @param response The outgoing response.\n\t * @param route The route to process.\n\t * @param contextIds The context IDs of the request.\n\t * @param processorState The state handed through the processors.\n\t * @returns A promise that resolves when the JWT has been verified and the context populated, or an error response set.\n\t */\n\tpublic async pre(\n\t\trequest: IHttpServerRequest,\n\t\tresponse: IHttpResponse,\n\t\troute: IBaseRoute | undefined,\n\t\tcontextIds: IContextIds,\n\t\tprocessorState: { [id: string]: unknown }\n\t): Promise<void> {\n\t\tif (!Is.empty(route) && !(route.skipAuth ?? false)) {\n\t\t\ttry {\n\t\t\t\tconst tokenAndLocation = TokenHelper.extractTokenFromHeaders(\n\t\t\t\t\trequest.headers,\n\t\t\t\t\tthis._cookieName\n\t\t\t\t);\n\n\t\t\t\tlet user: AuthenticationUser | undefined;\n\t\t\t\tlet tenantId: string | undefined;\n\t\t\t\tlet tenantOrganizationId: string | undefined;\n\t\t\t\tlet tenantPublicOrigin: string | undefined;\n\n\t\t\t\tawait TokenHelper.verify(\n\t\t\t\t\tthis._vaultConnector,\n\t\t\t\t\t`${this._nodeId}/${this._signingKeyName}`,\n\t\t\t\t\ttokenAndLocation?.token,\n\t\t\t\t\troute.requiredScope,\n\t\t\t\t\tasync (\n\t\t\t\t\t\tsub: string,\n\t\t\t\t\t\torg: string,\n\t\t\t\t\t\ttid: string | undefined,\n\t\t\t\t\t\tpasswordVersion: number | undefined\n\t\t\t\t\t) => {\n\t\t\t\t\t\tconst validParts = [];\n\n\t\t\t\t\t\ttenantId = tid;\n\n\t\t\t\t\t\tif (Is.stringValue(tenantId)) {\n\t\t\t\t\t\t\tconst tenant = await this._tenantAdminComponent?.get(tenantId);\n\t\t\t\t\t\t\tif (tenant?.id === tenantId) {\n\t\t\t\t\t\t\t\tvalidParts.push(\"tenant\");\n\t\t\t\t\t\t\t\ttenantOrganizationId = tenant.organizationId;\n\t\t\t\t\t\t\t\ttenantPublicOrigin = tenant.publicOrigin;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// We use the tenant id from the token, if the user is not in that\n\t\t\t\t\t\t// partition then the get will fail\n\t\t\t\t\t\tconst contextIdsForUserLookup = {\n\t\t\t\t\t\t\t...contextIds,\n\t\t\t\t\t\t\t[ContextIdKeys.Tenant]: tid\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\t// Wrap the user lookup in the request context so partitioned storage uses the correct tenant.\n\t\t\t\t\t\tuser = await ContextIdStore.run(contextIdsForUserLookup, async () =>\n\t\t\t\t\t\t\tthis._userEntityStorage.get(sub, \"identity\")\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\tif (user?.identity === sub && (passwordVersion ?? 0) === (user.passwordVersion ?? 0)) {\n\t\t\t\t\t\t\tvalidParts.push(\"user\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (user?.organization === org) {\n\t\t\t\t\t\t\tvalidParts.push(\"organization\");\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn validParts;\n\t\t\t\t\t}\n\t\t\t\t);\n\n\t\t\t\tcontextIds[ContextIdKeys.Tenant] = tenantId;\n\t\t\t\t// In a multi-tenant environment the tenant organization ID is authoritative,\n\t\t\t\t// in a single-tenant environment we fall back to the node organization ID.\n\t\t\t\tcontextIds[ContextIdKeys.Organization] = tenantOrganizationId ?? this._nodeOrganizationId;\n\t\t\t\tcontextIds[ContextIdKeys.User] = user?.identity;\n\t\t\t\tcontextIds[ContextIdKeys.UserOrganization] = user?.organization;\n\n\t\t\t\t// If the tenant has a custom public origin, we set it in the context for downstream processors to use.\n\t\t\t\tif (Is.stringValue(tenantPublicOrigin)) {\n\t\t\t\t\tcontextIds[HttpContextIdKeys.PublicOrigin] = tenantPublicOrigin;\n\t\t\t\t}\n\n\t\t\t\tprocessorState.authToken = tokenAndLocation?.token;\n\t\t\t\tprocessorState.authTokenLocation = tokenAndLocation?.location;\n\t\t\t} catch (err) {\n\t\t\t\tconst error = BaseError.fromError(err);\n\t\t\t\tHttpErrorHelper.buildResponse(response, error, HttpStatusCode.unauthorized);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Post process the REST request for the specified route.\n\t * @param request The incoming request.\n\t * @param response The outgoing response.\n\t * @param route The route to process.\n\t * @param contextIds The context IDs of the request.\n\t * @param processorState The state handed through the processors.\n\t * @returns A promise that resolves when the Set-Cookie header has been applied to the response if required.\n\t */\n\tpublic async post(\n\t\trequest: IHttpServerRequest,\n\t\tresponse: IHttpResponse,\n\t\troute: IBaseRoute | undefined,\n\t\tcontextIds: IContextIds,\n\t\tprocessorState: { [id: string]: unknown }\n\t): Promise<void> {\n\t\tconst responseAuthOperation = processorState?.authOperation;\n\t\tconst responseAuthToken = processorState?.authToken;\n\n\t\t// We don't populate the cookie if the incoming request was from an authorization header.\n\t\tif (\n\t\t\t!Is.empty(route) &&\n\t\t\tIs.stringValue(responseAuthOperation) &&\n\t\t\tprocessorState.authTokenLocation !== \"authorization\"\n\t\t) {\n\t\t\tif (\n\t\t\t\t(responseAuthOperation === \"login\" || responseAuthOperation === \"refresh\") &&\n\t\t\t\tIs.stringValue(responseAuthToken)\n\t\t\t) {\n\t\t\t\tresponse.headers ??= {};\n\t\t\t\tresponse.headers[HeaderTypes.SetCookie] = CookieHelper.createCookie(\n\t\t\t\t\tthis._cookieName,\n\t\t\t\t\tresponseAuthToken,\n\t\t\t\t\t{\n\t\t\t\t\t\tsecure: true,\n\t\t\t\t\t\thttpOnly: true,\n\t\t\t\t\t\tsameSite: \"None\",\n\t\t\t\t\t\tpath: \"/\"\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t} else if (responseAuthOperation === \"logout\") {\n\t\t\t\tresponse.headers ??= {};\n\t\t\t\tresponse.headers[HeaderTypes.SetCookie] = CookieHelper.deleteCookie(this._cookieName, {\n\t\t\t\t\tsecure: true,\n\t\t\t\t\thttpOnly: true,\n\t\t\t\t\tsameSite: \"None\",\n\t\t\t\t\tpath: \"/\"\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"authHeaderProcessor.js","sourceRoot":"","sources":["../../../src/processors/authHeaderProcessor.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport {\n\tHttpContextIdKeys,\n\tHttpErrorHelper,\n\ttype IBaseRoute,\n\ttype IBaseRouteProcessor,\n\ttype IHttpResponse,\n\ttype IHttpServerRequest,\n\ttype ITenantAdminComponent\n} from \"@twin.org/api-models\";\nimport {\n\tContextIdHelper,\n\tContextIdKeys,\n\tContextIdStore,\n\ttype IContextIds\n} from \"@twin.org/context\";\nimport { BaseError, ComponentFactory, GeneralError, Is } from \"@twin.org/core\";\nimport {\n\tEntityStorageConnectorFactory,\n\ttype IEntityStorageConnector\n} from \"@twin.org/entity-storage-models\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { VaultConnectorFactory, type IVaultConnector } from \"@twin.org/vault-models\";\nimport { CookieHelper, HeaderTypes, HttpStatusCode } from \"@twin.org/web\";\nimport type { AuthenticationUser } from \"../entities/authenticationUser.js\";\nimport type { IAuthHeaderProcessorConstructorOptions } from \"../models/IAuthHeaderProcessorConstructorOptions.js\";\nimport { TokenHelper } from \"../utils/tokenHelper.js\";\n\n/**\n * Handle a JWT token in the authorization header or cookies and validate it to populate request context identity.\n */\nexport class AuthHeaderProcessor implements IBaseRouteProcessor {\n\t/**\n\t * The default name for the access token as a cookie.\n\t * @internal\n\t */\n\tpublic static readonly DEFAULT_COOKIE_NAME: string = \"access_token\";\n\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<AuthHeaderProcessor>();\n\n\t/**\n\t * The vault for the keys.\n\t * @internal\n\t */\n\tprivate readonly _vaultConnector: IVaultConnector;\n\n\t/**\n\t * The component to retrieve tenant information.\n\t * @internal\n\t */\n\tprivate readonly _tenantAdminComponent?: ITenantAdminComponent;\n\n\t/**\n\t * The entity storage for users.\n\t * @internal\n\t */\n\tprivate readonly _userEntityStorage: IEntityStorageConnector<AuthenticationUser>;\n\n\t/**\n\t * The name of the key to retrieve from the vault for signing JWT.\n\t * @internal\n\t */\n\tprivate readonly _signingKeyName: string;\n\n\t/**\n\t * The name of the cookie to use for the token.\n\t * @internal\n\t */\n\tprivate readonly _cookieName: string;\n\n\t/**\n\t * The node identity.\n\t * @internal\n\t */\n\tprivate _nodeId?: string;\n\n\t/**\n\t * The organization ID for the single-tenant node, cached at startup.\n\t * @internal\n\t */\n\tprivate _nodeOrganizationId?: string;\n\n\t/**\n\t * Create a new instance of AuthHeaderProcessor.\n\t * @param options Options for the processor.\n\t */\n\tconstructor(options?: IAuthHeaderProcessorConstructorOptions) {\n\t\tthis._vaultConnector = VaultConnectorFactory.get(options?.vaultConnectorType ?? \"vault\");\n\t\tthis._userEntityStorage = EntityStorageConnectorFactory.get(\n\t\t\toptions?.userEntityStorageType ?? \"authentication-user\"\n\t\t);\n\t\tthis._tenantAdminComponent = ComponentFactory.getIfExists<ITenantAdminComponent>(\n\t\t\toptions?.tenantAdminComponentType ?? \"tenant-admin\"\n\t\t);\n\n\t\tthis._signingKeyName = options?.config?.signingKeyName ?? \"auth-signing\";\n\t\tthis._cookieName = options?.config?.cookieName ?? AuthHeaderProcessor.DEFAULT_COOKIE_NAME;\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 AuthHeaderProcessor.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 node identity and organization ID have been cached.\n\t */\n\tpublic async start(nodeLoggingComponentType?: string): Promise<void> {\n\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\tContextIdHelper.guard(contextIds, ContextIdKeys.Node);\n\t\tthis._nodeId = contextIds[ContextIdKeys.Node];\n\t\tthis._nodeOrganizationId = contextIds[ContextIdKeys.Organization];\n\t}\n\n\t/**\n\t * Pre process the REST request for the specified route.\n\t * @param request The incoming request.\n\t * @param response The outgoing response.\n\t * @param route The route to process.\n\t * @param contextIds The context IDs of the request.\n\t * @param processorState The state handed through the processors.\n\t * @returns A promise that resolves when the JWT has been verified and the context populated, or an error response set.\n\t */\n\tpublic async pre(\n\t\trequest: IHttpServerRequest,\n\t\tresponse: IHttpResponse,\n\t\troute: IBaseRoute | undefined,\n\t\tcontextIds: IContextIds,\n\t\tprocessorState: { [id: string]: unknown }\n\t): Promise<void> {\n\t\tif (!Is.empty(route) && !(route.skipAuth ?? false)) {\n\t\t\ttry {\n\t\t\t\tif (!Is.stringValue(this._nodeId)) {\n\t\t\t\t\tthrow new GeneralError(AuthHeaderProcessor.CLASS_NAME, \"nodeIdNotSet\");\n\t\t\t\t}\n\n\t\t\t\tconst tokenAndLocation = TokenHelper.extractTokenFromHeaders(\n\t\t\t\t\trequest.headers,\n\t\t\t\t\tthis._cookieName\n\t\t\t\t);\n\n\t\t\t\tlet user: AuthenticationUser | undefined;\n\t\t\t\tlet tenantId: string | undefined;\n\t\t\t\tlet tenantOrganizationId: string | undefined;\n\t\t\t\tlet tenantPublicOrigin: string | undefined;\n\n\t\t\t\tawait TokenHelper.verify(\n\t\t\t\t\tthis._vaultConnector,\n\t\t\t\t\tthis._nodeId,\n\t\t\t\t\tthis._signingKeyName,\n\t\t\t\t\ttokenAndLocation?.token,\n\t\t\t\t\troute.requiredScope,\n\t\t\t\t\tasync (\n\t\t\t\t\t\tsub: string,\n\t\t\t\t\t\torg: string,\n\t\t\t\t\t\ttid: string | undefined,\n\t\t\t\t\t\tpasswordVersion: number | undefined\n\t\t\t\t\t) => {\n\t\t\t\t\t\tconst validParts = [];\n\n\t\t\t\t\t\ttenantId = tid;\n\n\t\t\t\t\t\tif (Is.stringValue(tenantId)) {\n\t\t\t\t\t\t\tconst tenant = await this._tenantAdminComponent?.get(tenantId);\n\t\t\t\t\t\t\tif (tenant?.id === tenantId) {\n\t\t\t\t\t\t\t\tvalidParts.push(\"tenant\");\n\t\t\t\t\t\t\t\ttenantOrganizationId = tenant.organizationId;\n\t\t\t\t\t\t\t\ttenantPublicOrigin = tenant.publicOrigin;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// We use the tenant id from the token, if the user is not in that\n\t\t\t\t\t\t// partition then the get will fail\n\t\t\t\t\t\tconst contextIdsForUserLookup = {\n\t\t\t\t\t\t\t...contextIds,\n\t\t\t\t\t\t\t[ContextIdKeys.Tenant]: tid\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\t// Wrap the user lookup in the request context so partitioned storage uses the correct tenant.\n\t\t\t\t\t\tuser = await ContextIdStore.run(contextIdsForUserLookup, async () =>\n\t\t\t\t\t\t\tthis._userEntityStorage.get(sub, \"identity\")\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\tif (user?.identity === sub && (passwordVersion ?? 0) === (user.passwordVersion ?? 0)) {\n\t\t\t\t\t\t\tvalidParts.push(\"user\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (user?.organization === org) {\n\t\t\t\t\t\t\tvalidParts.push(\"organization\");\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn validParts;\n\t\t\t\t\t}\n\t\t\t\t);\n\n\t\t\t\tcontextIds[ContextIdKeys.Tenant] = tenantId;\n\t\t\t\t// In a multi-tenant environment the tenant organization ID is authoritative,\n\t\t\t\t// in a single-tenant environment we fall back to the node organization ID.\n\t\t\t\tcontextIds[ContextIdKeys.Organization] = tenantOrganizationId ?? this._nodeOrganizationId;\n\t\t\t\tcontextIds[ContextIdKeys.User] = user?.identity;\n\t\t\t\tcontextIds[ContextIdKeys.UserOrganization] = user?.organization;\n\n\t\t\t\t// If the tenant has a custom public origin, we set it in the context for downstream processors to use.\n\t\t\t\tif (Is.stringValue(tenantPublicOrigin)) {\n\t\t\t\t\tcontextIds[HttpContextIdKeys.PublicOrigin] = tenantPublicOrigin;\n\t\t\t\t}\n\n\t\t\t\tprocessorState.authToken = tokenAndLocation?.token;\n\t\t\t\tprocessorState.authTokenLocation = tokenAndLocation?.location;\n\t\t\t} catch (err) {\n\t\t\t\tconst error = BaseError.fromError(err);\n\t\t\t\tHttpErrorHelper.buildResponse(response, error, HttpStatusCode.unauthorized);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Post process the REST request for the specified route.\n\t * @param request The incoming request.\n\t * @param response The outgoing response.\n\t * @param route The route to process.\n\t * @param contextIds The context IDs of the request.\n\t * @param processorState The state handed through the processors.\n\t * @returns A promise that resolves when the Set-Cookie header has been applied to the response if required.\n\t */\n\tpublic async post(\n\t\trequest: IHttpServerRequest,\n\t\tresponse: IHttpResponse,\n\t\troute: IBaseRoute | undefined,\n\t\tcontextIds: IContextIds,\n\t\tprocessorState: { [id: string]: unknown }\n\t): Promise<void> {\n\t\tconst responseAuthOperation = processorState?.authOperation;\n\t\tconst responseAuthToken = processorState?.authToken;\n\n\t\t// We don't populate the cookie if the incoming request was from an authorization header.\n\t\tif (\n\t\t\t!Is.empty(route) &&\n\t\t\tIs.stringValue(responseAuthOperation) &&\n\t\t\tprocessorState.authTokenLocation !== \"authorization\"\n\t\t) {\n\t\t\tif (\n\t\t\t\t(responseAuthOperation === \"login\" || responseAuthOperation === \"refresh\") &&\n\t\t\t\tIs.stringValue(responseAuthToken)\n\t\t\t) {\n\t\t\t\tresponse.headers ??= {};\n\t\t\t\tresponse.headers[HeaderTypes.SetCookie] = CookieHelper.createCookie(\n\t\t\t\t\tthis._cookieName,\n\t\t\t\t\tresponseAuthToken,\n\t\t\t\t\t{\n\t\t\t\t\t\tsecure: true,\n\t\t\t\t\t\thttpOnly: true,\n\t\t\t\t\t\tsameSite: \"None\",\n\t\t\t\t\t\tpath: \"/\"\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t} else if (responseAuthOperation === \"logout\") {\n\t\t\t\tresponse.headers ??= {};\n\t\t\t\tresponse.headers[HeaderTypes.SetCookie] = CookieHelper.deleteCookie(this._cookieName, {\n\t\t\t\t\tsecure: true,\n\t\t\t\t\thttpOnly: true,\n\t\t\t\t\tsameSite: \"None\",\n\t\t\t\t\tpath: \"/\"\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
@@ -46,6 +46,11 @@ export class EntityStorageAuthenticationRateService {
46
46
  * @internal
47
47
  */
48
48
  _cleanupIntervalMinutes;
49
+ /**
50
+ * Platform component used to run the periodic cleanup with the correct tenant context.
51
+ * @internal
52
+ */
53
+ _platformComponent;
49
54
  /**
50
55
  * Create a new instance of EntityStorageAuthenticationRateService.
51
56
  * @param options The constructor options.
@@ -57,6 +62,7 @@ export class EntityStorageAuthenticationRateService {
57
62
  this._cleanupIntervalMinutes =
58
63
  options?.config?.cleanupIntervalMinutes ??
59
64
  EntityStorageAuthenticationRateService._DEFAULT_CLEANUP_INTERVAL_MINUTES;
65
+ this._platformComponent = ComponentFactory.get(options?.platformComponentType ?? "platform");
60
66
  }
61
67
  /**
62
68
  * Register or update rate-limit configuration for an action.
@@ -164,39 +170,44 @@ export class EntityStorageAuthenticationRateService {
164
170
  await this._authenticationRateEntryStorage.remove(compositeId);
165
171
  }
166
172
  /**
167
- * Cleanup expired rate limit entries.
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).
168
175
  * @returns A promise that resolves when all expired entries have been removed from storage.
169
176
  * @internal
170
177
  */
171
178
  async cleanupExpiredEntries() {
172
- const now = Date.now();
173
- for (const action of Object.keys(this._actionConfigs)) {
174
- const actionConfig = this._actionConfigs[action];
175
- const windowMs = actionConfig.windowMinutes * 60 * 1000;
176
- const cutoffIso = new Date(now - windowMs).toISOString();
177
- let cursor;
178
- do {
179
- // Query by composite rate entry id prefix and expiry window for this action.
180
- const result = await this._authenticationRateEntryStorage.query({
181
- conditions: [
182
- {
183
- property: "id",
184
- value: `|${action}|`,
185
- comparison: ComparisonOperator.Includes
186
- },
187
- {
188
- property: "dateModified",
189
- value: cutoffIso,
190
- comparison: ComparisonOperator.LessThanOrEqual
191
- }
192
- ]
193
- }, undefined, undefined, cursor, EntityStorageAuthenticationRateService._CLEANUP_PAGE_SIZE);
194
- for (const entity of result.entities) {
195
- await this._authenticationRateEntryStorage.remove(entity.id);
196
- }
197
- cursor = result.cursor;
198
- } while (!Is.empty(cursor));
199
- }
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
+ });
200
211
  }
201
212
  }
202
213
  //# sourceMappingURL=entityStorageAuthenticationRateService.js.map
@@ -1 +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 \"@twin.org/api-auth-entity-storage-models\";\nimport { TooManyRequestsError } from \"@twin.org/api-models\";\nimport type { ITaskSchedulerComponent } from \"@twin.org/background-task-models\";\nimport { ComponentFactory, Converter, GeneralError, Guards, Is } from \"@twin.org/core\";\nimport { Sha256 } from \"@twin.org/crypto\";\nimport { ComparisonOperator } from \"@twin.org/entity\";\nimport {\n\tEntityStorageConnectorFactory,\n\ttype IEntityStorageConnector\n} from \"@twin.org/entity-storage-models\";\nimport { nameof } from \"@twin.org/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 * 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}\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.\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\tconst now = Date.now();\n\n\t\tfor (const action of Object.keys(this._actionConfigs)) {\n\t\t\tconst actionConfig = this._actionConfigs[action];\n\t\t\tconst windowMs = actionConfig.windowMinutes * 60 * 1000;\n\t\t\tconst cutoffIso = new Date(now - windowMs).toISOString();\n\t\t\tlet cursor: string | undefined;\n\n\t\t\tdo {\n\t\t\t\t// Query by composite rate entry id prefix and expiry window for this action.\n\t\t\t\tconst result = await this._authenticationRateEntryStorage.query(\n\t\t\t\t\t{\n\t\t\t\t\t\tconditions: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tproperty: \"id\",\n\t\t\t\t\t\t\t\tvalue: `|${action}|`,\n\t\t\t\t\t\t\t\tcomparison: ComparisonOperator.Includes\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tproperty: \"dateModified\",\n\t\t\t\t\t\t\t\tvalue: cutoffIso,\n\t\t\t\t\t\t\t\tcomparison: ComparisonOperator.LessThanOrEqual\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t},\n\t\t\t\t\tundefined,\n\t\t\t\t\tundefined,\n\t\t\t\t\tcursor,\n\t\t\t\t\tEntityStorageAuthenticationRateService._CLEANUP_PAGE_SIZE\n\t\t\t\t);\n\n\t\t\t\tfor (const entity of result.entities as AuthenticationRateEntry[]) {\n\t\t\t\t\tawait this._authenticationRateEntryStorage.remove(entity.id);\n\t\t\t\t}\n\n\t\t\t\tcursor = result.cursor;\n\t\t\t} while (!Is.empty(cursor));\n\t\t}\n\t}\n}\n"]}
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 \"@twin.org/api-auth-entity-storage-models\";\nimport { TooManyRequestsError, type IPlatformComponent } from \"@twin.org/api-models\";\nimport type { ITaskSchedulerComponent } from \"@twin.org/background-task-models\";\nimport { ComponentFactory, Converter, GeneralError, Guards, Is } from \"@twin.org/core\";\nimport { Sha256 } from \"@twin.org/crypto\";\nimport { ComparisonOperator } from \"@twin.org/entity\";\nimport {\n\tEntityStorageConnectorFactory,\n\ttype IEntityStorageConnector\n} from \"@twin.org/entity-storage-models\";\nimport { nameof } from \"@twin.org/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"]}
@@ -181,6 +181,9 @@ export class EntityStorageAuthenticationService {
181
181
  async login(email, password) {
182
182
  Guards.stringValue(EntityStorageAuthenticationService.CLASS_NAME, "email", email);
183
183
  Guards.stringValue(EntityStorageAuthenticationService.CLASS_NAME, "password", password);
184
+ if (!Is.stringValue(this._nodeId)) {
185
+ throw new GeneralError(EntityStorageAuthenticationService.CLASS_NAME, "nodeIdNotSet");
186
+ }
184
187
  let loginUser;
185
188
  let loginTenantId;
186
189
  let tokenAndExpiry;
@@ -201,7 +204,7 @@ export class EntityStorageAuthenticationService {
201
204
  // the context
202
205
  const contextIds = await ContextIdStore.getContextIds();
203
206
  loginTenantId = contextIds?.[ContextIdKeys.Tenant];
204
- tokenAndExpiry = await TokenHelper.createToken(this._vaultConnector, `${this._nodeId}/${this._signingKeyName}`, user.identity, user.organization, loginTenantId, this._defaultTtlMinutes, user.scope, user.passwordVersion ?? 0);
207
+ tokenAndExpiry = await TokenHelper.createToken(this._vaultConnector, this._nodeId, this._signingKeyName, user.identity, user.organization, loginTenantId, this._defaultTtlMinutes, user.scope, user.passwordVersion ?? 0);
205
208
  loginUser = user;
206
209
  }
207
210
  catch (error) {
@@ -247,10 +250,13 @@ export class EntityStorageAuthenticationService {
247
250
  * @returns The refreshed token, if it uses a mechanism with public access.
248
251
  */
249
252
  async refresh(token) {
253
+ if (!Is.stringValue(this._nodeId)) {
254
+ throw new GeneralError(EntityStorageAuthenticationService.CLASS_NAME, "nodeIdNotSet");
255
+ }
250
256
  let refreshPasswordVersion;
251
257
  let tenantId;
252
258
  // If the verify fails on the current token then it will throw an exception.
253
- const headerAndPayload = await TokenHelper.verify(this._vaultConnector, `${this._nodeId}/${this._signingKeyName}`, token, undefined, async (sub, org, tid, passwordVersion) => {
259
+ const headerAndPayload = await TokenHelper.verify(this._vaultConnector, this._nodeId, this._signingKeyName, token, undefined, async (sub, org, tid, passwordVersion) => {
254
260
  const validParts = [];
255
261
  tenantId = tid;
256
262
  if (Is.stringValue(tenantId)) {
@@ -281,7 +287,7 @@ export class EntityStorageAuthenticationService {
281
287
  await this._authenticationRateService.check("token-refresh", refreshSub);
282
288
  const payloadOrg = Coerce.string(headerAndPayload.payload.org);
283
289
  const payloadScope = Coerce.string(headerAndPayload.payload?.scope);
284
- const refreshTokenAndExpiry = await TokenHelper.createToken(this._vaultConnector, `${this._nodeId}/${this._signingKeyName}`, refreshSub, payloadOrg, tenantId, this._defaultTtlMinutes, payloadScope, refreshPasswordVersion ?? 0);
290
+ const refreshTokenAndExpiry = await TokenHelper.createToken(this._vaultConnector, this._nodeId, this._signingKeyName, refreshSub, payloadOrg, tenantId, this._defaultTtlMinutes, payloadScope, refreshPasswordVersion ?? 0);
285
291
  await this._authenticationAuditService?.create({
286
292
  actorId: refreshSub,
287
293
  event: AuthAuditEvent.TokenRefreshed,
@@ -1 +1 @@
1
- {"version":3,"file":"entityStorageAuthenticationService.js","sourceRoot":"","sources":["../../../src/services/entityStorageAuthenticationService.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type {\n\tIAuthenticationRateActionConfig,\n\tIAuthenticationRateComponent,\n\tIAuthenticationAuditComponent,\n\tIAuthenticationComponent\n} from \"@twin.org/api-auth-entity-storage-models\";\nimport { AuthAuditEvent } from \"@twin.org/api-auth-entity-storage-models\";\nimport type { ITenantAdminComponent } from \"@twin.org/api-models\";\nimport { ContextIdHelper, ContextIdKeys, ContextIdStore } from \"@twin.org/context\";\nimport {\n\tCoerce,\n\tComponentFactory,\n\tConverter,\n\tGeneralError,\n\tGuards,\n\tIs,\n\tNotFoundError,\n\tUnauthorizedError\n} from \"@twin.org/core\";\nimport { PasswordGenerator, PasswordValidator } from \"@twin.org/crypto\";\nimport {\n\tEntityStorageConnectorFactory,\n\ttype IEntityStorageConnector\n} from \"@twin.org/entity-storage-models\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { VaultConnectorFactory, type IVaultConnector } from \"@twin.org/vault-models\";\nimport type { AuthenticationUser } from \"../entities/authenticationUser.js\";\nimport type { IEntityStorageAuthenticationServiceConstructorOptions } from \"../models/IEntityStorageAuthenticationServiceConstructorOptions.js\";\nimport { PasswordHelper } from \"../utils/passwordHelper.js\";\nimport { TokenHelper } from \"../utils/tokenHelper.js\";\n\n/**\n * Implementation of the authentication component using entity storage.\n */\nexport class EntityStorageAuthenticationService implements IAuthenticationComponent {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<EntityStorageAuthenticationService>();\n\n\t/**\n\t * Default TTL in minutes.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_TTL_MINUTES: number = 60;\n\n\t/**\n\t * Default maximum login attempts in a rate window.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_LOGIN_RATE_MAX_ATTEMPTS: number = 5;\n\n\t/**\n\t * Default login rate window in minutes.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_LOGIN_RATE_WINDOW_MINUTES: number = 15;\n\n\t/**\n\t * Default maximum password change attempts in a rate window.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_PASSWORD_CHANGE_RATE_MAX_ATTEMPTS: number = 5;\n\n\t/**\n\t * Default password change rate window in minutes.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_PASSWORD_CHANGE_RATE_WINDOW_MINUTES: number = 15;\n\n\t/**\n\t * Default maximum token refresh attempts in a rate window.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_TOKEN_REFRESH_RATE_MAX_ATTEMPTS: number = 30;\n\n\t/**\n\t * Default token refresh rate window in minutes.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_TOKEN_REFRESH_RATE_WINDOW_MINUTES: number = 60;\n\n\t/**\n\t * The audit service.\n\t * @internal\n\t */\n\tprivate readonly _authenticationAuditService?: IAuthenticationAuditComponent;\n\n\t/**\n\t * The rate service.\n\t * @internal\n\t */\n\tprivate readonly _authenticationRateService: IAuthenticationRateComponent;\n\n\t/**\n\t * The entity storage for users.\n\t * @internal\n\t */\n\tprivate readonly _userEntityStorage: IEntityStorageConnector<AuthenticationUser>;\n\n\t/**\n\t * The vault for the keys.\n\t * @internal\n\t */\n\tprivate readonly _vaultConnector: IVaultConnector;\n\n\t/**\n\t * The name of the key to retrieve from the vault for signing JWT.\n\t * @internal\n\t */\n\tprivate readonly _signingKeyName: string;\n\n\t/**\n\t * The default TTL for the token.\n\t * @internal\n\t */\n\tprivate readonly _defaultTtlMinutes: number;\n\n\t/**\n\t * The minimum password length for validation.\n\t * @internal\n\t */\n\tprivate readonly _minPasswordLength?: number;\n\n\t/**\n\t * Rate limit configuration for login failures.\n\t * @internal\n\t */\n\tprivate readonly _loginRateLimit: IAuthenticationRateActionConfig;\n\n\t/**\n\t * Rate limit configuration for password changes.\n\t * @internal\n\t */\n\tprivate readonly _passwordChangeRateLimit: IAuthenticationRateActionConfig;\n\n\t/**\n\t * Rate limit configuration for token refresh.\n\t * @internal\n\t */\n\tprivate readonly _tokenRefreshRateLimit: IAuthenticationRateActionConfig;\n\n\t/**\n\t * The component to retrieve tenant information.\n\t * @internal\n\t */\n\tprivate readonly _tenantAdminComponent?: ITenantAdminComponent;\n\n\t/**\n\t * The node identity.\n\t * @internal\n\t */\n\tprivate _nodeId?: string;\n\n\t/**\n\t * Create a new instance of EntityStorageAuthentication.\n\t * @param options The dependencies for the identity connector.\n\t */\n\tconstructor(options?: IEntityStorageAuthenticationServiceConstructorOptions) {\n\t\tthis._userEntityStorage = EntityStorageConnectorFactory.get(\n\t\t\toptions?.userEntityStorageType ?? \"authentication-user\"\n\t\t);\n\n\t\tthis._vaultConnector = VaultConnectorFactory.get(options?.vaultConnectorType ?? \"vault\");\n\n\t\tthis._authenticationAuditService = ComponentFactory.getIfExists<IAuthenticationAuditComponent>(\n\t\t\toptions?.authenticationAuditServiceType ?? \"authentication-audit\"\n\t\t);\n\n\t\tthis._authenticationRateService = ComponentFactory.get<IAuthenticationRateComponent>(\n\t\t\toptions?.authenticationRateServiceType ?? \"authentication-rate\"\n\t\t);\n\n\t\tthis._tenantAdminComponent = ComponentFactory.getIfExists<ITenantAdminComponent>(\n\t\t\toptions?.tenantAdminComponentType ?? \"tenant-admin\"\n\t\t);\n\n\t\tthis._signingKeyName = options?.config?.signingKeyName ?? \"auth-signing\";\n\t\tthis._defaultTtlMinutes =\n\t\t\toptions?.config?.defaultTtlMinutes ?? EntityStorageAuthenticationService._DEFAULT_TTL_MINUTES;\n\t\tthis._minPasswordLength = options?.config?.minPasswordLength;\n\t\tthis._loginRateLimit = {\n\t\t\tmaxAttempts:\n\t\t\t\toptions?.config?.loginRateLimit?.maxAttempts ??\n\t\t\t\tEntityStorageAuthenticationService._DEFAULT_LOGIN_RATE_MAX_ATTEMPTS,\n\t\t\twindowMinutes:\n\t\t\t\toptions?.config?.loginRateLimit?.windowMinutes ??\n\t\t\t\tEntityStorageAuthenticationService._DEFAULT_LOGIN_RATE_WINDOW_MINUTES\n\t\t};\n\t\tthis._passwordChangeRateLimit = {\n\t\t\tmaxAttempts:\n\t\t\t\toptions?.config?.passwordChangeRateLimit?.maxAttempts ??\n\t\t\t\tEntityStorageAuthenticationService._DEFAULT_PASSWORD_CHANGE_RATE_MAX_ATTEMPTS,\n\t\t\twindowMinutes:\n\t\t\t\toptions?.config?.passwordChangeRateLimit?.windowMinutes ??\n\t\t\t\tEntityStorageAuthenticationService._DEFAULT_PASSWORD_CHANGE_RATE_WINDOW_MINUTES\n\t\t};\n\t\tthis._tokenRefreshRateLimit = {\n\t\t\tmaxAttempts:\n\t\t\t\toptions?.config?.tokenRefreshRateLimit?.maxAttempts ??\n\t\t\t\tEntityStorageAuthenticationService._DEFAULT_TOKEN_REFRESH_RATE_MAX_ATTEMPTS,\n\t\t\twindowMinutes:\n\t\t\t\toptions?.config?.tokenRefreshRateLimit?.windowMinutes ??\n\t\t\t\tEntityStorageAuthenticationService._DEFAULT_TOKEN_REFRESH_RATE_WINDOW_MINUTES\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 EntityStorageAuthenticationService.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 rate-limit actions have been registered and the node identity cached.\n\t */\n\tpublic async start(nodeLoggingComponentType?: string): Promise<void> {\n\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\tContextIdHelper.guard(contextIds, ContextIdKeys.Node);\n\t\tthis._nodeId = contextIds[ContextIdKeys.Node];\n\n\t\tawait this._authenticationRateService.registerAction(\"login\", this._loginRateLimit);\n\t\tawait this._authenticationRateService.registerAction(\n\t\t\t\"password-change\",\n\t\t\tthis._passwordChangeRateLimit\n\t\t);\n\t\tawait this._authenticationRateService.registerAction(\n\t\t\t\"token-refresh\",\n\t\t\tthis._tokenRefreshRateLimit\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 all rate-limit actions have been unregistered.\n\t */\n\tpublic async stop(nodeLoggingComponentType?: string): Promise<void> {\n\t\tawait this._authenticationRateService.unregisterAction(\"login\");\n\t\tawait this._authenticationRateService.unregisterAction(\"password-change\");\n\t\tawait this._authenticationRateService.unregisterAction(\"token-refresh\");\n\t}\n\n\t/**\n\t * Perform a login for the user.\n\t * @param email The email address for the user.\n\t * @param password The password for the user.\n\t * @returns The authentication token for the user, if it uses a mechanism with public access.\n\t */\n\tpublic async login(\n\t\temail: string,\n\t\tpassword: string\n\t): Promise<{\n\t\ttoken?: string;\n\t\texpiry: number;\n\t}> {\n\t\tGuards.stringValue(EntityStorageAuthenticationService.CLASS_NAME, nameof(email), email);\n\t\tGuards.stringValue(EntityStorageAuthenticationService.CLASS_NAME, nameof(password), password);\n\n\t\tlet loginUser: AuthenticationUser | undefined;\n\t\tlet loginTenantId: string | undefined;\n\t\tlet tokenAndExpiry: { token?: string; expiry: number } | undefined;\n\n\t\ttry {\n\t\t\tawait this._authenticationRateService.check(\"login\", email);\n\n\t\t\tconst user = await this._userEntityStorage.get(email);\n\t\t\tif (!user) {\n\t\t\t\tthrow new GeneralError(EntityStorageAuthenticationService.CLASS_NAME, \"userNotFound\");\n\t\t\t}\n\n\t\t\tconst saltBytes = Converter.base64ToBytes(user.salt);\n\t\t\tconst passwordBytes = Converter.utf8ToBytes(password);\n\n\t\t\tconst hashedPassword = await PasswordGenerator.hashPassword(passwordBytes, saltBytes);\n\n\t\t\tif (!PasswordValidator.comparePasswordHashes(hashedPassword, user.password)) {\n\t\t\t\tthrow new GeneralError(EntityStorageAuthenticationService.CLASS_NAME, \"passwordMismatch\");\n\t\t\t}\n\n\t\t\t// This might be undefined if the login is performed in a single tenant context\n\t\t\t// it is verified during the token processing, tenant id will be matched against\n\t\t\t// the context\n\t\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\t\tloginTenantId = contextIds?.[ContextIdKeys.Tenant];\n\n\t\t\ttokenAndExpiry = await TokenHelper.createToken(\n\t\t\t\tthis._vaultConnector,\n\t\t\t\t`${this._nodeId}/${this._signingKeyName}`,\n\t\t\t\tuser.identity,\n\t\t\t\tuser.organization,\n\t\t\t\tloginTenantId,\n\t\t\t\tthis._defaultTtlMinutes,\n\t\t\t\tuser.scope,\n\t\t\t\tuser.passwordVersion ?? 0\n\t\t\t);\n\t\t\tloginUser = user;\n\t\t} catch (error) {\n\t\t\tawait this._authenticationAuditService?.create({\n\t\t\t\tactorId: email,\n\t\t\t\tevent: AuthAuditEvent.LoginFailure\n\t\t\t});\n\n\t\t\tthrow new UnauthorizedError(\n\t\t\t\tEntityStorageAuthenticationService.CLASS_NAME,\n\t\t\t\t\"loginFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\n\t\tawait this._authenticationRateService.clear(\"login\", email);\n\n\t\tawait this._authenticationAuditService?.create({\n\t\t\tactorId: email,\n\t\t\tevent: AuthAuditEvent.LoginSuccess,\n\t\t\tdata: {\n\t\t\t\tuserIdentity: loginUser.identity,\n\t\t\t\torganizationIdentity: loginUser.organization,\n\t\t\t\ttenantId: loginTenantId,\n\t\t\t\tscope: loginUser.scope.split(\",\")\n\t\t\t}\n\t\t});\n\n\t\treturn tokenAndExpiry;\n\t}\n\n\t/**\n\t * Logout the current user.\n\t * @param token The token to logout, if it uses a mechanism with public access.\n\t * @returns A promise that resolves when the logout audit entry has been recorded.\n\t */\n\tpublic async logout(token?: string): Promise<void> {\n\t\t// Nothing to do here, as we are stateless.\n\t\t// The cookie will be revoked by the REST route handling\n\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\tconst identifier = contextIds?.[ContextIdKeys.User];\n\t\tif (Is.stringValue(identifier)) {\n\t\t\tawait this._authenticationAuditService?.create({\n\t\t\t\tactorId: identifier,\n\t\t\t\tevent: AuthAuditEvent.Logout\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * Refresh the token.\n\t * @param token The token to refresh, if it uses a mechanism with public access.\n\t * @returns The refreshed token, if it uses a mechanism with public access.\n\t */\n\tpublic async refresh(token?: string): Promise<{\n\t\ttoken?: string;\n\t\texpiry: number;\n\t}> {\n\t\tlet refreshPasswordVersion: number | undefined;\n\t\tlet tenantId: string | undefined;\n\n\t\t// If the verify fails on the current token then it will throw an exception.\n\t\tconst headerAndPayload = await TokenHelper.verify(\n\t\t\tthis._vaultConnector,\n\t\t\t`${this._nodeId}/${this._signingKeyName}`,\n\t\t\ttoken,\n\t\t\tundefined,\n\t\t\tasync (sub, org, tid, passwordVersion) => {\n\t\t\t\tconst validParts = [];\n\n\t\t\t\ttenantId = tid;\n\n\t\t\t\tif (Is.stringValue(tenantId)) {\n\t\t\t\t\tconst tenant = await this._tenantAdminComponent?.get(tenantId);\n\t\t\t\t\tif (tenant?.id === tenantId) {\n\t\t\t\t\t\tvalidParts.push(\"tenant\");\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// We use the tenant id from the token, if the user is not in that\n\t\t\t\t// partition then the get will fail. Only override the tenant key when\n\t\t\t\t// tid is present — setting it to undefined would drop the active partition.\n\t\t\t\tconst baseContext = (await ContextIdStore.getContextIds()) ?? {};\n\t\t\t\tconst contextIdsForUserLookup = Is.stringValue(tid)\n\t\t\t\t\t? { ...baseContext, [ContextIdKeys.Tenant]: tid }\n\t\t\t\t\t: baseContext;\n\n\t\t\t\t// Wrap the user lookup in the request context so partitioned storage uses the correct tenant.\n\t\t\t\tconst user = await ContextIdStore.run(contextIdsForUserLookup, async () =>\n\t\t\t\t\tthis._userEntityStorage.get(sub, \"identity\")\n\t\t\t\t);\n\n\t\t\t\trefreshPasswordVersion = user?.passwordVersion;\n\t\t\t\tif (user?.identity === sub && (passwordVersion ?? 0) === (refreshPasswordVersion ?? 0)) {\n\t\t\t\t\tvalidParts.push(\"user\");\n\t\t\t\t}\n\t\t\t\tif (user?.organization === org) {\n\t\t\t\t\tvalidParts.push(\"organization\");\n\t\t\t\t}\n\t\t\t\treturn validParts;\n\t\t\t}\n\t\t);\n\n\t\tconst refreshSub = headerAndPayload.payload.sub ?? \"\";\n\t\tawait this._authenticationRateService.check(\"token-refresh\", refreshSub);\n\n\t\tconst payloadOrg = Coerce.string(headerAndPayload.payload.org);\n\t\tconst payloadScope = Coerce.string(headerAndPayload.payload?.scope);\n\n\t\tconst refreshTokenAndExpiry = await TokenHelper.createToken(\n\t\t\tthis._vaultConnector,\n\t\t\t`${this._nodeId}/${this._signingKeyName}`,\n\t\t\trefreshSub,\n\t\t\tpayloadOrg,\n\t\t\ttenantId,\n\t\t\tthis._defaultTtlMinutes,\n\t\t\tpayloadScope,\n\t\t\trefreshPasswordVersion ?? 0\n\t\t);\n\n\t\tawait this._authenticationAuditService?.create({\n\t\t\tactorId: refreshSub,\n\t\t\tevent: AuthAuditEvent.TokenRefreshed,\n\t\t\tdata: {\n\t\t\t\torganizationIdentity: payloadOrg,\n\t\t\t\ttenantId,\n\t\t\t\tscope: payloadScope?.split(\",\").filter(scope => scope.length > 0),\n\t\t\t\tversion: refreshPasswordVersion ?? 0\n\t\t\t}\n\t\t});\n\n\t\treturn refreshTokenAndExpiry;\n\t}\n\n\t/**\n\t * Update the user's password.\n\t * @param currentPassword The current password for the user.\n\t * @param newPassword The new password for the user.\n\t * @returns A promise that resolves when the password has been updated and the rate limit cleared.\n\t */\n\tpublic async updatePassword(currentPassword: string, newPassword: string): Promise<void> {\n\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\tContextIdHelper.guard(contextIds, ContextIdKeys.User);\n\n\t\tconst userIdentity = contextIds[ContextIdKeys.User];\n\t\tawait this._authenticationRateService.check(\"password-change\", userIdentity);\n\n\t\tconst user = await this._userEntityStorage.get(userIdentity, \"identity\");\n\t\tif (!Is.object<AuthenticationUser>(user)) {\n\t\t\tthrow new NotFoundError(\n\t\t\t\tEntityStorageAuthenticationService.CLASS_NAME,\n\t\t\t\t\"userNotFound\",\n\t\t\t\tuserIdentity\n\t\t\t);\n\t\t}\n\n\t\tawait PasswordHelper.updatePassword(\n\t\t\tthis._userEntityStorage,\n\t\t\tthis._authenticationAuditService,\n\t\t\tuser,\n\t\t\tnewPassword,\n\t\t\tcurrentPassword,\n\t\t\tthis._minPasswordLength\n\t\t);\n\n\t\tawait this._authenticationRateService.clear(\"password-change\", userIdentity);\n\t}\n}\n"]}
1
+ {"version":3,"file":"entityStorageAuthenticationService.js","sourceRoot":"","sources":["../../../src/services/entityStorageAuthenticationService.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type {\n\tIAuthenticationRateActionConfig,\n\tIAuthenticationRateComponent,\n\tIAuthenticationAuditComponent,\n\tIAuthenticationComponent\n} from \"@twin.org/api-auth-entity-storage-models\";\nimport { AuthAuditEvent } from \"@twin.org/api-auth-entity-storage-models\";\nimport type { ITenantAdminComponent } from \"@twin.org/api-models\";\nimport { ContextIdHelper, ContextIdKeys, ContextIdStore } from \"@twin.org/context\";\nimport {\n\tCoerce,\n\tComponentFactory,\n\tConverter,\n\tGeneralError,\n\tGuards,\n\tIs,\n\tNotFoundError,\n\tUnauthorizedError\n} from \"@twin.org/core\";\nimport { PasswordGenerator, PasswordValidator } from \"@twin.org/crypto\";\nimport {\n\tEntityStorageConnectorFactory,\n\ttype IEntityStorageConnector\n} from \"@twin.org/entity-storage-models\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { VaultConnectorFactory, type IVaultConnector } from \"@twin.org/vault-models\";\nimport type { AuthenticationUser } from \"../entities/authenticationUser.js\";\nimport type { IEntityStorageAuthenticationServiceConstructorOptions } from \"../models/IEntityStorageAuthenticationServiceConstructorOptions.js\";\nimport { PasswordHelper } from \"../utils/passwordHelper.js\";\nimport { TokenHelper } from \"../utils/tokenHelper.js\";\n\n/**\n * Implementation of the authentication component using entity storage.\n */\nexport class EntityStorageAuthenticationService implements IAuthenticationComponent {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<EntityStorageAuthenticationService>();\n\n\t/**\n\t * Default TTL in minutes.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_TTL_MINUTES: number = 60;\n\n\t/**\n\t * Default maximum login attempts in a rate window.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_LOGIN_RATE_MAX_ATTEMPTS: number = 5;\n\n\t/**\n\t * Default login rate window in minutes.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_LOGIN_RATE_WINDOW_MINUTES: number = 15;\n\n\t/**\n\t * Default maximum password change attempts in a rate window.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_PASSWORD_CHANGE_RATE_MAX_ATTEMPTS: number = 5;\n\n\t/**\n\t * Default password change rate window in minutes.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_PASSWORD_CHANGE_RATE_WINDOW_MINUTES: number = 15;\n\n\t/**\n\t * Default maximum token refresh attempts in a rate window.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_TOKEN_REFRESH_RATE_MAX_ATTEMPTS: number = 30;\n\n\t/**\n\t * Default token refresh rate window in minutes.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_TOKEN_REFRESH_RATE_WINDOW_MINUTES: number = 60;\n\n\t/**\n\t * The audit service.\n\t * @internal\n\t */\n\tprivate readonly _authenticationAuditService?: IAuthenticationAuditComponent;\n\n\t/**\n\t * The rate service.\n\t * @internal\n\t */\n\tprivate readonly _authenticationRateService: IAuthenticationRateComponent;\n\n\t/**\n\t * The entity storage for users.\n\t * @internal\n\t */\n\tprivate readonly _userEntityStorage: IEntityStorageConnector<AuthenticationUser>;\n\n\t/**\n\t * The vault for the keys.\n\t * @internal\n\t */\n\tprivate readonly _vaultConnector: IVaultConnector;\n\n\t/**\n\t * The name of the key to retrieve from the vault for signing JWT.\n\t * @internal\n\t */\n\tprivate readonly _signingKeyName: string;\n\n\t/**\n\t * The default TTL for the token.\n\t * @internal\n\t */\n\tprivate readonly _defaultTtlMinutes: number;\n\n\t/**\n\t * The minimum password length for validation.\n\t * @internal\n\t */\n\tprivate readonly _minPasswordLength?: number;\n\n\t/**\n\t * Rate limit configuration for login failures.\n\t * @internal\n\t */\n\tprivate readonly _loginRateLimit: IAuthenticationRateActionConfig;\n\n\t/**\n\t * Rate limit configuration for password changes.\n\t * @internal\n\t */\n\tprivate readonly _passwordChangeRateLimit: IAuthenticationRateActionConfig;\n\n\t/**\n\t * Rate limit configuration for token refresh.\n\t * @internal\n\t */\n\tprivate readonly _tokenRefreshRateLimit: IAuthenticationRateActionConfig;\n\n\t/**\n\t * The component to retrieve tenant information.\n\t * @internal\n\t */\n\tprivate readonly _tenantAdminComponent?: ITenantAdminComponent;\n\n\t/**\n\t * The node identity.\n\t * @internal\n\t */\n\tprivate _nodeId?: string;\n\n\t/**\n\t * Create a new instance of EntityStorageAuthentication.\n\t * @param options The dependencies for the identity connector.\n\t */\n\tconstructor(options?: IEntityStorageAuthenticationServiceConstructorOptions) {\n\t\tthis._userEntityStorage = EntityStorageConnectorFactory.get(\n\t\t\toptions?.userEntityStorageType ?? \"authentication-user\"\n\t\t);\n\n\t\tthis._vaultConnector = VaultConnectorFactory.get(options?.vaultConnectorType ?? \"vault\");\n\n\t\tthis._authenticationAuditService = ComponentFactory.getIfExists<IAuthenticationAuditComponent>(\n\t\t\toptions?.authenticationAuditServiceType ?? \"authentication-audit\"\n\t\t);\n\n\t\tthis._authenticationRateService = ComponentFactory.get<IAuthenticationRateComponent>(\n\t\t\toptions?.authenticationRateServiceType ?? \"authentication-rate\"\n\t\t);\n\n\t\tthis._tenantAdminComponent = ComponentFactory.getIfExists<ITenantAdminComponent>(\n\t\t\toptions?.tenantAdminComponentType ?? \"tenant-admin\"\n\t\t);\n\n\t\tthis._signingKeyName = options?.config?.signingKeyName ?? \"auth-signing\";\n\t\tthis._defaultTtlMinutes =\n\t\t\toptions?.config?.defaultTtlMinutes ?? EntityStorageAuthenticationService._DEFAULT_TTL_MINUTES;\n\t\tthis._minPasswordLength = options?.config?.minPasswordLength;\n\t\tthis._loginRateLimit = {\n\t\t\tmaxAttempts:\n\t\t\t\toptions?.config?.loginRateLimit?.maxAttempts ??\n\t\t\t\tEntityStorageAuthenticationService._DEFAULT_LOGIN_RATE_MAX_ATTEMPTS,\n\t\t\twindowMinutes:\n\t\t\t\toptions?.config?.loginRateLimit?.windowMinutes ??\n\t\t\t\tEntityStorageAuthenticationService._DEFAULT_LOGIN_RATE_WINDOW_MINUTES\n\t\t};\n\t\tthis._passwordChangeRateLimit = {\n\t\t\tmaxAttempts:\n\t\t\t\toptions?.config?.passwordChangeRateLimit?.maxAttempts ??\n\t\t\t\tEntityStorageAuthenticationService._DEFAULT_PASSWORD_CHANGE_RATE_MAX_ATTEMPTS,\n\t\t\twindowMinutes:\n\t\t\t\toptions?.config?.passwordChangeRateLimit?.windowMinutes ??\n\t\t\t\tEntityStorageAuthenticationService._DEFAULT_PASSWORD_CHANGE_RATE_WINDOW_MINUTES\n\t\t};\n\t\tthis._tokenRefreshRateLimit = {\n\t\t\tmaxAttempts:\n\t\t\t\toptions?.config?.tokenRefreshRateLimit?.maxAttempts ??\n\t\t\t\tEntityStorageAuthenticationService._DEFAULT_TOKEN_REFRESH_RATE_MAX_ATTEMPTS,\n\t\t\twindowMinutes:\n\t\t\t\toptions?.config?.tokenRefreshRateLimit?.windowMinutes ??\n\t\t\t\tEntityStorageAuthenticationService._DEFAULT_TOKEN_REFRESH_RATE_WINDOW_MINUTES\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 EntityStorageAuthenticationService.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 rate-limit actions have been registered and the node identity cached.\n\t */\n\tpublic async start(nodeLoggingComponentType?: string): Promise<void> {\n\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\tContextIdHelper.guard(contextIds, ContextIdKeys.Node);\n\t\tthis._nodeId = contextIds[ContextIdKeys.Node];\n\n\t\tawait this._authenticationRateService.registerAction(\"login\", this._loginRateLimit);\n\t\tawait this._authenticationRateService.registerAction(\n\t\t\t\"password-change\",\n\t\t\tthis._passwordChangeRateLimit\n\t\t);\n\t\tawait this._authenticationRateService.registerAction(\n\t\t\t\"token-refresh\",\n\t\t\tthis._tokenRefreshRateLimit\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 all rate-limit actions have been unregistered.\n\t */\n\tpublic async stop(nodeLoggingComponentType?: string): Promise<void> {\n\t\tawait this._authenticationRateService.unregisterAction(\"login\");\n\t\tawait this._authenticationRateService.unregisterAction(\"password-change\");\n\t\tawait this._authenticationRateService.unregisterAction(\"token-refresh\");\n\t}\n\n\t/**\n\t * Perform a login for the user.\n\t * @param email The email address for the user.\n\t * @param password The password for the user.\n\t * @returns The authentication token for the user, if it uses a mechanism with public access.\n\t */\n\tpublic async login(\n\t\temail: string,\n\t\tpassword: string\n\t): Promise<{\n\t\ttoken?: string;\n\t\texpiry: number;\n\t}> {\n\t\tGuards.stringValue(EntityStorageAuthenticationService.CLASS_NAME, nameof(email), email);\n\t\tGuards.stringValue(EntityStorageAuthenticationService.CLASS_NAME, nameof(password), password);\n\n\t\tif (!Is.stringValue(this._nodeId)) {\n\t\t\tthrow new GeneralError(EntityStorageAuthenticationService.CLASS_NAME, \"nodeIdNotSet\");\n\t\t}\n\n\t\tlet loginUser: AuthenticationUser | undefined;\n\t\tlet loginTenantId: string | undefined;\n\t\tlet tokenAndExpiry: { token?: string; expiry: number } | undefined;\n\n\t\ttry {\n\t\t\tawait this._authenticationRateService.check(\"login\", email);\n\n\t\t\tconst user = await this._userEntityStorage.get(email);\n\t\t\tif (!user) {\n\t\t\t\tthrow new GeneralError(EntityStorageAuthenticationService.CLASS_NAME, \"userNotFound\");\n\t\t\t}\n\n\t\t\tconst saltBytes = Converter.base64ToBytes(user.salt);\n\t\t\tconst passwordBytes = Converter.utf8ToBytes(password);\n\n\t\t\tconst hashedPassword = await PasswordGenerator.hashPassword(passwordBytes, saltBytes);\n\n\t\t\tif (!PasswordValidator.comparePasswordHashes(hashedPassword, user.password)) {\n\t\t\t\tthrow new GeneralError(EntityStorageAuthenticationService.CLASS_NAME, \"passwordMismatch\");\n\t\t\t}\n\n\t\t\t// This might be undefined if the login is performed in a single tenant context\n\t\t\t// it is verified during the token processing, tenant id will be matched against\n\t\t\t// the context\n\t\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\t\tloginTenantId = contextIds?.[ContextIdKeys.Tenant];\n\n\t\t\ttokenAndExpiry = await TokenHelper.createToken(\n\t\t\t\tthis._vaultConnector,\n\t\t\t\tthis._nodeId,\n\t\t\t\tthis._signingKeyName,\n\t\t\t\tuser.identity,\n\t\t\t\tuser.organization,\n\t\t\t\tloginTenantId,\n\t\t\t\tthis._defaultTtlMinutes,\n\t\t\t\tuser.scope,\n\t\t\t\tuser.passwordVersion ?? 0\n\t\t\t);\n\t\t\tloginUser = user;\n\t\t} catch (error) {\n\t\t\tawait this._authenticationAuditService?.create({\n\t\t\t\tactorId: email,\n\t\t\t\tevent: AuthAuditEvent.LoginFailure\n\t\t\t});\n\n\t\t\tthrow new UnauthorizedError(\n\t\t\t\tEntityStorageAuthenticationService.CLASS_NAME,\n\t\t\t\t\"loginFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\n\t\tawait this._authenticationRateService.clear(\"login\", email);\n\n\t\tawait this._authenticationAuditService?.create({\n\t\t\tactorId: email,\n\t\t\tevent: AuthAuditEvent.LoginSuccess,\n\t\t\tdata: {\n\t\t\t\tuserIdentity: loginUser.identity,\n\t\t\t\torganizationIdentity: loginUser.organization,\n\t\t\t\ttenantId: loginTenantId,\n\t\t\t\tscope: loginUser.scope.split(\",\")\n\t\t\t}\n\t\t});\n\n\t\treturn tokenAndExpiry;\n\t}\n\n\t/**\n\t * Logout the current user.\n\t * @param token The token to logout, if it uses a mechanism with public access.\n\t * @returns A promise that resolves when the logout audit entry has been recorded.\n\t */\n\tpublic async logout(token?: string): Promise<void> {\n\t\t// Nothing to do here, as we are stateless.\n\t\t// The cookie will be revoked by the REST route handling\n\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\tconst identifier = contextIds?.[ContextIdKeys.User];\n\t\tif (Is.stringValue(identifier)) {\n\t\t\tawait this._authenticationAuditService?.create({\n\t\t\t\tactorId: identifier,\n\t\t\t\tevent: AuthAuditEvent.Logout\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * Refresh the token.\n\t * @param token The token to refresh, if it uses a mechanism with public access.\n\t * @returns The refreshed token, if it uses a mechanism with public access.\n\t */\n\tpublic async refresh(token?: string): Promise<{\n\t\ttoken?: string;\n\t\texpiry: number;\n\t}> {\n\t\tif (!Is.stringValue(this._nodeId)) {\n\t\t\tthrow new GeneralError(EntityStorageAuthenticationService.CLASS_NAME, \"nodeIdNotSet\");\n\t\t}\n\n\t\tlet refreshPasswordVersion: number | undefined;\n\t\tlet tenantId: string | undefined;\n\n\t\t// If the verify fails on the current token then it will throw an exception.\n\t\tconst headerAndPayload = await TokenHelper.verify(\n\t\t\tthis._vaultConnector,\n\t\t\tthis._nodeId,\n\t\t\tthis._signingKeyName,\n\t\t\ttoken,\n\t\t\tundefined,\n\t\t\tasync (sub, org, tid, passwordVersion) => {\n\t\t\t\tconst validParts = [];\n\n\t\t\t\ttenantId = tid;\n\n\t\t\t\tif (Is.stringValue(tenantId)) {\n\t\t\t\t\tconst tenant = await this._tenantAdminComponent?.get(tenantId);\n\t\t\t\t\tif (tenant?.id === tenantId) {\n\t\t\t\t\t\tvalidParts.push(\"tenant\");\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// We use the tenant id from the token, if the user is not in that\n\t\t\t\t// partition then the get will fail. Only override the tenant key when\n\t\t\t\t// tid is present — setting it to undefined would drop the active partition.\n\t\t\t\tconst baseContext = (await ContextIdStore.getContextIds()) ?? {};\n\t\t\t\tconst contextIdsForUserLookup = Is.stringValue(tid)\n\t\t\t\t\t? { ...baseContext, [ContextIdKeys.Tenant]: tid }\n\t\t\t\t\t: baseContext;\n\n\t\t\t\t// Wrap the user lookup in the request context so partitioned storage uses the correct tenant.\n\t\t\t\tconst user = await ContextIdStore.run(contextIdsForUserLookup, async () =>\n\t\t\t\t\tthis._userEntityStorage.get(sub, \"identity\")\n\t\t\t\t);\n\n\t\t\t\trefreshPasswordVersion = user?.passwordVersion;\n\t\t\t\tif (user?.identity === sub && (passwordVersion ?? 0) === (refreshPasswordVersion ?? 0)) {\n\t\t\t\t\tvalidParts.push(\"user\");\n\t\t\t\t}\n\t\t\t\tif (user?.organization === org) {\n\t\t\t\t\tvalidParts.push(\"organization\");\n\t\t\t\t}\n\t\t\t\treturn validParts;\n\t\t\t}\n\t\t);\n\n\t\tconst refreshSub = headerAndPayload.payload.sub ?? \"\";\n\t\tawait this._authenticationRateService.check(\"token-refresh\", refreshSub);\n\n\t\tconst payloadOrg = Coerce.string(headerAndPayload.payload.org);\n\t\tconst payloadScope = Coerce.string(headerAndPayload.payload?.scope);\n\n\t\tconst refreshTokenAndExpiry = await TokenHelper.createToken(\n\t\t\tthis._vaultConnector,\n\t\t\tthis._nodeId,\n\t\t\tthis._signingKeyName,\n\t\t\trefreshSub,\n\t\t\tpayloadOrg,\n\t\t\ttenantId,\n\t\t\tthis._defaultTtlMinutes,\n\t\t\tpayloadScope,\n\t\t\trefreshPasswordVersion ?? 0\n\t\t);\n\n\t\tawait this._authenticationAuditService?.create({\n\t\t\tactorId: refreshSub,\n\t\t\tevent: AuthAuditEvent.TokenRefreshed,\n\t\t\tdata: {\n\t\t\t\torganizationIdentity: payloadOrg,\n\t\t\t\ttenantId,\n\t\t\t\tscope: payloadScope?.split(\",\").filter(scope => scope.length > 0),\n\t\t\t\tversion: refreshPasswordVersion ?? 0\n\t\t\t}\n\t\t});\n\n\t\treturn refreshTokenAndExpiry;\n\t}\n\n\t/**\n\t * Update the user's password.\n\t * @param currentPassword The current password for the user.\n\t * @param newPassword The new password for the user.\n\t * @returns A promise that resolves when the password has been updated and the rate limit cleared.\n\t */\n\tpublic async updatePassword(currentPassword: string, newPassword: string): Promise<void> {\n\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\tContextIdHelper.guard(contextIds, ContextIdKeys.User);\n\n\t\tconst userIdentity = contextIds[ContextIdKeys.User];\n\t\tawait this._authenticationRateService.check(\"password-change\", userIdentity);\n\n\t\tconst user = await this._userEntityStorage.get(userIdentity, \"identity\");\n\t\tif (!Is.object<AuthenticationUser>(user)) {\n\t\t\tthrow new NotFoundError(\n\t\t\t\tEntityStorageAuthenticationService.CLASS_NAME,\n\t\t\t\t\"userNotFound\",\n\t\t\t\tuserIdentity\n\t\t\t);\n\t\t}\n\n\t\tawait PasswordHelper.updatePassword(\n\t\t\tthis._userEntityStorage,\n\t\t\tthis._authenticationAuditService,\n\t\t\tuser,\n\t\t\tnewPassword,\n\t\t\tcurrentPassword,\n\t\t\tthis._minPasswordLength\n\t\t);\n\n\t\tawait this._authenticationRateService.clear(\"password-change\", userIdentity);\n\t}\n}\n"]}
@@ -14,7 +14,8 @@ export class TokenHelper {
14
14
  /**
15
15
  * Create a new token.
16
16
  * @param vaultConnector The vault connector.
17
- * @param signingKeyName The signing key name.
17
+ * @param nodeId The node identifier, embedded as the JWT issuer claim.
18
+ * @param signingKeyName The signing key name, embedded as the JWT key identifier.
18
19
  * @param userIdentity The subject for the token.
19
20
  * @param organizationIdentity The organization for the token.
20
21
  * @param tenantId The tenant id for the token.
@@ -23,17 +24,19 @@ export class TokenHelper {
23
24
  * @param passwordVersion The user's current password version counter, embedded in the token so that a password change invalidates existing tokens.
24
25
  * @returns The new token and its expiry date.
25
26
  */
26
- static async createToken(vaultConnector, signingKeyName, userIdentity, organizationIdentity, tenantId, ttlMinutes, scope, passwordVersion) {
27
+ static async createToken(vaultConnector, nodeId, signingKeyName, userIdentity, organizationIdentity, tenantId, ttlMinutes, scope, passwordVersion) {
27
28
  const nowSeconds = Math.trunc(Date.now() / 1000);
28
29
  const ttlSeconds = ttlMinutes * 60;
29
- const jwt = await Jwt.encodeWithSigner({ alg: "EdDSA" }, {
30
+ const vaultKeyName = `${nodeId}/${signingKeyName}`;
31
+ const jwt = await Jwt.encodeWithSigner({ alg: "EdDSA", kid: signingKeyName }, {
32
+ iss: nodeId,
30
33
  sub: userIdentity,
31
34
  org: organizationIdentity,
32
35
  tid: tenantId,
33
36
  exp: nowSeconds + ttlSeconds,
34
37
  scope,
35
38
  pver: passwordVersion
36
- }, async (header, payload) => VaultConnectorHelper.jwtSigner(vaultConnector, signingKeyName, header, payload));
39
+ }, async (header, payload) => VaultConnectorHelper.jwtSigner(vaultConnector, vaultKeyName, header, payload));
37
40
  return {
38
41
  token: jwt,
39
42
  expiry: (nowSeconds + ttlSeconds) * 1000
@@ -42,20 +45,28 @@ export class TokenHelper {
42
45
  /**
43
46
  * Verify the token.
44
47
  * @param vaultConnector The vault connector.
45
- * @param signingKeyName The signing key name.
48
+ * @param nodeId The node identifier, expected to match the JWT issuer claim.
49
+ * @param signingKeyName The signing key name, expected to match the JWT key identifier.
46
50
  * @param token The token to verify.
47
51
  * @param requiredScopes The required scopes.
48
52
  * @param verifyUser A function to verify the user identity and organization. The password version counter embedded in the token (pver claim) is passed so callers can detect if the password has changed since the token was issued.
49
53
  * @returns The verified details.
50
54
  * @throws UnauthorizedError if the token is missing, invalid or expired.
51
55
  */
52
- static async verify(vaultConnector, signingKeyName, token, requiredScopes, verifyUser) {
56
+ static async verify(vaultConnector, nodeId, signingKeyName, token, requiredScopes, verifyUser) {
53
57
  if (!Is.stringValue(token)) {
54
58
  throw new UnauthorizedError(TokenHelper.CLASS_NAME, "missing");
55
59
  }
56
- const decoded = await Jwt.verifyWithVerifier(token, async (t) => VaultConnectorHelper.jwtVerifier(vaultConnector, signingKeyName, t));
60
+ const vaultKeyName = `${nodeId}/${signingKeyName}`;
61
+ const decoded = await Jwt.verifyWithVerifier(token, async (t) => VaultConnectorHelper.jwtVerifier(vaultConnector, vaultKeyName, t));
57
62
  // If some of the header/payload data is not properly populated then it is unauthorized.
58
- if (!Is.stringValue(decoded.payload.sub)) {
63
+ if (decoded.header.kid !== signingKeyName) {
64
+ throw new UnauthorizedError(TokenHelper.CLASS_NAME, "headerKeyIdMismatch");
65
+ }
66
+ else if (decoded.payload.iss !== nodeId) {
67
+ throw new UnauthorizedError(TokenHelper.CLASS_NAME, "payloadIssuerMismatch");
68
+ }
69
+ else if (!Is.stringValue(decoded.payload.sub)) {
59
70
  throw new UnauthorizedError(TokenHelper.CLASS_NAME, "payloadMissingSubject");
60
71
  }
61
72
  else if (!Is.stringValue(decoded.payload.org)) {
@@ -1 +1 @@
1
- {"version":3,"file":"tokenHelper.js","sourceRoot":"","sources":["../../../src/utils/tokenHelper.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { Coerce, Is, UnauthorizedError } from \"@twin.org/core\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { type IVaultConnector, VaultConnectorHelper } from \"@twin.org/vault-models\";\nimport {\n\tCookieHelper,\n\tHeaderHelper,\n\tHeaderTypes,\n\ttype IHttpHeaders,\n\ttype IJwtHeader,\n\ttype IJwtPayload,\n\tJwt\n} from \"@twin.org/web\";\n\n/**\n * Helper class for token operations.\n */\nexport class TokenHelper {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<TokenHelper>();\n\n\t/**\n\t * Create a new token.\n\t * @param vaultConnector The vault connector.\n\t * @param signingKeyName The signing key name.\n\t * @param userIdentity The subject for the token.\n\t * @param organizationIdentity The organization for the token.\n\t * @param tenantId The tenant id for the token.\n\t * @param ttlMinutes The time to live for the token in minutes.\n\t * @param scope The scopes for the token.\n\t * @param passwordVersion The user's current password version counter, embedded in the token so that a password change invalidates existing tokens.\n\t * @returns The new token and its expiry date.\n\t */\n\tpublic static async createToken(\n\t\tvaultConnector: IVaultConnector,\n\t\tsigningKeyName: string,\n\t\tuserIdentity: string,\n\t\torganizationIdentity: string | undefined,\n\t\ttenantId: string | undefined,\n\t\tttlMinutes: number,\n\t\tscope?: string,\n\t\tpasswordVersion?: number\n\t): Promise<{\n\t\ttoken: string;\n\t\texpiry: number;\n\t}> {\n\t\tconst nowSeconds = Math.trunc(Date.now() / 1000);\n\t\tconst ttlSeconds = ttlMinutes * 60;\n\n\t\tconst jwt = await Jwt.encodeWithSigner(\n\t\t\t{ alg: \"EdDSA\" },\n\t\t\t{\n\t\t\t\tsub: userIdentity,\n\t\t\t\torg: organizationIdentity,\n\t\t\t\ttid: tenantId,\n\t\t\t\texp: nowSeconds + ttlSeconds,\n\t\t\t\tscope,\n\t\t\t\tpver: passwordVersion\n\t\t\t},\n\t\t\tasync (header, payload) =>\n\t\t\t\tVaultConnectorHelper.jwtSigner(vaultConnector, signingKeyName, header, payload)\n\t\t);\n\n\t\treturn {\n\t\t\ttoken: jwt,\n\t\t\texpiry: (nowSeconds + ttlSeconds) * 1000\n\t\t};\n\t}\n\n\t/**\n\t * Verify the token.\n\t * @param vaultConnector The vault connector.\n\t * @param signingKeyName The signing key name.\n\t * @param token The token to verify.\n\t * @param requiredScopes The required scopes.\n\t * @param verifyUser A function to verify the user identity and organization. The password version counter embedded in the token (pver claim) is passed so callers can detect if the password has changed since the token was issued.\n\t * @returns The verified details.\n\t * @throws UnauthorizedError if the token is missing, invalid or expired.\n\t */\n\tpublic static async verify(\n\t\tvaultConnector: IVaultConnector,\n\t\tsigningKeyName: string,\n\t\ttoken: string | undefined,\n\t\trequiredScopes?: string[],\n\t\tverifyUser?: (\n\t\t\tsub: string,\n\t\t\torg: string,\n\t\t\ttid: string | undefined,\n\t\t\tpasswordVersion: number | undefined\n\t\t) => Promise<string[]>\n\t): Promise<{\n\t\theader: IJwtHeader;\n\t\tpayload: IJwtPayload;\n\t}> {\n\t\tif (!Is.stringValue(token)) {\n\t\t\tthrow new UnauthorizedError(TokenHelper.CLASS_NAME, \"missing\");\n\t\t}\n\n\t\tconst decoded = await Jwt.verifyWithVerifier(token, async t =>\n\t\t\tVaultConnectorHelper.jwtVerifier(vaultConnector, signingKeyName, t)\n\t\t);\n\n\t\t// If some of the header/payload data is not properly populated then it is unauthorized.\n\t\tif (!Is.stringValue(decoded.payload.sub)) {\n\t\t\tthrow new UnauthorizedError(TokenHelper.CLASS_NAME, \"payloadMissingSubject\");\n\t\t} else if (!Is.stringValue(decoded.payload.org)) {\n\t\t\tthrow new UnauthorizedError(TokenHelper.CLASS_NAME, \"payloadMissingOrganization\");\n\t\t} else if (\n\t\t\t!Is.empty(decoded.payload?.exp) &&\n\t\t\tdecoded.payload.exp < Math.trunc(Date.now() / 1000)\n\t\t) {\n\t\t\tthrow new UnauthorizedError(TokenHelper.CLASS_NAME, \"expired\");\n\t\t}\n\n\t\tif (Is.function(verifyUser)) {\n\t\t\tconst tid = Coerce.string(decoded.payload.tid);\n\t\t\tconst userVerified = await verifyUser(\n\t\t\t\tdecoded.payload.sub,\n\t\t\t\tdecoded.payload.org,\n\t\t\t\ttid,\n\t\t\t\tCoerce.integer(decoded.payload.pver)\n\t\t\t);\n\t\t\tif (!userVerified.includes(\"user\")) {\n\t\t\t\tthrow new UnauthorizedError(TokenHelper.CLASS_NAME, \"userNotVerified\");\n\t\t\t} else if (!userVerified.includes(\"organization\")) {\n\t\t\t\tthrow new UnauthorizedError(TokenHelper.CLASS_NAME, \"organizationNotVerified\");\n\t\t\t} else if (Is.stringValue(tid) && !userVerified.includes(\"tenant\")) {\n\t\t\t\tthrow new UnauthorizedError(TokenHelper.CLASS_NAME, \"tenantNotVerified\");\n\t\t\t}\n\t\t}\n\n\t\tif (Is.arrayValue(requiredScopes)) {\n\t\t\tconst tokenScopes = Is.stringValue(decoded.payload.scope)\n\t\t\t\t? decoded.payload.scope.split(\",\")\n\t\t\t\t: [];\n\n\t\t\tfor (const requiredScope of requiredScopes) {\n\t\t\t\tif (!tokenScopes.includes(requiredScope)) {\n\t\t\t\t\tthrow new UnauthorizedError(TokenHelper.CLASS_NAME, \"insufficientScopes\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\theader: decoded.header,\n\t\t\tpayload: decoded.payload\n\t\t};\n\t}\n\n\t/**\n\t * Extract the auth token from the headers, either from the authorization header or the cookie header.\n\t * @param headers The headers to extract the token from.\n\t * @param cookieName The name of the cookie to extract the token from.\n\t * @returns The token if found.\n\t */\n\tpublic static extractTokenFromHeaders(\n\t\theaders?: IHttpHeaders,\n\t\tcookieName?: string\n\t):\n\t\t| {\n\t\t\t\ttoken: string;\n\t\t\t\tlocation: \"authorization\" | \"cookie\";\n\t\t }\n\t\t| undefined {\n\t\tconst authHeader = headers?.[HeaderTypes.Authorization];\n\t\tconst cookiesHeader = headers?.[HeaderTypes.Cookie];\n\n\t\tconst bearerToken = HeaderHelper.extractBearer(authHeader);\n\t\tif (Is.stringValue(bearerToken)) {\n\t\t\treturn {\n\t\t\t\ttoken: bearerToken,\n\t\t\t\tlocation: \"authorization\"\n\t\t\t};\n\t\t} else if (Is.notEmpty(cookiesHeader) && Is.stringValue(cookieName)) {\n\t\t\tconst value = CookieHelper.getCookieFromHeaders(cookiesHeader, cookieName);\n\t\t\tif (Is.stringValue(value)) {\n\t\t\t\treturn {\n\t\t\t\t\ttoken: value,\n\t\t\t\t\tlocation: \"cookie\"\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"tokenHelper.js","sourceRoot":"","sources":["../../../src/utils/tokenHelper.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { Coerce, Is, UnauthorizedError } from \"@twin.org/core\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { type IVaultConnector, VaultConnectorHelper } from \"@twin.org/vault-models\";\nimport {\n\tCookieHelper,\n\tHeaderHelper,\n\tHeaderTypes,\n\ttype IHttpHeaders,\n\ttype IJwtHeader,\n\ttype IJwtPayload,\n\tJwt\n} from \"@twin.org/web\";\n\n/**\n * Helper class for token operations.\n */\nexport class TokenHelper {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<TokenHelper>();\n\n\t/**\n\t * Create a new token.\n\t * @param vaultConnector The vault connector.\n\t * @param nodeId The node identifier, embedded as the JWT issuer claim.\n\t * @param signingKeyName The signing key name, embedded as the JWT key identifier.\n\t * @param userIdentity The subject for the token.\n\t * @param organizationIdentity The organization for the token.\n\t * @param tenantId The tenant id for the token.\n\t * @param ttlMinutes The time to live for the token in minutes.\n\t * @param scope The scopes for the token.\n\t * @param passwordVersion The user's current password version counter, embedded in the token so that a password change invalidates existing tokens.\n\t * @returns The new token and its expiry date.\n\t */\n\tpublic static async createToken(\n\t\tvaultConnector: IVaultConnector,\n\t\tnodeId: string,\n\t\tsigningKeyName: string,\n\t\tuserIdentity: string,\n\t\torganizationIdentity: string | undefined,\n\t\ttenantId: string | undefined,\n\t\tttlMinutes: number,\n\t\tscope?: string,\n\t\tpasswordVersion?: number\n\t): Promise<{\n\t\ttoken: string;\n\t\texpiry: number;\n\t}> {\n\t\tconst nowSeconds = Math.trunc(Date.now() / 1000);\n\t\tconst ttlSeconds = ttlMinutes * 60;\n\t\tconst vaultKeyName = `${nodeId}/${signingKeyName}`;\n\n\t\tconst jwt = await Jwt.encodeWithSigner(\n\t\t\t{ alg: \"EdDSA\", kid: signingKeyName },\n\t\t\t{\n\t\t\t\tiss: nodeId,\n\t\t\t\tsub: userIdentity,\n\t\t\t\torg: organizationIdentity,\n\t\t\t\ttid: tenantId,\n\t\t\t\texp: nowSeconds + ttlSeconds,\n\t\t\t\tscope,\n\t\t\t\tpver: passwordVersion\n\t\t\t},\n\t\t\tasync (header, payload) =>\n\t\t\t\tVaultConnectorHelper.jwtSigner(vaultConnector, vaultKeyName, header, payload)\n\t\t);\n\n\t\treturn {\n\t\t\ttoken: jwt,\n\t\t\texpiry: (nowSeconds + ttlSeconds) * 1000\n\t\t};\n\t}\n\n\t/**\n\t * Verify the token.\n\t * @param vaultConnector The vault connector.\n\t * @param nodeId The node identifier, expected to match the JWT issuer claim.\n\t * @param signingKeyName The signing key name, expected to match the JWT key identifier.\n\t * @param token The token to verify.\n\t * @param requiredScopes The required scopes.\n\t * @param verifyUser A function to verify the user identity and organization. The password version counter embedded in the token (pver claim) is passed so callers can detect if the password has changed since the token was issued.\n\t * @returns The verified details.\n\t * @throws UnauthorizedError if the token is missing, invalid or expired.\n\t */\n\tpublic static async verify(\n\t\tvaultConnector: IVaultConnector,\n\t\tnodeId: string,\n\t\tsigningKeyName: string,\n\t\ttoken: string | undefined,\n\t\trequiredScopes?: string[],\n\t\tverifyUser?: (\n\t\t\tsub: string,\n\t\t\torg: string,\n\t\t\ttid: string | undefined,\n\t\t\tpasswordVersion: number | undefined\n\t\t) => Promise<string[]>\n\t): Promise<{\n\t\theader: IJwtHeader;\n\t\tpayload: IJwtPayload;\n\t}> {\n\t\tif (!Is.stringValue(token)) {\n\t\t\tthrow new UnauthorizedError(TokenHelper.CLASS_NAME, \"missing\");\n\t\t}\n\n\t\tconst vaultKeyName = `${nodeId}/${signingKeyName}`;\n\t\tconst decoded = await Jwt.verifyWithVerifier(token, async t =>\n\t\t\tVaultConnectorHelper.jwtVerifier(vaultConnector, vaultKeyName, t)\n\t\t);\n\n\t\t// If some of the header/payload data is not properly populated then it is unauthorized.\n\t\tif (decoded.header.kid !== signingKeyName) {\n\t\t\tthrow new UnauthorizedError(TokenHelper.CLASS_NAME, \"headerKeyIdMismatch\");\n\t\t} else if (decoded.payload.iss !== nodeId) {\n\t\t\tthrow new UnauthorizedError(TokenHelper.CLASS_NAME, \"payloadIssuerMismatch\");\n\t\t} else if (!Is.stringValue(decoded.payload.sub)) {\n\t\t\tthrow new UnauthorizedError(TokenHelper.CLASS_NAME, \"payloadMissingSubject\");\n\t\t} else if (!Is.stringValue(decoded.payload.org)) {\n\t\t\tthrow new UnauthorizedError(TokenHelper.CLASS_NAME, \"payloadMissingOrganization\");\n\t\t} else if (\n\t\t\t!Is.empty(decoded.payload?.exp) &&\n\t\t\tdecoded.payload.exp < Math.trunc(Date.now() / 1000)\n\t\t) {\n\t\t\tthrow new UnauthorizedError(TokenHelper.CLASS_NAME, \"expired\");\n\t\t}\n\n\t\tif (Is.function(verifyUser)) {\n\t\t\tconst tid = Coerce.string(decoded.payload.tid);\n\t\t\tconst userVerified = await verifyUser(\n\t\t\t\tdecoded.payload.sub,\n\t\t\t\tdecoded.payload.org,\n\t\t\t\ttid,\n\t\t\t\tCoerce.integer(decoded.payload.pver)\n\t\t\t);\n\t\t\tif (!userVerified.includes(\"user\")) {\n\t\t\t\tthrow new UnauthorizedError(TokenHelper.CLASS_NAME, \"userNotVerified\");\n\t\t\t} else if (!userVerified.includes(\"organization\")) {\n\t\t\t\tthrow new UnauthorizedError(TokenHelper.CLASS_NAME, \"organizationNotVerified\");\n\t\t\t} else if (Is.stringValue(tid) && !userVerified.includes(\"tenant\")) {\n\t\t\t\tthrow new UnauthorizedError(TokenHelper.CLASS_NAME, \"tenantNotVerified\");\n\t\t\t}\n\t\t}\n\n\t\tif (Is.arrayValue(requiredScopes)) {\n\t\t\tconst tokenScopes = Is.stringValue(decoded.payload.scope)\n\t\t\t\t? decoded.payload.scope.split(\",\")\n\t\t\t\t: [];\n\n\t\t\tfor (const requiredScope of requiredScopes) {\n\t\t\t\tif (!tokenScopes.includes(requiredScope)) {\n\t\t\t\t\tthrow new UnauthorizedError(TokenHelper.CLASS_NAME, \"insufficientScopes\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\theader: decoded.header,\n\t\t\tpayload: decoded.payload\n\t\t};\n\t}\n\n\t/**\n\t * Extract the auth token from the headers, either from the authorization header or the cookie header.\n\t * @param headers The headers to extract the token from.\n\t * @param cookieName The name of the cookie to extract the token from.\n\t * @returns The token if found.\n\t */\n\tpublic static extractTokenFromHeaders(\n\t\theaders?: IHttpHeaders,\n\t\tcookieName?: string\n\t):\n\t\t| {\n\t\t\t\ttoken: string;\n\t\t\t\tlocation: \"authorization\" | \"cookie\";\n\t\t }\n\t\t| undefined {\n\t\tconst authHeader = headers?.[HeaderTypes.Authorization];\n\t\tconst cookiesHeader = headers?.[HeaderTypes.Cookie];\n\n\t\tconst bearerToken = HeaderHelper.extractBearer(authHeader);\n\t\tif (Is.stringValue(bearerToken)) {\n\t\t\treturn {\n\t\t\t\ttoken: bearerToken,\n\t\t\t\tlocation: \"authorization\"\n\t\t\t};\n\t\t} else if (Is.notEmpty(cookiesHeader) && Is.stringValue(cookieName)) {\n\t\t\tconst value = CookieHelper.getCookieFromHeaders(cookiesHeader, cookieName);\n\t\t\tif (Is.stringValue(value)) {\n\t\t\t\treturn {\n\t\t\t\t\ttoken: value,\n\t\t\t\t\tlocation: \"cookie\"\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
@@ -13,6 +13,11 @@ export interface IEntityStorageAuthenticationRateServiceConstructorOptions {
13
13
  * @default task-scheduler
14
14
  */
15
15
  taskSchedulerComponentType?: string;
16
+ /**
17
+ * The platform component type, used to run the periodic cleanup per tenant.
18
+ * @default platform
19
+ */
20
+ platformComponentType?: string;
16
21
  /**
17
22
  * The configuration for the authentication rate service.
18
23
  */
@@ -11,7 +11,8 @@ export declare class TokenHelper {
11
11
  /**
12
12
  * Create a new token.
13
13
  * @param vaultConnector The vault connector.
14
- * @param signingKeyName The signing key name.
14
+ * @param nodeId The node identifier, embedded as the JWT issuer claim.
15
+ * @param signingKeyName The signing key name, embedded as the JWT key identifier.
15
16
  * @param userIdentity The subject for the token.
16
17
  * @param organizationIdentity The organization for the token.
17
18
  * @param tenantId The tenant id for the token.
@@ -20,21 +21,22 @@ export declare class TokenHelper {
20
21
  * @param passwordVersion The user's current password version counter, embedded in the token so that a password change invalidates existing tokens.
21
22
  * @returns The new token and its expiry date.
22
23
  */
23
- static createToken(vaultConnector: IVaultConnector, signingKeyName: string, userIdentity: string, organizationIdentity: string | undefined, tenantId: string | undefined, ttlMinutes: number, scope?: string, passwordVersion?: number): Promise<{
24
+ static createToken(vaultConnector: IVaultConnector, nodeId: string, signingKeyName: string, userIdentity: string, organizationIdentity: string | undefined, tenantId: string | undefined, ttlMinutes: number, scope?: string, passwordVersion?: number): Promise<{
24
25
  token: string;
25
26
  expiry: number;
26
27
  }>;
27
28
  /**
28
29
  * Verify the token.
29
30
  * @param vaultConnector The vault connector.
30
- * @param signingKeyName The signing key name.
31
+ * @param nodeId The node identifier, expected to match the JWT issuer claim.
32
+ * @param signingKeyName The signing key name, expected to match the JWT key identifier.
31
33
  * @param token The token to verify.
32
34
  * @param requiredScopes The required scopes.
33
35
  * @param verifyUser A function to verify the user identity and organization. The password version counter embedded in the token (pver claim) is passed so callers can detect if the password has changed since the token was issued.
34
36
  * @returns The verified details.
35
37
  * @throws UnauthorizedError if the token is missing, invalid or expired.
36
38
  */
37
- static verify(vaultConnector: IVaultConnector, signingKeyName: string, token: string | undefined, requiredScopes?: string[], verifyUser?: (sub: string, org: string, tid: string | undefined, passwordVersion: number | undefined) => Promise<string[]>): Promise<{
39
+ static verify(vaultConnector: IVaultConnector, nodeId: string, signingKeyName: string, token: string | undefined, requiredScopes?: string[], verifyUser?: (sub: string, org: string, tid: string | undefined, passwordVersion: number | undefined) => Promise<string[]>): Promise<{
38
40
  header: IJwtHeader;
39
41
  payload: IJwtPayload;
40
42
  }>;
package/docs/changelog.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.3-next.53](https://github.com/iotaledger/twin-api/compare/api-auth-entity-storage-service-v0.0.3-next.52...api-auth-entity-storage-service-v0.0.3-next.53) (2026-06-23)
4
+
5
+
6
+ ### Features
7
+
8
+ * include iss and kid in jwt ([#189](https://github.com/iotaledger/twin-api/issues/189)) ([9df072a](https://github.com/iotaledger/twin-api/commit/9df072a1ecfc60dec611f59001449089264bbb1c))
9
+
10
+
11
+ ### Dependencies
12
+
13
+ * The following workspace dependencies were updated
14
+ * dependencies
15
+ * @twin.org/api-auth-entity-storage-models bumped from 0.0.3-next.52 to 0.0.3-next.53
16
+ * @twin.org/api-core bumped from 0.0.3-next.52 to 0.0.3-next.53
17
+ * @twin.org/api-models bumped from 0.0.3-next.52 to 0.0.3-next.53
18
+
19
+ ## [0.0.3-next.52](https://github.com/iotaledger/twin-api/compare/api-auth-entity-storage-service-v0.0.3-next.51...api-auth-entity-storage-service-v0.0.3-next.52) (2026-06-22)
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * run authentication rate cleanup per tenant ([#186](https://github.com/iotaledger/twin-api/issues/186)) ([d69c7aa](https://github.com/iotaledger/twin-api/commit/d69c7aa4ade77f1a52f1f554d8f7a3bd20f1b330))
25
+
26
+
27
+ ### Dependencies
28
+
29
+ * The following workspace dependencies were updated
30
+ * dependencies
31
+ * @twin.org/api-auth-entity-storage-models bumped from 0.0.3-next.51 to 0.0.3-next.52
32
+ * @twin.org/api-core bumped from 0.0.3-next.51 to 0.0.3-next.52
33
+ * @twin.org/api-models bumped from 0.0.3-next.51 to 0.0.3-next.52
34
+
3
35
  ## [0.0.3-next.51](https://github.com/iotaledger/twin-api/compare/api-auth-entity-storage-service-v0.0.3-next.50...api-auth-entity-storage-service-v0.0.3-next.51) (2026-06-20)
4
36
 
5
37
 
@@ -24,7 +24,7 @@ Runtime name for the class.
24
24
 
25
25
  ### createToken() {#createtoken}
26
26
 
27
- > `static` **createToken**(`vaultConnector`, `signingKeyName`, `userIdentity`, `organizationIdentity`, `tenantId`, `ttlMinutes`, `scope?`, `passwordVersion?`): `Promise`\<\{ `token`: `string`; `expiry`: `number`; \}\>
27
+ > `static` **createToken**(`vaultConnector`, `nodeId`, `signingKeyName`, `userIdentity`, `organizationIdentity`, `tenantId`, `ttlMinutes`, `scope?`, `passwordVersion?`): `Promise`\<\{ `token`: `string`; `expiry`: `number`; \}\>
28
28
 
29
29
  Create a new token.
30
30
 
@@ -36,11 +36,17 @@ Create a new token.
36
36
 
37
37
  The vault connector.
38
38
 
39
+ ##### nodeId
40
+
41
+ `string`
42
+
43
+ The node identifier, embedded as the JWT issuer claim.
44
+
39
45
  ##### signingKeyName
40
46
 
41
47
  `string`
42
48
 
43
- The signing key name.
49
+ The signing key name, embedded as the JWT key identifier.
44
50
 
45
51
  ##### userIdentity
46
52
 
@@ -88,7 +94,7 @@ The new token and its expiry date.
88
94
 
89
95
  ### verify() {#verify}
90
96
 
91
- > `static` **verify**(`vaultConnector`, `signingKeyName`, `token`, `requiredScopes?`, `verifyUser?`): `Promise`\<\{ `header`: `JWTHeaderParameters`; `payload`: `JWTPayload`; \}\>
97
+ > `static` **verify**(`vaultConnector`, `nodeId`, `signingKeyName`, `token`, `requiredScopes?`, `verifyUser?`): `Promise`\<\{ `header`: `JWTHeaderParameters`; `payload`: `JWTPayload`; \}\>
92
98
 
93
99
  Verify the token.
94
100
 
@@ -100,11 +106,17 @@ Verify the token.
100
106
 
101
107
  The vault connector.
102
108
 
109
+ ##### nodeId
110
+
111
+ `string`
112
+
113
+ The node identifier, expected to match the JWT issuer claim.
114
+
103
115
  ##### signingKeyName
104
116
 
105
117
  `string`
106
118
 
107
- The signing key name.
119
+ The signing key name, expected to match the JWT key identifier.
108
120
 
109
121
  ##### token
110
122
 
@@ -32,6 +32,20 @@ task-scheduler
32
32
 
33
33
  ***
34
34
 
35
+ ### platformComponentType? {#platformcomponenttype}
36
+
37
+ > `optional` **platformComponentType?**: `string`
38
+
39
+ The platform component type, used to run the periodic cleanup per tenant.
40
+
41
+ #### Default
42
+
43
+ ```ts
44
+ platform
45
+ ```
46
+
47
+ ***
48
+
35
49
  ### config? {#config}
36
50
 
37
51
  > `optional` **config?**: [`IEntityStorageAuthenticationRateServiceConfig`](IEntityStorageAuthenticationRateServiceConfig.md)
package/locales/en.json CHANGED
@@ -3,7 +3,8 @@
3
3
  "entityStorageAuthenticationService": {
4
4
  "loginFailed": "Login failed",
5
5
  "userNotFound": "The user with the specified e-mail could not be found",
6
- "passwordMismatch": "The password does not match the user's password"
6
+ "passwordMismatch": "The password does not match the user's password",
7
+ "nodeIdNotSet": "The node ID has not been set"
7
8
  },
8
9
  "entityStorageAuthenticationAdminService": {
9
10
  "userExists": "The user with the specified e-mail already exists",
@@ -14,6 +15,9 @@
14
15
  "updatePasswordFailed": "Updating the user's password failed",
15
16
  "userNotFound": "The user with the specified e-mail could not be found \"{notFoundId}\""
16
17
  },
18
+ "authHeaderProcessor": {
19
+ "nodeIdNotSet": "The node ID has not been set"
20
+ },
17
21
  "passwordHelper": {
18
22
  "currentPasswordMismatch": "The current password is incorrect"
19
23
  },
@@ -23,6 +27,8 @@
23
27
  },
24
28
  "tokenHelper": {
25
29
  "missing": "The JSON Web token could not be found in the authorization header",
30
+ "headerKeyIdMismatch": "The JSON Web token key identifier does not match the expected signing key",
31
+ "payloadIssuerMismatch": "The JSON Web token issuer does not match the expected signing key",
26
32
  "payloadMissingSubject": "The JSON Web token payload does not contain a subject",
27
33
  "payloadMissingOrganization": "The JSON Web token payload does not contain an organization",
28
34
  "expired": "The JSON Web token has expired",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twin.org/api-auth-entity-storage-service",
3
- "version": "0.0.3-next.51",
3
+ "version": "0.0.3-next.53",
4
4
  "description": "Authentication service implementation and REST routes backed by entity storage.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,9 +14,9 @@
14
14
  "node": ">=20.0.0"
15
15
  },
16
16
  "dependencies": {
17
- "@twin.org/api-auth-entity-storage-models": "0.0.3-next.51",
18
- "@twin.org/api-core": "0.0.3-next.51",
19
- "@twin.org/api-models": "0.0.3-next.51",
17
+ "@twin.org/api-auth-entity-storage-models": "0.0.3-next.53",
18
+ "@twin.org/api-core": "0.0.3-next.53",
19
+ "@twin.org/api-models": "0.0.3-next.53",
20
20
  "@twin.org/background-task-models": "next",
21
21
  "@twin.org/context": "next",
22
22
  "@twin.org/core": "next",