Skip to content

Commit

Permalink
feat: add token complete check and option to decode the header part o…
Browse files Browse the repository at this point in the history
…f the token
  • Loading branch information
DanielRivers committed Jun 7, 2024
1 parent 0d611db commit f2303f1
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 10 deletions.
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,4 @@ dist-ssr
*.njsproj
*.sln
*.sw?
coverage

api_
coverage
4 changes: 3 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
generated
pnpm-lock.yaml
pnpm-lock.yaml
CHANGELOG.md
.github
54 changes: 52 additions & 2 deletions lib/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,71 @@
// setToken.test.ts
import { jwtDecoder } from "./main.ts";
import { describe, it, expect } from "vitest";
import { JWTHeader, TokenPart, jwtDecoder, type JWTDecoded } from "./main.ts";
import { describe, it, expect, expectTypeOf } from "vitest";

describe("jwtDecode", () => {
it("decode John Balázs", () => {
const decodedToken = jwtDecoder(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gQmFsw6F6cyIsImlhdCI6MTUxNjIzOTAyMn0.rpQt1WOpF4h5SP9iBBj2NfYvKQLuCI3lHSvxSS3eexs",
);

expectTypeOf(decodedToken).toEqualTypeOf<JWTDecoded | null>();
expect(decodedToken).toEqual({
sub: "1234567890",
name: "John Balázs",
iat: 1516239022,
});
});

it("incomplete token", () => {
const decodedToken = jwtDecoder(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gQmFsw6F6cyIsImlhdCI6MTUxNjIzOTAyMn0",
);

expectTypeOf(decodedToken).toEqualTypeOf<JWTDecoded | null>();
expect(decodedToken).toEqual(null);
});

it("decode header", () => {
const decodedToken = jwtDecoder(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gQmFsw6F6cyIsImlhdCI6MTUxNjIzOTAyMn0.rpQt1WOpF4h5SP9iBBj2NfYvKQLuCI3lHSvxSS3eexs",
TokenPart.header,
);

expectTypeOf(decodedToken).toEqualTypeOf<JWTHeader | null>();

expect(decodedToken).toEqual({
alg: "HS256",
typ: "JWT",
});
});

it("return null when nothing defined", () => {
const decodedToken = jwtDecoder();
expect(decodedToken).toBeNull();
});

it("extended type", () => {
const decodedToken = jwtDecoder<
JWTDecoded & {
roles: string[];
}
>("");

expectTypeOf(decodedToken).toEqualTypeOf<
| (JWTDecoded & {
roles: string[];
})
| null
>();
expect(decodedToken).toBeNull();
});

it("extended type", () => {
const decodedToken = jwtDecoder<
JWTDecoded & {
// Extra attributes here
}
>("");
expect(decodedToken).toBeNull();
});
});
44 changes: 40 additions & 4 deletions lib/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type JWTDecoded = {
type JWTDecoded = {
aud: string[];
azp: string;
exp: number;
Expand All @@ -9,9 +9,20 @@ export type JWTDecoded = {
sub: string;
};

type JWTHeader = {
alg: string;
typ: string;
};

enum TokenPart {
header = 0,
body = 1,
}

/**
* Decode JWT token
* @param token - JWT token to be decoded
* @param part - Part of the token to be decoded, default is body
* @returns - Decoded JWT token
*
* @example
Expand All @@ -22,18 +33,43 @@ export type JWTDecoded = {
* name: "John Doe",
* iat: 1516239022
* }
*
* jwtDecode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", TokenPart.header)
* Returns:
* {
* alg: "HS256",
* typ: "JWT"
* }
*/
export const jwtDecoder = <T = JWTDecoded>(token?: string): T | null => {
type JwtDecoderResult<
T,
P extends TokenPart | undefined = undefined,
> = P extends TokenPart.header ? JWTHeader : T | null;

function jwtDecoder<
T = JWTDecoded,
P extends TokenPart | undefined = undefined,
>(token?: string, part?: P): JwtDecoderResult<T, P> | null {
if (!token) {
return null;
}
const base64Url = token.split(".")[1];

const tokenSplit = token.split(".");
if (tokenSplit.length !== 3) {
return null;
}

const base64Url = tokenSplit[part !== undefined ? part : TokenPart.body];
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");

const jsonPayload = decodeURIComponent(
atob(base64)
.split("")
.map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2))
.join(""),
);

return JSON.parse(jsonPayload);
};
}

export { jwtDecoder, type JWTDecoded, type JWTHeader, TokenPart };

0 comments on commit f2303f1

Please sign in to comment.