rxnt-authentication 0.0.2-alpha-2345
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/index.d.ts +15 -0
- package/dist/index.js +124 -0
- package/package.json +38 -0
- package/src/index.spec.ts +239 -0
- package/src/index.ts +108 -0
package/dist/index.d.ts
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
import { Request } from 'express';
|
2
|
+
|
3
|
+
declare class RxntAuthentication {
|
4
|
+
private readonly jwtIssuerBaseUrl;
|
5
|
+
private jwtAuthPublicKeys;
|
6
|
+
constructor(jwtIssuerBaseUrl: string);
|
7
|
+
stripJwtIfExists(req: Request): void;
|
8
|
+
getCookie(req: Request, cookieName: string): string;
|
9
|
+
getJwt(req: Request): string;
|
10
|
+
verifyJwt(req: Request): Promise<void>;
|
11
|
+
getClaimFromJwt(req: Request, claimName: string): any;
|
12
|
+
private getJwtAuthPublicKeys;
|
13
|
+
}
|
14
|
+
|
15
|
+
export { RxntAuthentication as default };
|
package/dist/index.js
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __create = Object.create;
|
3
|
+
var __defProp = Object.defineProperty;
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
8
|
+
var __export = (target, all) => {
|
9
|
+
for (var name in all)
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
11
|
+
};
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
14
|
+
for (let key of __getOwnPropNames(from))
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
17
|
+
}
|
18
|
+
return to;
|
19
|
+
};
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
26
|
+
mod
|
27
|
+
));
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
29
|
+
|
30
|
+
// src/index.ts
|
31
|
+
var src_exports = {};
|
32
|
+
__export(src_exports, {
|
33
|
+
default: () => RxntAuthentication
|
34
|
+
});
|
35
|
+
module.exports = __toCommonJS(src_exports);
|
36
|
+
var jwt = __toESM(require("jsonwebtoken"));
|
37
|
+
var RxntAuthentication = class {
|
38
|
+
constructor(jwtIssuerBaseUrl) {
|
39
|
+
this.jwtIssuerBaseUrl = jwtIssuerBaseUrl;
|
40
|
+
this.getJwtAuthPublicKeys();
|
41
|
+
}
|
42
|
+
jwtAuthPublicKeys = [];
|
43
|
+
stripJwtIfExists(req) {
|
44
|
+
const authHeaders = req.headers.authorization?.split(",") ?? [];
|
45
|
+
for (const auth of authHeaders) {
|
46
|
+
const parts = auth.split(" ").filter((p) => !!p);
|
47
|
+
if (parts[0] === "Bearer") {
|
48
|
+
const jwtlessHeaders = authHeaders.filter((h) => h != auth);
|
49
|
+
if (jwtlessHeaders.length > 0) {
|
50
|
+
req.headers.authorization = jwtlessHeaders.join(",");
|
51
|
+
} else {
|
52
|
+
delete req.headers.authorization;
|
53
|
+
}
|
54
|
+
}
|
55
|
+
}
|
56
|
+
const cookie = req.headers.cookie;
|
57
|
+
const splitCookies = cookie?.split("; ") ?? [];
|
58
|
+
const accessTokenCookieInd = splitCookies.findIndex((c) => c.startsWith("app.at="));
|
59
|
+
if (accessTokenCookieInd >= 0) {
|
60
|
+
const filteredCookies = splitCookies.filter((_, ind) => ind !== accessTokenCookieInd);
|
61
|
+
if (filteredCookies.length > 0) {
|
62
|
+
req.headers.cookie = filteredCookies.join("; ");
|
63
|
+
} else {
|
64
|
+
delete req.headers.cookie;
|
65
|
+
}
|
66
|
+
}
|
67
|
+
}
|
68
|
+
getCookie(req, cookieName) {
|
69
|
+
const cookie = req.headers.cookie;
|
70
|
+
const splitCookies = cookie?.split("; ") ?? [];
|
71
|
+
const targetCookie = splitCookies.find((c) => c.startsWith(`${cookieName}=`));
|
72
|
+
let cookieValue = "";
|
73
|
+
if (targetCookie) {
|
74
|
+
cookieValue = targetCookie.split("=")[1] ?? "";
|
75
|
+
if (cookieValue.endsWith(";")) {
|
76
|
+
cookieValue = cookieValue.substring(0, cookieValue.length - 1);
|
77
|
+
}
|
78
|
+
}
|
79
|
+
return cookieValue;
|
80
|
+
}
|
81
|
+
getJwt(req) {
|
82
|
+
const authHeaders = req.headers.authorization?.split(",") ?? [];
|
83
|
+
let jwtFromAuthHeader = "";
|
84
|
+
for (const auth of authHeaders) {
|
85
|
+
const parts = auth.split(" ").filter((p) => !!p);
|
86
|
+
if (parts[0] === "Bearer") {
|
87
|
+
jwtFromAuthHeader = parts[1] ?? "";
|
88
|
+
break;
|
89
|
+
}
|
90
|
+
}
|
91
|
+
if (jwtFromAuthHeader) {
|
92
|
+
return jwtFromAuthHeader;
|
93
|
+
}
|
94
|
+
return this.getCookie(req, "app.at");
|
95
|
+
}
|
96
|
+
async verifyJwt(req) {
|
97
|
+
const reqJwt = this.getJwt(req);
|
98
|
+
const jwtKid = jwt.decode(reqJwt, { complete: true, json: true }).header.kid;
|
99
|
+
let publicKey = this.jwtAuthPublicKeys.find((k) => k.key.kid === jwtKid);
|
100
|
+
if (!publicKey) {
|
101
|
+
await this.getJwtAuthPublicKeys();
|
102
|
+
publicKey = this.jwtAuthPublicKeys.find((k) => k.key.kid === jwtKid);
|
103
|
+
if (!publicKey) {
|
104
|
+
throw new Error("Public key for the given jwt does not exist on FusionAuth.");
|
105
|
+
}
|
106
|
+
}
|
107
|
+
const verifiedJwt = jwt.verify(reqJwt, publicKey);
|
108
|
+
if (!verifiedJwt) {
|
109
|
+
throw new Error("JWT verification failed, returning unauthorized status.");
|
110
|
+
}
|
111
|
+
}
|
112
|
+
getClaimFromJwt(req, claimName) {
|
113
|
+
const reqJwt = this.getJwt(req);
|
114
|
+
const jwtObj = jwt.decode(reqJwt, { complete: true, json: true });
|
115
|
+
const payload = jwtObj?.payload ?? {};
|
116
|
+
return payload[claimName];
|
117
|
+
}
|
118
|
+
async getJwtAuthPublicKeys() {
|
119
|
+
if (this.jwtIssuerBaseUrl) {
|
120
|
+
const publicKeyRes = await fetch(`${this.jwtIssuerBaseUrl}/.well-known/jwks.json`);
|
121
|
+
this.jwtAuthPublicKeys = (await publicKeyRes.json()).keys.map((k) => ({ format: "jwk", key: k }));
|
122
|
+
}
|
123
|
+
}
|
124
|
+
};
|
package/package.json
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
{
|
2
|
+
"name": "rxnt-authentication",
|
3
|
+
"version": "0.0.2-alpha-2345",
|
4
|
+
"description": "Authentication helper methods for RXNT Authentication in Node APIs",
|
5
|
+
"main": "dist/index.js",
|
6
|
+
"types": "dist/indext.d.ts",
|
7
|
+
"files": [
|
8
|
+
"src",
|
9
|
+
"dist"
|
10
|
+
],
|
11
|
+
"repository": {
|
12
|
+
"type": "git",
|
13
|
+
"url": "https://github.com/RXNT/common.git",
|
14
|
+
"directory": "rxnt-authentication"
|
15
|
+
},
|
16
|
+
"scripts": {
|
17
|
+
"build": "tsup src/index.ts --dts",
|
18
|
+
"test": "jest",
|
19
|
+
"format": "prettier --write src/**/*",
|
20
|
+
"lint": "eslint src/**/*.{js,ts,json}",
|
21
|
+
"increment-version": "node ../scripts/increment-version.script.js",
|
22
|
+
"publish-package": "node ../scripts/publish.script.js"
|
23
|
+
},
|
24
|
+
"author": "",
|
25
|
+
"license": "ISC",
|
26
|
+
"devDependencies": {
|
27
|
+
"@types/express": "^5.0.3",
|
28
|
+
"@types/jest": "^30.0.0",
|
29
|
+
"@types/jsonwebtoken": "^9.0.10",
|
30
|
+
"@types/node": "^24.3.1",
|
31
|
+
"jest": "^30.1.3",
|
32
|
+
"typescript": "^5.9.2"
|
33
|
+
},
|
34
|
+
"dependencies": {
|
35
|
+
"express": "^5.1.0",
|
36
|
+
"jsonwebtoken": "^9.0.2"
|
37
|
+
}
|
38
|
+
}
|
@@ -0,0 +1,239 @@
|
|
1
|
+
import { Request } from 'express';
|
2
|
+
import RxntAuthentication from './index';
|
3
|
+
|
4
|
+
describe('RxntAuthentication Instantiation', () => {
|
5
|
+
it('should be able to instantiate RxntAuthentication with base issuer URL', () => {
|
6
|
+
const auth = new RxntAuthentication('');
|
7
|
+
expect(auth).toBeInstanceOf(RxntAuthentication);
|
8
|
+
});
|
9
|
+
});
|
10
|
+
|
11
|
+
describe('stripJwtIfExists', () => {
|
12
|
+
it('should be able to strip JWT from request if it exists as single authorization header', () => {
|
13
|
+
// Arrange
|
14
|
+
const req = {
|
15
|
+
headers: {
|
16
|
+
authorization: 'Bearer insertjwthere',
|
17
|
+
},
|
18
|
+
} as Request;
|
19
|
+
const auth = new RxntAuthentication('');
|
20
|
+
// Act
|
21
|
+
auth.stripJwtIfExists(req);
|
22
|
+
// Assert
|
23
|
+
expect(req.headers.authorization).toBeUndefined();
|
24
|
+
});
|
25
|
+
|
26
|
+
it('should be able to strip JWT from request if it exists as part of combination authorization header', () => {
|
27
|
+
// Arrange
|
28
|
+
const mockBasicToken = 'Basic insertbasictokenhere';
|
29
|
+
const mockBearerToken = 'Bearer insertjwthere';
|
30
|
+
const req = {
|
31
|
+
headers: {
|
32
|
+
authorization: `${mockBasicToken}, ${mockBearerToken}`,
|
33
|
+
},
|
34
|
+
} as Request;
|
35
|
+
const auth = new RxntAuthentication('');
|
36
|
+
// Act
|
37
|
+
auth.stripJwtIfExists(req);
|
38
|
+
// Assert
|
39
|
+
expect(req.headers.authorization).toBe(mockBasicToken);
|
40
|
+
});
|
41
|
+
|
42
|
+
it('should be able to strip JWT from request if it exists as single cookie header', () => {
|
43
|
+
// Arrange
|
44
|
+
const req = {
|
45
|
+
headers: {
|
46
|
+
cookie: 'app.at=insertjwthere',
|
47
|
+
},
|
48
|
+
} as Request;
|
49
|
+
const auth = new RxntAuthentication('');
|
50
|
+
// Act
|
51
|
+
auth.stripJwtIfExists(req);
|
52
|
+
// Assert
|
53
|
+
expect(req.headers.cookie).toBeUndefined();
|
54
|
+
});
|
55
|
+
|
56
|
+
it('should be able to strip JWT from request if it exists as part of combination cookie header', () => {
|
57
|
+
// Arrange
|
58
|
+
const mockCookie1 = 'cookieOne=someValue';
|
59
|
+
const mockCookie2 = 'cookieTwo=someValue';
|
60
|
+
const mockAuthenticationTokenCookie = 'app.at=insertjwthere';
|
61
|
+
const req = {
|
62
|
+
headers: {
|
63
|
+
cookie: `${mockCookie1}; ${mockAuthenticationTokenCookie}; ${mockCookie2}`,
|
64
|
+
},
|
65
|
+
} as Request;
|
66
|
+
const auth = new RxntAuthentication('');
|
67
|
+
// Act
|
68
|
+
auth.stripJwtIfExists(req);
|
69
|
+
// Assert
|
70
|
+
expect(req.headers.cookie).toBe(`${mockCookie1}; ${mockCookie2}`);
|
71
|
+
});
|
72
|
+
|
73
|
+
it('should be able to strip JWT from request if it exists as both authorization header and cookie header', () => {
|
74
|
+
// Arrange
|
75
|
+
const mockAuthorizationHeader = 'Bearer insertjwthere';
|
76
|
+
const mockAuthenticationTokenCookie = 'app.at=insertjwthere';
|
77
|
+
const req = {
|
78
|
+
headers: {
|
79
|
+
authorization: mockAuthorizationHeader,
|
80
|
+
cookie: mockAuthenticationTokenCookie,
|
81
|
+
},
|
82
|
+
} as Request;
|
83
|
+
const auth = new RxntAuthentication('');
|
84
|
+
// Act
|
85
|
+
auth.stripJwtIfExists(req);
|
86
|
+
// Assert
|
87
|
+
expect(req.headers.authorization).toBeUndefined();
|
88
|
+
expect(req.headers.cookie).toBeUndefined();
|
89
|
+
});
|
90
|
+
|
91
|
+
it('should leave request headers unchanged if no JWT exists', () => {
|
92
|
+
// Arrange
|
93
|
+
const mockCookieHeader = 'cookieOne=someValue; cookieTwo=someValue';
|
94
|
+
const mockAuthorizationHeader = 'Basic insertbasictokenhere';
|
95
|
+
const req = {
|
96
|
+
headers: {
|
97
|
+
cookie: mockCookieHeader,
|
98
|
+
authorization: mockAuthorizationHeader,
|
99
|
+
},
|
100
|
+
} as Request;
|
101
|
+
const auth = new RxntAuthentication('');
|
102
|
+
// Act
|
103
|
+
auth.stripJwtIfExists(req);
|
104
|
+
// Assert
|
105
|
+
expect(req.headers.cookie).toBe(mockCookieHeader);
|
106
|
+
expect(req.headers.authorization).toBe(mockAuthorizationHeader);
|
107
|
+
});
|
108
|
+
});
|
109
|
+
|
110
|
+
describe('getCookie', () => {
|
111
|
+
it('should fetch cookie value from request cookie header when exists', () => {
|
112
|
+
// Arrange
|
113
|
+
const mockCookie1Name = 'cookieOne';
|
114
|
+
const mockCookie1Value = 'value1';
|
115
|
+
const mockCookie2Name = 'cookieTwo';
|
116
|
+
const mockCookie2Value = 'value2';
|
117
|
+
const req = {
|
118
|
+
headers: {
|
119
|
+
cookie: `${mockCookie1Name}=${mockCookie1Value}; ${mockCookie2Name}=${mockCookie2Value}`,
|
120
|
+
},
|
121
|
+
} as Request;
|
122
|
+
const auth = new RxntAuthentication('');
|
123
|
+
// Act
|
124
|
+
const cookie1Res = auth.getCookie(req, mockCookie1Name);
|
125
|
+
const cookie2Res = auth.getCookie(req, mockCookie2Name);
|
126
|
+
// Assert
|
127
|
+
expect(cookie1Res).toBe(mockCookie1Value);
|
128
|
+
expect(cookie2Res).toBe(mockCookie2Value);
|
129
|
+
});
|
130
|
+
|
131
|
+
it('should return empty string when requested cookie does not exist', () => {
|
132
|
+
// Arrange
|
133
|
+
const mockCookie3Name = 'cookieThree';
|
134
|
+
const req = {
|
135
|
+
headers: {
|
136
|
+
cookie: `cookieOne=value1; cookieTwo=value2`,
|
137
|
+
},
|
138
|
+
} as Request;
|
139
|
+
const auth = new RxntAuthentication('');
|
140
|
+
// Act
|
141
|
+
const cookie3Res = auth.getCookie(req, mockCookie3Name);
|
142
|
+
// Assert
|
143
|
+
expect(cookie3Res).toBe('');
|
144
|
+
});
|
145
|
+
});
|
146
|
+
|
147
|
+
describe('getJwt', () => {
|
148
|
+
it('should fetch JWT from authorization header', () => {
|
149
|
+
// Arrange
|
150
|
+
const mockJwt = 'insertjwthere';
|
151
|
+
const req = {
|
152
|
+
headers: {
|
153
|
+
authorization: `Bearer ${mockJwt}`,
|
154
|
+
},
|
155
|
+
} as Request;
|
156
|
+
const auth = new RxntAuthentication('');
|
157
|
+
// Act
|
158
|
+
const jwtRes = auth.getJwt(req);
|
159
|
+
// Assert
|
160
|
+
expect(jwtRes).toBe(mockJwt);
|
161
|
+
});
|
162
|
+
|
163
|
+
it('should fetch JWT from cookie header', () => {
|
164
|
+
// Arrange
|
165
|
+
const mockJwt = 'insertjwthere';
|
166
|
+
const req = {
|
167
|
+
headers: {
|
168
|
+
cookie: `app.at=${mockJwt}`,
|
169
|
+
},
|
170
|
+
} as Request;
|
171
|
+
const auth = new RxntAuthentication('');
|
172
|
+
// Act
|
173
|
+
const jwtRes = auth.getJwt(req);
|
174
|
+
// Assert
|
175
|
+
expect(jwtRes).toBe(mockJwt);
|
176
|
+
});
|
177
|
+
|
178
|
+
it('should prioritize authorization header over cookie header', () => {
|
179
|
+
// Arrange
|
180
|
+
const mockJwt1 = 'jwt1';
|
181
|
+
const mockJwt2 = 'jwt2';
|
182
|
+
const req = {
|
183
|
+
headers: {
|
184
|
+
authorization: `Bearer ${mockJwt1}`,
|
185
|
+
cookie: `app.at=${mockJwt2}`,
|
186
|
+
},
|
187
|
+
} as Request;
|
188
|
+
const auth = new RxntAuthentication('');
|
189
|
+
// Act
|
190
|
+
const jwtRes = auth.getJwt(req);
|
191
|
+
// Assert
|
192
|
+
expect(jwtRes).toBe(mockJwt1);
|
193
|
+
});
|
194
|
+
|
195
|
+
it('should return empty string when no JWT exists', () => {
|
196
|
+
// Arrange
|
197
|
+
const req = {
|
198
|
+
headers: {},
|
199
|
+
} as Request;
|
200
|
+
const auth = new RxntAuthentication('');
|
201
|
+
// Act
|
202
|
+
const jwtRes = auth.getJwt(req);
|
203
|
+
// Assert
|
204
|
+
expect(jwtRes).toBe('');
|
205
|
+
});
|
206
|
+
});
|
207
|
+
|
208
|
+
describe('getClaimFromJwt', () => {
|
209
|
+
it('should fetch claim object from JWT', () => {
|
210
|
+
// Arrange
|
211
|
+
const claimObj = {
|
212
|
+
v2LoginId: 123456
|
213
|
+
};
|
214
|
+
const req = {
|
215
|
+
headers: {
|
216
|
+
authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwiZGFzaGJvYXJkIjp7InYyTG9naW5JZCI6MTIzNDU2fX0.8GYXMoAaxAsBWoX7Xql2cXmz8Tw0nUTqFnR0t-mjMpY',
|
217
|
+
},
|
218
|
+
} as Request;
|
219
|
+
const auth = new RxntAuthentication('');
|
220
|
+
// Act
|
221
|
+
const claimRes = auth.getClaimFromJwt(req, 'dashboard');
|
222
|
+
// Assert
|
223
|
+
expect(claimRes).toEqual(claimObj);
|
224
|
+
});
|
225
|
+
|
226
|
+
it('should return undefined for nonexistent claim name', () => {
|
227
|
+
// Arrange
|
228
|
+
const req = {
|
229
|
+
headers: {
|
230
|
+
authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwiZGFzaGJvYXJkIjp7InYyTG9naW5JZCI6MTIzNDU2fX0.8GYXMoAaxAsBWoX7Xql2cXmz8Tw0nUTqFnR0t-mjMpY',
|
231
|
+
},
|
232
|
+
} as Request;
|
233
|
+
const auth = new RxntAuthentication('');
|
234
|
+
// Act
|
235
|
+
const claimRes = auth.getClaimFromJwt(req, 'dashboard2');
|
236
|
+
// Assert
|
237
|
+
expect(claimRes).toBeUndefined();
|
238
|
+
});
|
239
|
+
});
|
package/src/index.ts
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
import * as jwt from 'jsonwebtoken';
|
2
|
+
import { Request } from 'express';
|
3
|
+
import { JsonWebKeyInput } from 'crypto';
|
4
|
+
|
5
|
+
export default class RxntAuthentication {
|
6
|
+
private jwtAuthPublicKeys: JsonWebKeyInput[] = [];
|
7
|
+
constructor(private readonly jwtIssuerBaseUrl: string) {
|
8
|
+
this.getJwtAuthPublicKeys();
|
9
|
+
}
|
10
|
+
|
11
|
+
stripJwtIfExists(req: Request): void {
|
12
|
+
// try to strip bearer authorization header first
|
13
|
+
const authHeaders = req.headers.authorization?.split(',') ?? [];
|
14
|
+
for (const auth of authHeaders) {
|
15
|
+
const parts = auth.split(' ').filter((p) => !!p); // split on spaces, filter out empty strings
|
16
|
+
if (parts[0] === 'Bearer') {
|
17
|
+
const jwtlessHeaders = authHeaders.filter(h => h != auth);
|
18
|
+
if (jwtlessHeaders.length > 0) {
|
19
|
+
req.headers.authorization = jwtlessHeaders.join(',');
|
20
|
+
} else {
|
21
|
+
delete req.headers.authorization;
|
22
|
+
}
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
// strip access token cookie from cookie header if previous was unfound
|
27
|
+
const cookie = req.headers.cookie;
|
28
|
+
const splitCookies = cookie?.split('; ') ?? [];
|
29
|
+
const accessTokenCookieInd = splitCookies.findIndex((c) => c.startsWith('app.at='));
|
30
|
+
if (accessTokenCookieInd >= 0) {
|
31
|
+
const filteredCookies = splitCookies.filter((_, ind) => ind !== accessTokenCookieInd);
|
32
|
+
if (filteredCookies.length > 0) {
|
33
|
+
req.headers.cookie = filteredCookies.join('; ');
|
34
|
+
} else {
|
35
|
+
delete req.headers.cookie;
|
36
|
+
}
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
getCookie(req: Request, cookieName: string): string {
|
41
|
+
const cookie = req.headers.cookie;
|
42
|
+
const splitCookies = cookie?.split('; ') ?? [];
|
43
|
+
const targetCookie = splitCookies.find((c) => c.startsWith(`${cookieName}=`));
|
44
|
+
let cookieValue = '';
|
45
|
+
if (targetCookie) {
|
46
|
+
cookieValue = targetCookie.split('=')[1] ?? '';
|
47
|
+
// if there's only one cookie or the target cookie is last on the cookie list, the `; ` split won't trim the final semicolon.
|
48
|
+
// this shouldn't ever show up in production, but it's better to be safe than sorry.
|
49
|
+
if (cookieValue.endsWith(';')) {
|
50
|
+
cookieValue = cookieValue.substring(0, cookieValue.length - 1);
|
51
|
+
}
|
52
|
+
}
|
53
|
+
return cookieValue;
|
54
|
+
}
|
55
|
+
|
56
|
+
getJwt(req: Request): string {
|
57
|
+
const authHeaders = req.headers.authorization?.split(',') ?? [];
|
58
|
+
let jwtFromAuthHeader = '';
|
59
|
+
for (const auth of authHeaders) {
|
60
|
+
const parts = auth.split(' ').filter((p) => !!p); // split on spaces, filter out empty strings
|
61
|
+
if (parts[0] === 'Bearer') {
|
62
|
+
jwtFromAuthHeader = parts[1] ?? '';
|
63
|
+
break;
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
if (jwtFromAuthHeader) {
|
68
|
+
return jwtFromAuthHeader;
|
69
|
+
}
|
70
|
+
return this.getCookie(req, 'app.at');
|
71
|
+
}
|
72
|
+
|
73
|
+
async verifyJwt(req: Request): Promise<void> {
|
74
|
+
const reqJwt = this.getJwt(req);
|
75
|
+
const jwtKid = (jwt.decode(reqJwt, { complete: true, json: true }) as jwt.JwtPayload).header.kid;
|
76
|
+
|
77
|
+
// get the public key from the current stored keys. if it doesn't exist, refresh them and try again. if it still doesn't exist, throw an error
|
78
|
+
let publicKey = this.jwtAuthPublicKeys.find(k => k.key.kid === jwtKid);
|
79
|
+
if (!publicKey) {
|
80
|
+
// cache miss case
|
81
|
+
await this.getJwtAuthPublicKeys();
|
82
|
+
publicKey = this.jwtAuthPublicKeys.find(k => k.key.kid === jwtKid);
|
83
|
+
if (!publicKey) {
|
84
|
+
throw new Error('Public key for the given jwt does not exist on FusionAuth.');
|
85
|
+
}
|
86
|
+
}
|
87
|
+
|
88
|
+
// verify jwt
|
89
|
+
const verifiedJwt = jwt.verify(reqJwt, publicKey);
|
90
|
+
if (!verifiedJwt) {
|
91
|
+
throw new Error('JWT verification failed, returning unauthorized status.');
|
92
|
+
}
|
93
|
+
}
|
94
|
+
|
95
|
+
getClaimFromJwt(req: Request, claimName: string): any {
|
96
|
+
const reqJwt = this.getJwt(req);
|
97
|
+
const jwtObj = jwt.decode(reqJwt, { complete: true, json: true });
|
98
|
+
const payload = (jwtObj?.payload as jwt.JwtPayload) ?? {};
|
99
|
+
return payload[claimName];
|
100
|
+
}
|
101
|
+
|
102
|
+
private async getJwtAuthPublicKeys() {
|
103
|
+
if (this.jwtIssuerBaseUrl) {
|
104
|
+
const publicKeyRes = await fetch(`${this.jwtIssuerBaseUrl}/.well-known/jwks.json`);
|
105
|
+
this.jwtAuthPublicKeys = (await publicKeyRes.json()).keys.map((k: any) => ({ format: 'jwk', key: k }));
|
106
|
+
}
|
107
|
+
}
|
108
|
+
}
|