@technomoron/api-server-base 1.1.9 → 1.1.11

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;
@@ -600,15 +601,14 @@ class ApiServer {
600
601
  if (authHeader?.startsWith('Bearer ')) {
601
602
  token = authHeader.slice(7).trim();
602
603
  }
603
- else if (requiresAuthToken && !authHeader) {
604
- throw new ApiError({ code: 401, message: 'Authorization header is missing or invalid' });
605
- }
606
- if (!token || token === null) {
604
+ if (!token) {
607
605
  const access = apiReq.req.cookies?.dat;
608
606
  if (access) {
609
607
  token = access;
610
608
  }
611
- else if (requiresAuthToken) {
609
+ }
610
+ if (!token || token === null) {
611
+ if (requiresAuthToken) {
612
612
  throw new ApiError({ code: 401, message: 'Authorization token is required (Bearer/cookie)' });
613
613
  }
614
614
  }
@@ -624,6 +624,8 @@ class ApiServer {
624
624
  if (!tokenData) {
625
625
  throw new ApiError({ code: 401, message: 'Unathorized Access - ' + error });
626
626
  }
627
+ const effectiveUserId = this.extractTokenUserId(tokenData);
628
+ apiReq.realUid = this.resolveRealUserId(tokenData, effectiveUserId);
627
629
  if (this.shouldValidateStoredToken(authType)) {
628
630
  await this.assertStoredAccessToken(apiReq, token, tokenData);
629
631
  }
@@ -666,10 +668,7 @@ class ApiServer {
666
668
  return this.config.validateTokens || authType === 'strict';
667
669
  }
668
670
  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;
671
+ const userId = this.extractTokenUserId(tokenData);
673
672
  const stored = await this.storageAdapter.getToken({
674
673
  accessToken: token,
675
674
  userId
@@ -679,6 +678,36 @@ class ApiServer {
679
678
  }
680
679
  apiReq.authToken = stored;
681
680
  }
681
+ normalizeAuthIdentifier(candidate) {
682
+ if (typeof candidate === 'number' && Number.isFinite(candidate)) {
683
+ return candidate;
684
+ }
685
+ if (typeof candidate === 'string') {
686
+ const trimmed = candidate.trim();
687
+ if (trimmed.length > 0) {
688
+ return trimmed;
689
+ }
690
+ }
691
+ return null;
692
+ }
693
+ extractTokenUserId(tokenData) {
694
+ const normalized = this.normalizeAuthIdentifier(tokenData.uid);
695
+ if (normalized === null) {
696
+ throw new ApiError({ code: 401, message: 'Authorization token is malformed' });
697
+ }
698
+ return normalized;
699
+ }
700
+ resolveRealUserId(tokenData, effectiveUserId) {
701
+ const withReal = tokenData;
702
+ const rawReal = this.normalizeAuthIdentifier(withReal.ruid);
703
+ if (rawReal === null) {
704
+ return effectiveUserId;
705
+ }
706
+ if (typeof rawReal === 'number' && rawReal === 0) {
707
+ return effectiveUserId;
708
+ }
709
+ return rawReal;
710
+ }
682
711
  handle_request(handler, auth) {
683
712
  return async (req, res, next) => {
684
713
  void next;
@@ -688,9 +717,22 @@ class ApiServer {
688
717
  res,
689
718
  token: '',
690
719
  tokenData: null,
720
+ realUid: null,
691
721
  getClientInfo: () => ensureClientInfo(apiReq),
692
722
  getClientIp: () => ensureClientInfo(apiReq).ip,
693
- getClientIpChain: () => ensureClientInfo(apiReq).ipchain
723
+ getClientIpChain: () => ensureClientInfo(apiReq).ipchain,
724
+ getRealUid: () => apiReq.realUid ?? null,
725
+ isImpersonating: () => {
726
+ const realUid = apiReq.realUid;
727
+ const tokenUid = apiReq.tokenData?.uid;
728
+ if (realUid === null || realUid === undefined) {
729
+ return false;
730
+ }
731
+ if (tokenUid === null || tokenUid === undefined) {
732
+ return false;
733
+ }
734
+ return realUid !== tokenUid;
735
+ }
694
736
  };
695
737
  this.currReq = apiReq;
696
738
  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;
@@ -592,15 +593,14 @@ export class ApiServer {
592
593
  if (authHeader?.startsWith('Bearer ')) {
593
594
  token = authHeader.slice(7).trim();
594
595
  }
595
- else if (requiresAuthToken && !authHeader) {
596
- throw new ApiError({ code: 401, message: 'Authorization header is missing or invalid' });
597
- }
598
- if (!token || token === null) {
596
+ if (!token) {
599
597
  const access = apiReq.req.cookies?.dat;
600
598
  if (access) {
601
599
  token = access;
602
600
  }
603
- else if (requiresAuthToken) {
601
+ }
602
+ if (!token || token === null) {
603
+ if (requiresAuthToken) {
604
604
  throw new ApiError({ code: 401, message: 'Authorization token is required (Bearer/cookie)' });
605
605
  }
606
606
  }
@@ -616,6 +616,8 @@ export class ApiServer {
616
616
  if (!tokenData) {
617
617
  throw new ApiError({ code: 401, message: 'Unathorized Access - ' + error });
618
618
  }
619
+ const effectiveUserId = this.extractTokenUserId(tokenData);
620
+ apiReq.realUid = this.resolveRealUserId(tokenData, effectiveUserId);
619
621
  if (this.shouldValidateStoredToken(authType)) {
620
622
  await this.assertStoredAccessToken(apiReq, token, tokenData);
621
623
  }
@@ -658,10 +660,7 @@ export class ApiServer {
658
660
  return this.config.validateTokens || authType === 'strict';
659
661
  }
660
662
  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;
663
+ const userId = this.extractTokenUserId(tokenData);
665
664
  const stored = await this.storageAdapter.getToken({
666
665
  accessToken: token,
667
666
  userId
@@ -671,6 +670,36 @@ export class ApiServer {
671
670
  }
672
671
  apiReq.authToken = stored;
673
672
  }
673
+ normalizeAuthIdentifier(candidate) {
674
+ if (typeof candidate === 'number' && Number.isFinite(candidate)) {
675
+ return candidate;
676
+ }
677
+ if (typeof candidate === 'string') {
678
+ const trimmed = candidate.trim();
679
+ if (trimmed.length > 0) {
680
+ return trimmed;
681
+ }
682
+ }
683
+ return null;
684
+ }
685
+ extractTokenUserId(tokenData) {
686
+ const normalized = this.normalizeAuthIdentifier(tokenData.uid);
687
+ if (normalized === null) {
688
+ throw new ApiError({ code: 401, message: 'Authorization token is malformed' });
689
+ }
690
+ return normalized;
691
+ }
692
+ resolveRealUserId(tokenData, effectiveUserId) {
693
+ const withReal = tokenData;
694
+ const rawReal = this.normalizeAuthIdentifier(withReal.ruid);
695
+ if (rawReal === null) {
696
+ return effectiveUserId;
697
+ }
698
+ if (typeof rawReal === 'number' && rawReal === 0) {
699
+ return effectiveUserId;
700
+ }
701
+ return rawReal;
702
+ }
674
703
  handle_request(handler, auth) {
675
704
  return async (req, res, next) => {
676
705
  void next;
@@ -680,9 +709,22 @@ export class ApiServer {
680
709
  res,
681
710
  token: '',
682
711
  tokenData: null,
712
+ realUid: null,
683
713
  getClientInfo: () => ensureClientInfo(apiReq),
684
714
  getClientIp: () => ensureClientInfo(apiReq).ip,
685
- getClientIpChain: () => ensureClientInfo(apiReq).ipchain
715
+ getClientIpChain: () => ensureClientInfo(apiReq).ipchain,
716
+ getRealUid: () => apiReq.realUid ?? null,
717
+ isImpersonating: () => {
718
+ const realUid = apiReq.realUid;
719
+ const tokenUid = apiReq.tokenData?.uid;
720
+ if (realUid === null || realUid === undefined) {
721
+ return false;
722
+ }
723
+ if (tokenUid === null || tokenUid === undefined) {
724
+ return false;
725
+ }
726
+ return realUid !== tokenUid;
727
+ }
686
728
  };
687
729
  this.currReq = apiReq;
688
730
  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.11",
4
4
  "description": "Api Server Skeleton / Base Class",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.cjs",