@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.
- package/dist/cjs/api-server-base.cjs +52 -10
- package/dist/cjs/api-server-base.d.ts +7 -1
- package/dist/cjs/auth-storage.cjs +5 -0
- package/dist/cjs/auth-storage.d.ts +9 -0
- package/dist/esm/api-server-base.d.ts +7 -1
- package/dist/esm/api-server-base.js +52 -10
- package/dist/esm/auth-storage.d.ts +9 -0
- package/dist/esm/auth-storage.js +5 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>;
|
package/dist/esm/auth-storage.js
CHANGED
|
@@ -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();
|