@medi-car-bd/mcb-shared-auth 0.0.7 → 0.0.12
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/README.md +5 -13
- package/dist/config/env.d.ts +2 -1
- package/dist/config/env.js +3 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/jwt.d.ts +97 -0
- package/dist/types/jwt.js +10 -0
- package/dist/types/userType.d.ts +1 -1
- package/dist/utils/generateAccessToken.d.ts +15 -0
- package/dist/utils/generateAccessToken.js +31 -0
- package/dist/utils/generateRefreshToken.d.ts +10 -0
- package/dist/utils/generateRefreshToken.js +30 -0
- package/dist/utils/generateTokenId.d.ts +4 -0
- package/dist/utils/generateTokenId.js +11 -0
- package/dist/utils/generateTokenPair.d.ts +2 -0
- package/dist/utils/generateTokenPair.js +53 -0
- package/dist/utils/hashToken.d.ts +4 -0
- package/dist/utils/hashToken.js +11 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/rotateRefreshToken.d.ts +9 -0
- package/dist/utils/rotateRefreshToken.js +153 -0
- package/dist/utils/signToken.js +0 -1
- package/package.json +64 -63
package/README.md
CHANGED
|
@@ -32,21 +32,13 @@ pnpm prisma:migrate
|
|
|
32
32
|
- Shared Auth package installation
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
|
|
35
|
+
npm login
|
|
36
36
|
|
|
37
|
-
npm
|
|
38
|
-
|
|
39
|
-
pnpm install @mcb-shared-auth/auth
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
#Or Use
|
|
37
|
+
npm version patch
|
|
43
38
|
|
|
44
|
-
|
|
45
|
-
pnpm install github:Medi-Car-BD/mcb-shared-auth
|
|
46
|
-
```
|
|
39
|
+
npm config set //registry.npmjs.org/:_authToken=npm_xxxxxxxxxxxxxxxxx
|
|
47
40
|
|
|
48
|
-
|
|
41
|
+
npm publish --access public
|
|
49
42
|
|
|
50
|
-
|
|
51
|
-
npm version patch
|
|
43
|
+
pnpm install @mcb-shared-auth/auth
|
|
52
44
|
```
|
package/dist/config/env.d.ts
CHANGED
|
@@ -2,4 +2,5 @@ import "dotenv/config";
|
|
|
2
2
|
declare const NODE_ENV: string | undefined;
|
|
3
3
|
declare const JWT_SECRET: string | undefined;
|
|
4
4
|
declare const JWT_EXPIRES_IN: string | undefined;
|
|
5
|
-
|
|
5
|
+
declare const GITHUB_TOKEN: string | undefined;
|
|
6
|
+
export { NODE_ENV, JWT_SECRET, JWT_EXPIRES_IN, GITHUB_TOKEN };
|
package/dist/config/env.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.JWT_EXPIRES_IN = exports.JWT_SECRET = exports.NODE_ENV = void 0;
|
|
3
|
+
exports.GITHUB_TOKEN = exports.JWT_EXPIRES_IN = exports.JWT_SECRET = exports.NODE_ENV = void 0;
|
|
4
4
|
require("dotenv/config");
|
|
5
5
|
const NODE_ENV = process.env.NODE_ENV;
|
|
6
6
|
exports.NODE_ENV = NODE_ENV;
|
|
@@ -8,3 +8,5 @@ const JWT_SECRET = process.env.JWT_SECRET;
|
|
|
8
8
|
exports.JWT_SECRET = JWT_SECRET;
|
|
9
9
|
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN;
|
|
10
10
|
exports.JWT_EXPIRES_IN = JWT_EXPIRES_IN;
|
|
11
|
+
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
|
|
12
|
+
exports.GITHUB_TOKEN = GITHUB_TOKEN;
|
package/dist/types/index.d.ts
CHANGED
package/dist/types/index.js
CHANGED
|
@@ -16,3 +16,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./userType"), exports);
|
|
18
18
|
__exportStar(require("./commonType"), exports);
|
|
19
|
+
__exportStar(require("./jwt"), exports);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The user shape that token utilities accept.
|
|
3
|
+
*
|
|
4
|
+
* `role` and `tenantId` are authorization claims, not decoration: `authorize`
|
|
5
|
+
* reads `role` and every client scopes data by `tenantId`, so they must survive
|
|
6
|
+
* both initial issuance and refresh rotation. Persistence shape (Prisma) allows
|
|
7
|
+
* nulls; the signed payload omits empty values.
|
|
8
|
+
*/
|
|
9
|
+
export interface AccessTokenPayload {
|
|
10
|
+
sub: string;
|
|
11
|
+
email: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
role?: string;
|
|
14
|
+
tenantId?: string;
|
|
15
|
+
type: "access";
|
|
16
|
+
jti?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface RefreshTokenPayload {
|
|
19
|
+
sub: string;
|
|
20
|
+
type: "refresh";
|
|
21
|
+
jti: string;
|
|
22
|
+
}
|
|
23
|
+
export interface TokenConfig {
|
|
24
|
+
accessTokenExpiresIn: string | number;
|
|
25
|
+
refreshTokenExpiresIn: string | number;
|
|
26
|
+
jwtSecret: string;
|
|
27
|
+
refreshTokenSecret?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface RefreshTokenRecord {
|
|
30
|
+
id: string;
|
|
31
|
+
userId: string;
|
|
32
|
+
token: string;
|
|
33
|
+
deviceInfo?: string;
|
|
34
|
+
ipAddress?: string;
|
|
35
|
+
expiresAt: Date;
|
|
36
|
+
revokedAt?: Date | null;
|
|
37
|
+
replacedBy?: string | null;
|
|
38
|
+
lastUsedAt?: Date | null;
|
|
39
|
+
user?: {
|
|
40
|
+
id: string;
|
|
41
|
+
email: string;
|
|
42
|
+
name?: string | null;
|
|
43
|
+
role?: string | null;
|
|
44
|
+
tenantId?: string | null;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export interface RefreshTokenRepository {
|
|
48
|
+
create(record: RefreshTokenRecord): Promise<void>;
|
|
49
|
+
findById(id: string): Promise<RefreshTokenRecord | null>;
|
|
50
|
+
update(id: string, data: Partial<RefreshTokenRecord>): Promise<void>;
|
|
51
|
+
updateMany(where: {
|
|
52
|
+
userId: string;
|
|
53
|
+
revokedAt?: null;
|
|
54
|
+
id?: {
|
|
55
|
+
not: string;
|
|
56
|
+
};
|
|
57
|
+
}, data: {
|
|
58
|
+
revokedAt: Date;
|
|
59
|
+
}): Promise<{
|
|
60
|
+
count: number;
|
|
61
|
+
}>;
|
|
62
|
+
}
|
|
63
|
+
export interface GenerateTokenPairParams {
|
|
64
|
+
user: {
|
|
65
|
+
id: string;
|
|
66
|
+
email: string;
|
|
67
|
+
name?: string | null;
|
|
68
|
+
role?: string | null;
|
|
69
|
+
tenantId?: string | null;
|
|
70
|
+
};
|
|
71
|
+
deviceInfo?: string;
|
|
72
|
+
ipAddress?: string;
|
|
73
|
+
config: TokenConfig;
|
|
74
|
+
repo: RefreshTokenRepository;
|
|
75
|
+
}
|
|
76
|
+
export interface TokenPair {
|
|
77
|
+
accessToken: string;
|
|
78
|
+
refreshToken: string;
|
|
79
|
+
}
|
|
80
|
+
export interface RevokeAllUserTokensParams {
|
|
81
|
+
userId: string;
|
|
82
|
+
excludeTokenId?: string;
|
|
83
|
+
reason?: "password_change" | "security" | "admin_revocation";
|
|
84
|
+
repo: RefreshTokenRepository;
|
|
85
|
+
}
|
|
86
|
+
export interface RotateRefreshTokenParams {
|
|
87
|
+
oldRefreshToken: string;
|
|
88
|
+
deviceInfo?: string;
|
|
89
|
+
ipAddress?: string;
|
|
90
|
+
config: TokenConfig;
|
|
91
|
+
repo: RefreshTokenRepository;
|
|
92
|
+
}
|
|
93
|
+
export interface RotateRefreshTokenResult {
|
|
94
|
+
accessToken: string;
|
|
95
|
+
refreshToken: string;
|
|
96
|
+
userId: string;
|
|
97
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The user shape that token utilities accept.
|
|
4
|
+
*
|
|
5
|
+
* `role` and `tenantId` are authorization claims, not decoration: `authorize`
|
|
6
|
+
* reads `role` and every client scopes data by `tenantId`, so they must survive
|
|
7
|
+
* both initial issuance and refresh rotation. Persistence shape (Prisma) allows
|
|
8
|
+
* nulls; the signed payload omits empty values.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
package/dist/types/userType.d.ts
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { TokenConfig } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Generate a new access token
|
|
4
|
+
*
|
|
5
|
+
* `role` and `tenantId` are signed into the payload because clients authorize
|
|
6
|
+
* and scope data from the token alone, without a database round trip. Empty
|
|
7
|
+
* values are omitted rather than serialized as null.
|
|
8
|
+
*/
|
|
9
|
+
export declare const generateAccessToken: (user: {
|
|
10
|
+
id: string;
|
|
11
|
+
email: string;
|
|
12
|
+
name?: string | null;
|
|
13
|
+
role?: string | null;
|
|
14
|
+
tenantId?: string | null;
|
|
15
|
+
}, config: TokenConfig, jti?: string) => string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.generateAccessToken = void 0;
|
|
7
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
8
|
+
/**
|
|
9
|
+
* Generate a new access token
|
|
10
|
+
*
|
|
11
|
+
* `role` and `tenantId` are signed into the payload because clients authorize
|
|
12
|
+
* and scope data from the token alone, without a database round trip. Empty
|
|
13
|
+
* values are omitted rather than serialized as null.
|
|
14
|
+
*/
|
|
15
|
+
const generateAccessToken = (user, config, jti) => {
|
|
16
|
+
const payload = {
|
|
17
|
+
sub: user.id,
|
|
18
|
+
email: user.email,
|
|
19
|
+
name: user.name || undefined,
|
|
20
|
+
role: user.role || undefined,
|
|
21
|
+
tenantId: user.tenantId || undefined,
|
|
22
|
+
type: "access",
|
|
23
|
+
jti,
|
|
24
|
+
};
|
|
25
|
+
return jsonwebtoken_1.default.sign(payload, config.jwtSecret, {
|
|
26
|
+
expiresIn: config.accessTokenExpiresIn,
|
|
27
|
+
audience: "mcb-services",
|
|
28
|
+
issuer: "mcb-auth",
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
exports.generateAccessToken = generateAccessToken;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TokenConfig } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Generate a new refresh token (returns both the raw token and its hash)
|
|
4
|
+
*/
|
|
5
|
+
export declare const generateRefreshToken: (user: {
|
|
6
|
+
id: string;
|
|
7
|
+
}, config: TokenConfig, tokenId: string) => {
|
|
8
|
+
token: string;
|
|
9
|
+
hashedToken: string;
|
|
10
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.generateRefreshToken = void 0;
|
|
7
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
8
|
+
const hashToken_1 = require("./hashToken");
|
|
9
|
+
/**
|
|
10
|
+
* Generate a new refresh token (returns both the raw token and its hash)
|
|
11
|
+
*/
|
|
12
|
+
const generateRefreshToken = (user, config, tokenId) => {
|
|
13
|
+
const payload = {
|
|
14
|
+
sub: user.id,
|
|
15
|
+
type: "refresh",
|
|
16
|
+
jti: tokenId,
|
|
17
|
+
};
|
|
18
|
+
// Use refresh token secret if provided, otherwise use main secret
|
|
19
|
+
const secret = config.refreshTokenSecret || config.jwtSecret;
|
|
20
|
+
const token = jsonwebtoken_1.default.sign(payload, secret, {
|
|
21
|
+
expiresIn: config.refreshTokenExpiresIn,
|
|
22
|
+
audience: "mcb-services",
|
|
23
|
+
issuer: "mcb-auth",
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
token,
|
|
27
|
+
hashedToken: (0, hashToken_1.hashToken)(token),
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
exports.generateRefreshToken = generateRefreshToken;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateTokenId = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
/**
|
|
6
|
+
* Generate a unique token ID
|
|
7
|
+
*/
|
|
8
|
+
const generateTokenId = () => {
|
|
9
|
+
return (0, crypto_1.randomBytes)(32).toString("hex");
|
|
10
|
+
};
|
|
11
|
+
exports.generateTokenId = generateTokenId;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.generateTokenPair = void 0;
|
|
13
|
+
const generateRefreshToken_1 = require("./generateRefreshToken");
|
|
14
|
+
const generateAccessToken_1 = require("./generateAccessToken");
|
|
15
|
+
const generateTokenId_1 = require("./generateTokenId");
|
|
16
|
+
/**
|
|
17
|
+
* Store the refresh token in the database
|
|
18
|
+
*/
|
|
19
|
+
const storeRefreshToken = (tokenData, repo) => __awaiter(void 0, void 0, void 0, function* () {
|
|
20
|
+
yield repo.create(tokenData);
|
|
21
|
+
});
|
|
22
|
+
const generateTokenPair = (_a) => __awaiter(void 0, [_a], void 0, function* ({ user, deviceInfo, ipAddress, config, repo, }) {
|
|
23
|
+
try {
|
|
24
|
+
// Generate unique token ID for refresh token
|
|
25
|
+
const tokenId = (0, generateTokenId_1.generateTokenId)();
|
|
26
|
+
// Generate refresh token (raw and hashed)
|
|
27
|
+
const { token: refreshToken, hashedToken } = (0, generateRefreshToken_1.generateRefreshToken)(user, config, tokenId);
|
|
28
|
+
// Store refresh token in database
|
|
29
|
+
yield storeRefreshToken({
|
|
30
|
+
id: tokenId,
|
|
31
|
+
userId: user.id,
|
|
32
|
+
token: hashedToken,
|
|
33
|
+
deviceInfo,
|
|
34
|
+
ipAddress,
|
|
35
|
+
expiresAt: typeof config.refreshTokenExpiresIn === "number"
|
|
36
|
+
? new Date(Date.now() + config.refreshTokenExpiresIn * 1000)
|
|
37
|
+
: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
|
38
|
+
revokedAt: null,
|
|
39
|
+
replacedBy: null,
|
|
40
|
+
lastUsedAt: null,
|
|
41
|
+
}, repo);
|
|
42
|
+
// Generate access token with optional reference to refresh token
|
|
43
|
+
const accessToken = (0, generateAccessToken_1.generateAccessToken)(user, config, tokenId);
|
|
44
|
+
return {
|
|
45
|
+
accessToken,
|
|
46
|
+
refreshToken,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
throw new Error(`Failed to generate token pair: ${error instanceof Error ? error.message : String(error)}`);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
exports.generateTokenPair = generateTokenPair;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hashToken = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
/**
|
|
6
|
+
* Hash a token for storage (never store raw tokens in DB)
|
|
7
|
+
*/
|
|
8
|
+
const hashToken = (token) => {
|
|
9
|
+
return (0, crypto_1.createHash)("sha256").update(token).digest("hex");
|
|
10
|
+
};
|
|
11
|
+
exports.hashToken = hashToken;
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -5,3 +5,9 @@ export * from "./comparePassword";
|
|
|
5
5
|
export * from "./signToken";
|
|
6
6
|
export * from "./globalError";
|
|
7
7
|
export * from "./limiter";
|
|
8
|
+
export * from "./generateTokenPair";
|
|
9
|
+
export * from "./generateRefreshToken";
|
|
10
|
+
export * from "./rotateRefreshToken";
|
|
11
|
+
export * from "./generateAccessToken";
|
|
12
|
+
export * from "./hashToken";
|
|
13
|
+
export * from "./generateTokenId";
|
package/dist/utils/index.js
CHANGED
|
@@ -21,3 +21,9 @@ __exportStar(require("./comparePassword"), exports);
|
|
|
21
21
|
__exportStar(require("./signToken"), exports);
|
|
22
22
|
__exportStar(require("./globalError"), exports);
|
|
23
23
|
__exportStar(require("./limiter"), exports);
|
|
24
|
+
__exportStar(require("./generateTokenPair"), exports);
|
|
25
|
+
__exportStar(require("./generateRefreshToken"), exports);
|
|
26
|
+
__exportStar(require("./rotateRefreshToken"), exports);
|
|
27
|
+
__exportStar(require("./generateAccessToken"), exports);
|
|
28
|
+
__exportStar(require("./hashToken"), exports);
|
|
29
|
+
__exportStar(require("./generateTokenId"), exports);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RevokeAllUserTokensParams, RotateRefreshTokenParams, RotateRefreshTokenResult } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Main function to rotate refresh token
|
|
4
|
+
*/
|
|
5
|
+
export declare const rotateRefreshToken: ({ oldRefreshToken, deviceInfo, ipAddress, config, repo, }: RotateRefreshTokenParams) => Promise<RotateRefreshTokenResult>;
|
|
6
|
+
/**
|
|
7
|
+
* Revoke all tokens for a user
|
|
8
|
+
*/
|
|
9
|
+
export declare const revokeAllUserTokens: ({ userId, excludeTokenId, reason, repo, }: RevokeAllUserTokensParams) => Promise<number>;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// In mcb-shared-auth
|
|
3
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
4
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
5
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
6
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
7
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
8
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
9
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
13
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
14
|
+
};
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.revokeAllUserTokens = exports.rotateRefreshToken = void 0;
|
|
17
|
+
const crypto_1 = require("crypto");
|
|
18
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
19
|
+
const generateTokenPair_1 = require("./generateTokenPair");
|
|
20
|
+
/**
|
|
21
|
+
* Verify and decode refresh token
|
|
22
|
+
*/
|
|
23
|
+
const verifyRefreshToken = (token, config) => __awaiter(void 0, void 0, void 0, function* () {
|
|
24
|
+
try {
|
|
25
|
+
const secret = config.refreshTokenSecret || config.jwtSecret;
|
|
26
|
+
const decoded = jsonwebtoken_1.default.verify(token, secret, {
|
|
27
|
+
audience: "mcb-services",
|
|
28
|
+
issuer: "mcb-auth",
|
|
29
|
+
});
|
|
30
|
+
if (decoded.type !== "refresh") {
|
|
31
|
+
throw new Error("Invalid token type");
|
|
32
|
+
}
|
|
33
|
+
return decoded;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new Error("Invalid refresh token: " + error.message);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* Find and validate stored refresh token
|
|
41
|
+
*/
|
|
42
|
+
const findAndValidateStoredToken = (token, jti, repo) => __awaiter(void 0, void 0, void 0, function* () {
|
|
43
|
+
const hashedToken = (0, crypto_1.createHash)("sha256").update(token).digest("hex");
|
|
44
|
+
const storedToken = yield repo.findById(jti);
|
|
45
|
+
if (!storedToken) {
|
|
46
|
+
throw new Error("Refresh token not found");
|
|
47
|
+
}
|
|
48
|
+
// Check if token matches
|
|
49
|
+
if (storedToken.token !== hashedToken) {
|
|
50
|
+
throw new Error("Token mismatch");
|
|
51
|
+
}
|
|
52
|
+
// Check if token is revoked
|
|
53
|
+
if (storedToken.revokedAt) {
|
|
54
|
+
throw new Error("Token has been revoked");
|
|
55
|
+
}
|
|
56
|
+
// Check if token has been replaced
|
|
57
|
+
if (storedToken.replacedBy) {
|
|
58
|
+
throw new Error("Token has already been used (possible replay attack)");
|
|
59
|
+
}
|
|
60
|
+
// Check if token is expired
|
|
61
|
+
if (storedToken.expiresAt < new Date()) {
|
|
62
|
+
throw new Error("Refresh token expired");
|
|
63
|
+
}
|
|
64
|
+
return storedToken;
|
|
65
|
+
});
|
|
66
|
+
/**
|
|
67
|
+
* Mark old token as replaced
|
|
68
|
+
*/
|
|
69
|
+
const markTokenAsReplaced = (oldTokenId, newTokenId, repo) => __awaiter(void 0, void 0, void 0, function* () {
|
|
70
|
+
yield repo.update(oldTokenId, {
|
|
71
|
+
replacedBy: newTokenId,
|
|
72
|
+
lastUsedAt: new Date(),
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
/**
|
|
76
|
+
* Update last used timestamp
|
|
77
|
+
*/
|
|
78
|
+
const updateTokenLastUsed = (tokenId, ipAddress, repo) => __awaiter(void 0, void 0, void 0, function* () {
|
|
79
|
+
if (repo) {
|
|
80
|
+
yield repo.update(tokenId, {
|
|
81
|
+
lastUsedAt: new Date(),
|
|
82
|
+
ipAddress,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
/**
|
|
87
|
+
* Main function to rotate refresh token
|
|
88
|
+
*/
|
|
89
|
+
const rotateRefreshToken = (_a) => __awaiter(void 0, [_a], void 0, function* ({ oldRefreshToken, deviceInfo, ipAddress, config, repo, }) {
|
|
90
|
+
try {
|
|
91
|
+
// 1. Verify the old refresh token
|
|
92
|
+
const decoded = yield verifyRefreshToken(oldRefreshToken, config);
|
|
93
|
+
// 2. Find and validate stored token
|
|
94
|
+
const storedToken = yield findAndValidateStoredToken(oldRefreshToken, decoded.jti, repo);
|
|
95
|
+
// 3. Generate new token pair
|
|
96
|
+
const newTokens = yield (0, generateTokenPair_1.generateTokenPair)({
|
|
97
|
+
user: storedToken.user,
|
|
98
|
+
deviceInfo: deviceInfo || storedToken.deviceInfo,
|
|
99
|
+
ipAddress: ipAddress || storedToken.ipAddress,
|
|
100
|
+
config,
|
|
101
|
+
repo,
|
|
102
|
+
});
|
|
103
|
+
// 4. Decode new refresh token to get its JTI
|
|
104
|
+
const newDecoded = jsonwebtoken_1.default.decode(newTokens.refreshToken);
|
|
105
|
+
// 5. Mark old token as replaced
|
|
106
|
+
yield markTokenAsReplaced(storedToken.id, newDecoded.jti, repo);
|
|
107
|
+
// 6. Update last used on old token (optional, can be omitted if replaced)
|
|
108
|
+
yield updateTokenLastUsed(storedToken.id, ipAddress, repo);
|
|
109
|
+
return {
|
|
110
|
+
accessToken: newTokens.accessToken,
|
|
111
|
+
refreshToken: newTokens.refreshToken,
|
|
112
|
+
userId: storedToken.userId,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
// Log security events for monitoring
|
|
117
|
+
console.error("Token rotation failed:", {
|
|
118
|
+
error: error.message,
|
|
119
|
+
ipAddress,
|
|
120
|
+
deviceInfo,
|
|
121
|
+
timestamp: new Date().toISOString(),
|
|
122
|
+
});
|
|
123
|
+
throw new Error(`Token rotation failed: ${error.message}`);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
exports.rotateRefreshToken = rotateRefreshToken;
|
|
127
|
+
/**
|
|
128
|
+
* Revoke all tokens for a user
|
|
129
|
+
*/
|
|
130
|
+
const revokeAllUserTokens = (_a) => __awaiter(void 0, [_a], void 0, function* ({ userId, excludeTokenId, reason = "security", repo, }) {
|
|
131
|
+
try {
|
|
132
|
+
const where = {
|
|
133
|
+
userId,
|
|
134
|
+
revokedAt: null,
|
|
135
|
+
};
|
|
136
|
+
if (excludeTokenId) {
|
|
137
|
+
where.id = { not: excludeTokenId };
|
|
138
|
+
}
|
|
139
|
+
const result = yield repo.updateMany(where, { revokedAt: new Date() });
|
|
140
|
+
// Log security event
|
|
141
|
+
console.log("Tokens revoked:", {
|
|
142
|
+
userId,
|
|
143
|
+
count: result.count,
|
|
144
|
+
reason,
|
|
145
|
+
timestamp: new Date().toISOString(),
|
|
146
|
+
});
|
|
147
|
+
return result.count;
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
throw new Error(`Failed to revoke user tokens: ${error.message}`);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
exports.revokeAllUserTokens = revokeAllUserTokens;
|
package/dist/utils/signToken.js
CHANGED
package/package.json
CHANGED
|
@@ -1,63 +1,64 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@medi-car-bd/mcb-shared-auth",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Shared authentication module for MCB applications",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"lint
|
|
18
|
-
"
|
|
19
|
-
"format
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
"@types/
|
|
27
|
-
"@types/
|
|
28
|
-
"@types/
|
|
29
|
-
"@
|
|
30
|
-
"@typescript-eslint/
|
|
31
|
-
"eslint": "^
|
|
32
|
-
"eslint
|
|
33
|
-
"prettier": "^
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"winston
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@medi-car-bd/mcb-shared-auth",
|
|
3
|
+
"version": "0.0.12",
|
|
4
|
+
"description": "Shared authentication module for MCB applications",
|
|
5
|
+
"packageManager": "pnpm@10.18.0",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"preinstall": "node -e \"\"",
|
|
13
|
+
"postinstall": "",
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"start": "node dist/index.js",
|
|
16
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
17
|
+
"lint": "eslint . --ext .ts,.tsx,.js,.jsx",
|
|
18
|
+
"lint:fix": "eslint . --ext .ts,.tsx,.js,.jsx --fix",
|
|
19
|
+
"format": "prettier --write .",
|
|
20
|
+
"format:check": "prettier --check ."
|
|
21
|
+
},
|
|
22
|
+
"keywords": [],
|
|
23
|
+
"author": "MediCarBD",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/bcryptjs": "^2.4.6",
|
|
27
|
+
"@types/express": "^4.17.21",
|
|
28
|
+
"@types/jsonwebtoken": "^9.0.5",
|
|
29
|
+
"@types/node": "^20.11.0",
|
|
30
|
+
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
|
31
|
+
"@typescript-eslint/parser": "^8.54.0",
|
|
32
|
+
"eslint": "^9.39.2",
|
|
33
|
+
"eslint-config-prettier": "^9.1.0",
|
|
34
|
+
"prettier": "^3.2.5",
|
|
35
|
+
"typescript": "^5.3.3"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"bcryptjs": "^2.4.3",
|
|
39
|
+
"dotenv": "^16.3.1",
|
|
40
|
+
"express-rate-limit": "^7.1.5",
|
|
41
|
+
"jsonwebtoken": "^9.0.2",
|
|
42
|
+
"winston": "^3.11.0",
|
|
43
|
+
"winston-daily-rotate-file": "^4.7.1"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"express": "^4.18.2"
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"dist"
|
|
50
|
+
],
|
|
51
|
+
"lint-staged": {
|
|
52
|
+
"*.{ts,tsx,js,jsx}": [
|
|
53
|
+
"eslint --max-warnings=0 --fix",
|
|
54
|
+
"prettier --write"
|
|
55
|
+
],
|
|
56
|
+
"*.{json,md,yml,yaml}": [
|
|
57
|
+
"prettier --write"
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
"repository": {
|
|
61
|
+
"type": "git",
|
|
62
|
+
"url": "git+https://github.com/mcb-org/mcb-shared-auth.git"
|
|
63
|
+
}
|
|
64
|
+
}
|