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.
@@ -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
+ }