@technomoron/api-server-base 1.1.9 → 1.1.10

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.
@@ -588,6 +588,7 @@ class ApiServer {
588
588
  }
589
589
  async authenticate(apiReq, authType) {
590
590
  if (authType === 'none') {
591
+ apiReq.realUid = null;
591
592
  return null;
592
593
  }
593
594
  let token = null;
@@ -624,6 +625,8 @@ class ApiServer {
624
625
  if (!tokenData) {
625
626
  throw new ApiError({ code: 401, message: 'Unathorized Access - ' + error });
626
627
  }
628
+ const effectiveUserId = this.extractTokenUserId(tokenData);
629
+ apiReq.realUid = this.resolveRealUserId(tokenData, effectiveUserId);
627
630
  if (this.shouldValidateStoredToken(authType)) {
628
631
  await this.assertStoredAccessToken(apiReq, token, tokenData);
629
632
  }
@@ -666,10 +669,7 @@ class ApiServer {
666
669
  return this.config.validateTokens || authType === 'strict';
667
670
  }
668
671
  async assertStoredAccessToken(apiReq, token, tokenData) {
669
- if (typeof tokenData.uid !== 'string' && typeof tokenData.uid !== 'number') {
670
- throw new ApiError({ code: 401, message: 'Authorization token is malformed' });
671
- }
672
- const userId = tokenData.uid;
672
+ const userId = this.extractTokenUserId(tokenData);
673
673
  const stored = await this.storageAdapter.getToken({
674
674
  accessToken: token,
675
675
  userId
@@ -679,6 +679,36 @@ class ApiServer {
679
679
  }
680
680
  apiReq.authToken = stored;
681
681
  }
682
+ normalizeAuthIdentifier(candidate) {
683
+ if (typeof candidate === 'number' && Number.isFinite(candidate)) {
684
+ return candidate;
685
+ }
686
+ if (typeof candidate === 'string') {
687
+ const trimmed = candidate.trim();
688
+ if (trimmed.length > 0) {
689
+ return trimmed;
690
+ }
691
+ }
692
+ return null;
693
+ }
694
+ extractTokenUserId(tokenData) {
695
+ const normalized = this.normalizeAuthIdentifier(tokenData.uid);
696
+ if (normalized === null) {
697
+ throw new ApiError({ code: 401, message: 'Authorization token is malformed' });
698
+ }
699
+ return normalized;
700
+ }
701
+ resolveRealUserId(tokenData, effectiveUserId) {
702
+ const withReal = tokenData;
703
+ const rawReal = this.normalizeAuthIdentifier(withReal.ruid);
704
+ if (rawReal === null) {
705
+ return effectiveUserId;
706
+ }
707
+ if (typeof rawReal === 'number' && rawReal === 0) {
708
+ return effectiveUserId;
709
+ }
710
+ return rawReal;
711
+ }
682
712
  handle_request(handler, auth) {
683
713
  return async (req, res, next) => {
684
714
  void next;
@@ -688,9 +718,22 @@ class ApiServer {
688
718
  res,
689
719
  token: '',
690
720
  tokenData: null,
721
+ realUid: null,
691
722
  getClientInfo: () => ensureClientInfo(apiReq),
692
723
  getClientIp: () => ensureClientInfo(apiReq).ip,
693
- getClientIpChain: () => ensureClientInfo(apiReq).ipchain
724
+ getClientIpChain: () => ensureClientInfo(apiReq).ipchain,
725
+ getRealUid: () => apiReq.realUid ?? null,
726
+ isImpersonating: () => {
727
+ const realUid = apiReq.realUid;
728
+ const tokenUid = apiReq.tokenData?.uid;
729
+ if (realUid === null || realUid === undefined) {
730
+ return false;
731
+ }
732
+ if (tokenUid === null || tokenUid === undefined) {
733
+ return false;
734
+ }
735
+ return realUid !== tokenUid;
736
+ }
694
737
  };
695
738
  this.currReq = apiReq;
696
739
  try {
@@ -9,7 +9,7 @@ import jwt, { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
9
9
  import { ApiModule } from './api-module.js';
10
10
  import type { ApiAuthClass, ApiKey } from './api-module.js';
11
11
  import type { AuthProviderModule } from './auth-module.js';
12
- import type { AuthStorage, AuthTokenData, AuthTokenMetadata } from './auth-storage.js';
12
+ import type { AuthStorage, AuthIdentifier, AuthTokenData, AuthTokenMetadata } from './auth-storage.js';
13
13
  export type { Application, Request, Response, NextFunction, Router } from 'express';
14
14
  export type { Multer } from 'multer';
15
15
  export type { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
@@ -49,9 +49,12 @@ export interface ApiRequest {
49
49
  authToken?: AuthTokenData | null;
50
50
  apiKey?: ApiKey | null;
51
51
  clientInfo?: ClientInfo;
52
+ realUid?: AuthIdentifier | null;
52
53
  getClientInfo: () => ClientInfo;
53
54
  getClientIp: () => string | null;
54
55
  getClientIpChain: () => string[];
56
+ getRealUid: () => AuthIdentifier | null;
57
+ isImpersonating: () => boolean;
55
58
  }
56
59
  export interface ClientAgentProfile {
57
60
  ua: string;
@@ -148,6 +151,9 @@ export declare class ApiServer {
148
151
  private requiresAuthToken;
149
152
  private shouldValidateStoredToken;
150
153
  private assertStoredAccessToken;
154
+ private normalizeAuthIdentifier;
155
+ private extractTokenUserId;
156
+ private resolveRealUserId;
151
157
  private handle_request;
152
158
  api<T extends ApiModule<any>>(module: T): this;
153
159
  dumpRequest(apiReq: ApiRequest): void;
@@ -82,6 +82,11 @@ class BaseAuthStorage {
82
82
  void clientId;
83
83
  return null;
84
84
  }
85
+ // Override to decide if a real user may impersonate another user
86
+ async canImpersonate(params) {
87
+ void params;
88
+ return false;
89
+ }
85
90
  }
86
91
  exports.BaseAuthStorage = BaseAuthStorage;
87
92
  exports.nullAuthStorage = new BaseAuthStorage();
@@ -1,5 +1,6 @@
1
1
  export type AuthIdentifier = string | number;
2
2
  export interface AuthTokenMetadata {
3
+ ruid?: AuthIdentifier;
3
4
  clientId?: string;
4
5
  domain?: string;
5
6
  fingerprint?: string;
@@ -101,6 +102,10 @@ export interface AuthStorage<UserRow, SafeUser> {
101
102
  verifyClientSecret?(client: OAuthClient, clientSecret: string | null): Promise<boolean>;
102
103
  createAuthCode?(request: AuthCodeRequest): Promise<AuthCodeData>;
103
104
  consumeAuthCode?(code: string, clientId: string): Promise<AuthCodeData | null>;
105
+ canImpersonate?(params: {
106
+ realUserId: AuthIdentifier;
107
+ effectiveUserId: AuthIdentifier;
108
+ }): Promise<boolean>;
104
109
  }
105
110
  export declare class BaseAuthStorage<UserRow = unknown, SafeUser = unknown> implements AuthStorage<UserRow, SafeUser> {
106
111
  getUser(identifier: AuthIdentifier): Promise<UserRow | null>;
@@ -120,5 +125,9 @@ export declare class BaseAuthStorage<UserRow = unknown, SafeUser = unknown> impl
120
125
  verifyClientSecret(client: OAuthClient, clientSecret: string | null): Promise<boolean>;
121
126
  createAuthCode(request: AuthCodeRequest): Promise<AuthCodeData>;
122
127
  consumeAuthCode(code: string, clientId: string): Promise<AuthCodeData | null>;
128
+ canImpersonate(params: {
129
+ realUserId: AuthIdentifier;
130
+ effectiveUserId: AuthIdentifier;
131
+ }): Promise<boolean>;
123
132
  }
124
133
  export declare const nullAuthStorage: AuthStorage<unknown, unknown>;
@@ -9,7 +9,7 @@ import jwt, { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
9
9
  import { ApiModule } from './api-module.js';
10
10
  import type { ApiAuthClass, ApiKey } from './api-module.js';
11
11
  import type { AuthProviderModule } from './auth-module.js';
12
- import type { AuthStorage, AuthTokenData, AuthTokenMetadata } from './auth-storage.js';
12
+ import type { AuthStorage, AuthIdentifier, AuthTokenData, AuthTokenMetadata } from './auth-storage.js';
13
13
  export type { Application, Request, Response, NextFunction, Router } from 'express';
14
14
  export type { Multer } from 'multer';
15
15
  export type { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
@@ -49,9 +49,12 @@ export interface ApiRequest {
49
49
  authToken?: AuthTokenData | null;
50
50
  apiKey?: ApiKey | null;
51
51
  clientInfo?: ClientInfo;
52
+ realUid?: AuthIdentifier | null;
52
53
  getClientInfo: () => ClientInfo;
53
54
  getClientIp: () => string | null;
54
55
  getClientIpChain: () => string[];
56
+ getRealUid: () => AuthIdentifier | null;
57
+ isImpersonating: () => boolean;
55
58
  }
56
59
  export interface ClientAgentProfile {
57
60
  ua: string;
@@ -148,6 +151,9 @@ export declare class ApiServer {
148
151
  private requiresAuthToken;
149
152
  private shouldValidateStoredToken;
150
153
  private assertStoredAccessToken;
154
+ private normalizeAuthIdentifier;
155
+ private extractTokenUserId;
156
+ private resolveRealUserId;
151
157
  private handle_request;
152
158
  api<T extends ApiModule<any>>(module: T): this;
153
159
  dumpRequest(apiReq: ApiRequest): void;
@@ -580,6 +580,7 @@ export class ApiServer {
580
580
  }
581
581
  async authenticate(apiReq, authType) {
582
582
  if (authType === 'none') {
583
+ apiReq.realUid = null;
583
584
  return null;
584
585
  }
585
586
  let token = null;
@@ -616,6 +617,8 @@ export class ApiServer {
616
617
  if (!tokenData) {
617
618
  throw new ApiError({ code: 401, message: 'Unathorized Access - ' + error });
618
619
  }
620
+ const effectiveUserId = this.extractTokenUserId(tokenData);
621
+ apiReq.realUid = this.resolveRealUserId(tokenData, effectiveUserId);
619
622
  if (this.shouldValidateStoredToken(authType)) {
620
623
  await this.assertStoredAccessToken(apiReq, token, tokenData);
621
624
  }
@@ -658,10 +661,7 @@ export class ApiServer {
658
661
  return this.config.validateTokens || authType === 'strict';
659
662
  }
660
663
  async assertStoredAccessToken(apiReq, token, tokenData) {
661
- if (typeof tokenData.uid !== 'string' && typeof tokenData.uid !== 'number') {
662
- throw new ApiError({ code: 401, message: 'Authorization token is malformed' });
663
- }
664
- const userId = tokenData.uid;
664
+ const userId = this.extractTokenUserId(tokenData);
665
665
  const stored = await this.storageAdapter.getToken({
666
666
  accessToken: token,
667
667
  userId
@@ -671,6 +671,36 @@ export class ApiServer {
671
671
  }
672
672
  apiReq.authToken = stored;
673
673
  }
674
+ normalizeAuthIdentifier(candidate) {
675
+ if (typeof candidate === 'number' && Number.isFinite(candidate)) {
676
+ return candidate;
677
+ }
678
+ if (typeof candidate === 'string') {
679
+ const trimmed = candidate.trim();
680
+ if (trimmed.length > 0) {
681
+ return trimmed;
682
+ }
683
+ }
684
+ return null;
685
+ }
686
+ extractTokenUserId(tokenData) {
687
+ const normalized = this.normalizeAuthIdentifier(tokenData.uid);
688
+ if (normalized === null) {
689
+ throw new ApiError({ code: 401, message: 'Authorization token is malformed' });
690
+ }
691
+ return normalized;
692
+ }
693
+ resolveRealUserId(tokenData, effectiveUserId) {
694
+ const withReal = tokenData;
695
+ const rawReal = this.normalizeAuthIdentifier(withReal.ruid);
696
+ if (rawReal === null) {
697
+ return effectiveUserId;
698
+ }
699
+ if (typeof rawReal === 'number' && rawReal === 0) {
700
+ return effectiveUserId;
701
+ }
702
+ return rawReal;
703
+ }
674
704
  handle_request(handler, auth) {
675
705
  return async (req, res, next) => {
676
706
  void next;
@@ -680,9 +710,22 @@ export class ApiServer {
680
710
  res,
681
711
  token: '',
682
712
  tokenData: null,
713
+ realUid: null,
683
714
  getClientInfo: () => ensureClientInfo(apiReq),
684
715
  getClientIp: () => ensureClientInfo(apiReq).ip,
685
- getClientIpChain: () => ensureClientInfo(apiReq).ipchain
716
+ getClientIpChain: () => ensureClientInfo(apiReq).ipchain,
717
+ getRealUid: () => apiReq.realUid ?? null,
718
+ isImpersonating: () => {
719
+ const realUid = apiReq.realUid;
720
+ const tokenUid = apiReq.tokenData?.uid;
721
+ if (realUid === null || realUid === undefined) {
722
+ return false;
723
+ }
724
+ if (tokenUid === null || tokenUid === undefined) {
725
+ return false;
726
+ }
727
+ return realUid !== tokenUid;
728
+ }
686
729
  };
687
730
  this.currReq = apiReq;
688
731
  try {
@@ -1,5 +1,6 @@
1
1
  export type AuthIdentifier = string | number;
2
2
  export interface AuthTokenMetadata {
3
+ ruid?: AuthIdentifier;
3
4
  clientId?: string;
4
5
  domain?: string;
5
6
  fingerprint?: string;
@@ -101,6 +102,10 @@ export interface AuthStorage<UserRow, SafeUser> {
101
102
  verifyClientSecret?(client: OAuthClient, clientSecret: string | null): Promise<boolean>;
102
103
  createAuthCode?(request: AuthCodeRequest): Promise<AuthCodeData>;
103
104
  consumeAuthCode?(code: string, clientId: string): Promise<AuthCodeData | null>;
105
+ canImpersonate?(params: {
106
+ realUserId: AuthIdentifier;
107
+ effectiveUserId: AuthIdentifier;
108
+ }): Promise<boolean>;
104
109
  }
105
110
  export declare class BaseAuthStorage<UserRow = unknown, SafeUser = unknown> implements AuthStorage<UserRow, SafeUser> {
106
111
  getUser(identifier: AuthIdentifier): Promise<UserRow | null>;
@@ -120,5 +125,9 @@ export declare class BaseAuthStorage<UserRow = unknown, SafeUser = unknown> impl
120
125
  verifyClientSecret(client: OAuthClient, clientSecret: string | null): Promise<boolean>;
121
126
  createAuthCode(request: AuthCodeRequest): Promise<AuthCodeData>;
122
127
  consumeAuthCode(code: string, clientId: string): Promise<AuthCodeData | null>;
128
+ canImpersonate(params: {
129
+ realUserId: AuthIdentifier;
130
+ effectiveUserId: AuthIdentifier;
131
+ }): Promise<boolean>;
123
132
  }
124
133
  export declare const nullAuthStorage: AuthStorage<unknown, unknown>;
@@ -79,5 +79,10 @@ export class BaseAuthStorage {
79
79
  void clientId;
80
80
  return null;
81
81
  }
82
+ // Override to decide if a real user may impersonate another user
83
+ async canImpersonate(params) {
84
+ void params;
85
+ return false;
86
+ }
82
87
  }
83
88
  export const nullAuthStorage = new BaseAuthStorage();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@technomoron/api-server-base",
3
- "version": "1.1.9",
3
+ "version": "1.1.10",
4
4
  "description": "Api Server Skeleton / Base Class",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.cjs",