Skip to content

Commit 6da08f6

Browse files
authored
Merge pull request #2 from kinde-oss/feat/add-header-decode
feat: add token complete check and option to decode the header part o…
2 parents cd391c4 + 718fea4 commit 6da08f6

File tree

8 files changed

+939
-23
lines changed

8 files changed

+939
-23
lines changed

.gitignore

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,4 @@ dist-ssr
2222
*.njsproj
2323
*.sln
2424
*.sw?
25-
coverage
26-
27-
api_
25+
coverage

.prettierignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
generated
22
pnpm-lock.yaml
3-
CHANGELOG.md
3+
CHANGELOG.md
4+
.github

eslint.config.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import globals from "globals";
2+
import pluginJs from "@eslint/js";
3+
import tseslint from "typescript-eslint";
4+
import jsdoc from "eslint-plugin-jsdoc";
5+
6+
export default [
7+
{
8+
ignores: ["dist"],
9+
},
10+
{
11+
languageOptions: { globals: { ...globals.browser, ...globals.node } },
12+
plugins: {
13+
jsdoc,
14+
},
15+
rules: {
16+
"jsdoc/require-description": "warn",
17+
},
18+
},
19+
jsdoc.configs["flat/recommended"],
20+
pluginJs.configs.recommended,
21+
...tseslint.configs.recommended,
22+
];

lib/main.test.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,62 @@
11
// setToken.test.ts
2-
import { jwtDecoder } from "./main.ts";
3-
import { describe, it, expect } from "vitest";
2+
import { JWTHeader, TokenPart, jwtDecoder, type JWTDecoded } from "./main.ts";
3+
import { describe, it, expect, expectTypeOf } from "vitest";
44

55
describe("jwtDecode", () => {
66
it("decode John Balázs", () => {
77
const decodedToken = jwtDecoder(
88
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gQmFsw6F6cyIsImlhdCI6MTUxNjIzOTAyMn0.rpQt1WOpF4h5SP9iBBj2NfYvKQLuCI3lHSvxSS3eexs",
99
);
10+
11+
expectTypeOf(decodedToken).toEqualTypeOf<JWTDecoded | null>();
1012
expect(decodedToken).toEqual({
1113
sub: "1234567890",
1214
name: "John Balázs",
1315
iat: 1516239022,
1416
});
1517
});
1618

19+
it("incomplete token", () => {
20+
const decodedToken = jwtDecoder(
21+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gQmFsw6F6cyIsImlhdCI6MTUxNjIzOTAyMn0",
22+
);
23+
24+
expectTypeOf(decodedToken).toEqualTypeOf<JWTDecoded | null>();
25+
expect(decodedToken).toEqual(null);
26+
});
27+
28+
it("decode header", () => {
29+
const decodedToken = jwtDecoder(
30+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gQmFsw6F6cyIsImlhdCI6MTUxNjIzOTAyMn0.rpQt1WOpF4h5SP9iBBj2NfYvKQLuCI3lHSvxSS3eexs",
31+
TokenPart.header,
32+
);
33+
34+
expectTypeOf(decodedToken).toEqualTypeOf<JWTHeader | null>();
35+
36+
expect(decodedToken).toEqual({
37+
alg: "HS256",
38+
typ: "JWT",
39+
});
40+
});
41+
1742
it("return null when nothing defined", () => {
1843
const decodedToken = jwtDecoder();
1944
expect(decodedToken).toBeNull();
2045
});
46+
47+
it("extended type", () => {
48+
const decodedToken = jwtDecoder<
49+
JWTDecoded & {
50+
roles: string[];
51+
}
52+
>("");
53+
54+
expectTypeOf(decodedToken).toEqualTypeOf<
55+
| (JWTDecoded & {
56+
roles: string[];
57+
})
58+
| null
59+
>();
60+
expect(decodedToken).toBeNull();
61+
});
2162
});

lib/main.ts

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export type JWTDecoded = {
1+
type JWTDecoded = {
22
aud: string[];
33
azp: string;
44
exp: number;
@@ -9,31 +9,66 @@ export type JWTDecoded = {
99
sub: string;
1010
};
1111

12+
type JWTHeader = {
13+
alg: string;
14+
typ: string;
15+
};
16+
17+
enum TokenPart {
18+
header = 0,
19+
body = 1,
20+
}
21+
22+
type JwtDecoderResult<
23+
T,
24+
P extends TokenPart = TokenPart.body,
25+
> = P extends TokenPart.header ? JWTHeader : T | null;
26+
1227
/**
1328
* Decode JWT token
14-
* @param token - JWT token to be decoded
15-
* @returns - Decoded JWT token
16-
*
29+
* @param {string} token - JWT token to be decoded
30+
* @param {TokenPart} [part] - Part of the token to be decoded (header, body, or signature)
31+
* @returns {object} - Decoded JWT token as an object
1732
* @example
18-
* jwtDecode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c")
33+
* jwtDecode("[TOKEN]")
1934
* Returns:
2035
* {
2136
* sub: "1234567890",
2237
* name: "John Doe",
2338
* iat: 1516239022
2439
* }
40+
*
41+
* jwtDecode("[TOKEN]", TokenPart.header)
42+
* Returns:
43+
* {
44+
* alg: "HS256",
45+
* typ: "JWT"
46+
* }
2547
*/
26-
export const jwtDecoder = <T = JWTDecoded>(token?: string): T | null => {
48+
function jwtDecoder<T = JWTDecoded, P extends TokenPart = TokenPart.body>(
49+
token?: string,
50+
part?: P,
51+
): JwtDecoderResult<T, P> | null {
2752
if (!token) {
2853
return null;
2954
}
30-
const base64Url = token.split(".")[1];
55+
56+
const tokenSplit = token.split(".");
57+
if (tokenSplit.length !== 3) {
58+
return null;
59+
}
60+
61+
const base64Url = tokenSplit[part ?? TokenPart.body];
3162
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
63+
3264
const jsonPayload = decodeURIComponent(
3365
atob(base64)
3466
.split("")
3567
.map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2))
3668
.join(""),
3769
);
70+
3871
return JSON.parse(jsonPayload);
39-
};
72+
}
73+
74+
export { jwtDecoder, type JWTDecoded, type JWTHeader, TokenPart };

package.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,25 @@
1515
"preview": "vite preview",
1616
"test": "vitest",
1717
"test:coverage": "vitest --coverage",
18-
"lint": "prettier --check .",
19-
"lint:fix": "prettier --write ."
18+
"lint": "eslint && prettier --check .",
19+
"lint:fix": "eslint --fix && prettier --write ."
2020
},
2121
"module": "dist/jwt-decoder.js",
2222
"main": "dist/jwt-decoder.cjs",
2323
"types": "dist/main.d.ts",
2424
"devDependencies": {
25+
"@eslint/js": "^9.4.0",
2526
"@types/node": "^20.12.7",
2627
"@vitest/coverage-v8": "^1.5.2",
28+
"eslint": "9.x",
29+
"eslint-plugin-jsdoc": "^48.2.9",
30+
"globals": "^15.4.0",
2731
"prettier": "^3.2.5",
2832
"typescript": "^5.4.5",
33+
"typescript-eslint": "^7.12.0",
2934
"vite": "^5.2.10",
3035
"vite-plugin-dts": "^3.9.0",
3136
"vitest": "^1.5.2"
32-
}
37+
},
38+
"packageManager": "[email protected]+sha256.2df78e65d433d7693b9d3fbdaf431b2d96bb4f96a2ffecd51a50efe16e50a6a8"
3339
}

0 commit comments

Comments
 (0)