Skip to content

Commit 3b51919

Browse files
committed
added jwt-decoder.test.ts
1 parent b5c7b11 commit 3b51919

File tree

4 files changed

+151
-14
lines changed

4 files changed

+151
-14
lines changed

src/jwt-decoder.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,42 +101,42 @@ const decodePayload = (
101101
if (!isNonEmptyString(payload.sub)) {
102102
throw new JwtError(
103103
JwtErrorCode.INVALID_ARGUMENT,
104-
`"sub" claim must be a string but got "${payload.sub}}"`
104+
`"sub" claim must be a string but got "${payload.sub}"`
105105
);
106106
}
107107

108108
if (!isNonEmptyString(payload.iss)) {
109109
throw new JwtError(
110110
JwtErrorCode.INVALID_ARGUMENT,
111-
`"iss" claim must be a string but got "${payload.iss}}"`
111+
`"iss" claim must be a string but got "${payload.iss}"`
112112
);
113113
}
114114

115115
if (!isNumber(payload.iat)) {
116116
throw new JwtError(
117117
JwtErrorCode.INVALID_ARGUMENT,
118-
`"iat" claim must be a number but got "${payload.iat}}"`
118+
`"iat" claim must be a number but got "${payload.iat}"`
119119
);
120120
}
121121

122-
if (currentTimestamp < payload.iat) {
122+
if (currentTimestamp <= payload.iat) {
123123
throw new JwtError(
124124
JwtErrorCode.INVALID_ARGUMENT,
125-
`Incorrect "iat" claim must be a newer than "${currentTimestamp}" (iat: "${payload.iat}")`
125+
`Incorrect "iat" claim must be a older than "${currentTimestamp}" (iat: "${payload.iat}")`
126126
);
127127
}
128128

129129
if (!isNumber(payload.exp)) {
130130
throw new JwtError(
131131
JwtErrorCode.INVALID_ARGUMENT,
132-
`"exp" claim must be a number but got "${payload.exp}"}`
132+
`"exp" claim must be a number but got "${payload.exp}"`
133133
);
134134
}
135135

136-
if (currentTimestamp >= payload.exp) {
136+
if (currentTimestamp > payload.exp) {
137137
throw new JwtError(
138138
JwtErrorCode.TOKEN_EXPIRED,
139-
`Incorrect "exp" (expiration time) claim must be a older than "${currentTimestamp}" (exp: ${payload.exp})`
139+
`Incorrect "exp" (expiration time) claim must be a newer than "${currentTimestamp}" (exp: "${payload.exp}")`
140140
);
141141
}
142142

tests/jwk-utils.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ export class TestingKeyFetcher implements KeyFetcher {
2323
}
2424
}
2525

26-
export const genIat = (ms: number = Date.now()): number => Math.floor(ms / 1000)
26+
export const genTime = (ms: number = Date.now()): number => Math.floor(ms / 1000)
2727
export const genIss = (projectId: string = "projectId1234"): string => "https://securetoken.google.com/" + projectId
2828

29+
export const encodeObjectBase64Url = (obj: any): string => encodeBase64Url(jsonUTF8Stringify(obj))
30+
2931
const jsonUTF8Stringify = (obj: any): Uint8Array => utf8Encoder.encode(JSON.stringify(obj))
3032

3133
export const signJWT = async (kid: string, payload: DecodedPayload, privateKey: CryptoKey) => {
@@ -34,8 +36,8 @@ export const signJWT = async (kid: string, payload: DecodedPayload, privateKey:
3436
typ: 'JWT',
3537
kid,
3638
};
37-
const encodedHeader = encodeBase64Url(jsonUTF8Stringify(header)).replace(/=/g, "")
38-
const encodedPayload = encodeBase64Url(jsonUTF8Stringify(payload)).replace(/=/g, "")
39+
const encodedHeader = encodeObjectBase64Url(header).replace(/=/g, "")
40+
const encodedPayload = encodeObjectBase64Url(payload).replace(/=/g, "")
3941
const headerAndPayload = `${encodedHeader}.${encodedPayload}`;
4042

4143
const signature = await crypto.subtle.sign(

tests/jws-verifier.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import { JwtError, JwtErrorCode } from "../src/errors"
22
import { PublicKeySignatureVerifier, rs256alg } from "../src/jws-verifier"
33
import { DecodedPayload, RS256Token } from "../src/jwt-decoder"
4-
import { genIat, genIss, signJWT, TestingKeyFetcher } from "./jwk-utils"
4+
import { genTime, genIss, signJWT, TestingKeyFetcher } from "./jwk-utils"
55

66
describe("PublicKeySignatureVerifier", () => {
77
const kid = "kid123456"
88
const projectId = "projectId1234"
9-
const currentTimestamp = Date.now()
9+
const currentTimestamp = genTime(Date.now())
1010
const payload: DecodedPayload = {
1111
aud: projectId,
1212
exp: currentTimestamp + 9999,
13-
iat: genIat(currentTimestamp - 10000), // -10s
13+
iat: currentTimestamp - 10000, // -10s
1414
iss: genIss(projectId),
1515
sub: "userId12345",
1616
}

tests/jwt-decoder.test.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { JwtError, JwtErrorCode } from "../src/errors"
2+
import { DecodedHeader, DecodedPayload, RS256Token } from "../src/jwt-decoder"
3+
import { genTime, genIss, signJWT, TestingKeyFetcher, encodeObjectBase64Url } from "./jwk-utils"
4+
5+
describe("TokenDecoder", () => {
6+
const kid = "kid123456"
7+
const projectId = "projectId1234"
8+
const currentTimestamp = genTime(Date.now())
9+
const payload: DecodedPayload = {
10+
aud: projectId,
11+
exp: currentTimestamp, // now (for border test)
12+
iat: currentTimestamp - 10000, // -10s
13+
iss: genIss(projectId),
14+
sub: "userId12345",
15+
}
16+
17+
it("invalid token", () => {
18+
expect(() => RS256Token.decode("invalid", currentTimestamp)).toThrowError(
19+
new JwtError(
20+
JwtErrorCode.INVALID_ARGUMENT,
21+
"token must consist of 3 parts"
22+
)
23+
)
24+
})
25+
26+
describe("header", () => {
27+
const validHeader: DecodedHeader = {
28+
kid,
29+
alg: "RS256",
30+
}
31+
32+
it("invalid kid", () => {
33+
const headerPart = encodeObjectBase64Url({
34+
...validHeader,
35+
kid: undefined
36+
})
37+
const jwt = `${headerPart}.payload.signature`
38+
expect(() => RS256Token.decode(jwt, currentTimestamp)).toThrowError(
39+
new JwtError(
40+
JwtErrorCode.NO_KID_IN_HEADER,
41+
`kid must be a string but got ${undefined}`
42+
)
43+
)
44+
})
45+
46+
it("invalid alg", () => {
47+
const headerPart = encodeObjectBase64Url({
48+
...validHeader,
49+
alg: "HS256"
50+
})
51+
const jwt = `${headerPart}.payload.signature`
52+
expect(() => RS256Token.decode(jwt, currentTimestamp)).toThrowError(
53+
new JwtError(
54+
JwtErrorCode.INVALID_ARGUMENT,
55+
`algorithm must be RS256 but got ${"HS256"}`
56+
)
57+
)
58+
})
59+
})
60+
61+
describe("payload", () => {
62+
test.each([
63+
[
64+
"aud",
65+
{
66+
...payload,
67+
aud: "",
68+
},
69+
new JwtError(
70+
JwtErrorCode.INVALID_ARGUMENT,
71+
`"aud" claim must be a string but got ""`
72+
)
73+
],
74+
[
75+
"sub",
76+
{
77+
...payload,
78+
sub: "",
79+
},
80+
new JwtError(
81+
JwtErrorCode.INVALID_ARGUMENT,
82+
`"sub" claim must be a string but got ""`
83+
)
84+
],
85+
[
86+
"iss",
87+
{
88+
...payload,
89+
iss: "",
90+
},
91+
new JwtError(
92+
JwtErrorCode.INVALID_ARGUMENT,
93+
`"iss" claim must be a string but got ""`
94+
)
95+
],
96+
[
97+
"iat is in future",
98+
{
99+
...payload,
100+
iat: currentTimestamp + 10000, // +10s
101+
},
102+
new JwtError(
103+
JwtErrorCode.INVALID_ARGUMENT,
104+
`Incorrect "iat" claim must be a older than "${currentTimestamp}" (iat: "${currentTimestamp + 10000}")`
105+
)
106+
],
107+
[
108+
"iat is now",
109+
{
110+
...payload,
111+
iat: currentTimestamp,
112+
},
113+
new JwtError(
114+
JwtErrorCode.INVALID_ARGUMENT,
115+
`Incorrect "iat" claim must be a older than "${currentTimestamp}" (iat: "${currentTimestamp}")`
116+
)
117+
],
118+
[
119+
"exp is in past",
120+
{
121+
...payload,
122+
exp: currentTimestamp - 10000, // -10s
123+
},
124+
new JwtError(
125+
JwtErrorCode.INVALID_ARGUMENT,
126+
`Incorrect "exp" (expiration time) claim must be a newer than "${currentTimestamp}" (exp: "${currentTimestamp - 10000}")`
127+
)
128+
],
129+
])("invalid %s", async (_, payload, wantErr) => {
130+
const testingKeyFetcher = await TestingKeyFetcher.withKeyPairGeneration("mismachKid")
131+
const jwt = await signJWT(kid, payload, testingKeyFetcher.getPrivateKey())
132+
expect(() => RS256Token.decode(jwt, currentTimestamp)).toThrowError(wantErr)
133+
})
134+
})
135+
})

0 commit comments

Comments
 (0)