@technomoron/api-server-base 1.1.8 → 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.
- package/dist/cjs/api-server-base.cjs +48 -5
- 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 +48 -5
- 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;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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>;
|
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();
|