-
-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: decode token * feat: use zod to guard decodeToken * chore: no need for as IDToken * chore: utils doc foramt Co-authored-by: Gao Sun <[email protected]> Co-authored-by: Gao Sun <[email protected]>
- Loading branch information
Showing
2 changed files
with
89 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { SignJWT } from 'jose/jwt/sign'; | ||
import { generateKeyPair } from 'jose/util/generate_key_pair'; | ||
import { ZodError } from 'zod'; | ||
|
||
import { decodeToken } from './utils'; | ||
|
||
describe('decodeToken', () => { | ||
test('decode token and get claims', async () => { | ||
const JWT = await new SignJWT({}) | ||
.setProtectedHeader({ alg: 'RS256' }) | ||
.setAudience('foo') | ||
.setSubject('foz') | ||
.setIssuer('logto') | ||
.setIssuedAt() | ||
.setExpirationTime('2h') | ||
.sign((await generateKeyPair('RS256')).privateKey); | ||
const payload = decodeToken(JWT); | ||
expect(payload.sub).toEqual('foz'); | ||
}); | ||
|
||
test('throw on invalid JWT string', async () => { | ||
expect(() => decodeToken('invalid-JWT')).toThrow(); | ||
}); | ||
|
||
test('throw ZodError when iss is missing', async () => { | ||
const JWT = await new SignJWT({}) | ||
.setProtectedHeader({ alg: 'RS256' }) | ||
.setAudience('foo') | ||
.setSubject('foz') | ||
.setIssuedAt() | ||
.setExpirationTime('2h') | ||
.sign((await generateKeyPair('RS256')).privateKey); | ||
expect(() => decodeToken(JWT)).toThrowError(ZodError); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { z, ZodError } from 'zod'; | ||
|
||
const fullfillBase64 = (input: string) => { | ||
if (input.length === 2) { | ||
return `${input}==`; | ||
} | ||
|
||
if (input.length === 3) { | ||
return `${input}=`; | ||
} | ||
|
||
return input; | ||
}; | ||
|
||
const IDTokenSchema = z.object({ | ||
iss: z.string(), | ||
sub: z.string(), | ||
aud: z.string(), | ||
exp: z.number(), | ||
iat: z.number(), | ||
at_hash: z.optional(z.string()), | ||
}); | ||
|
||
export type IDToken = z.infer<typeof IDTokenSchema>; | ||
|
||
/** | ||
* Decode IDToken from JWT, without verifing. | ||
* Verifing JWT requires fetching public key first, this can not | ||
* be done in a sync function, in some cases, verifing is not necessary. | ||
* @param token JWT string. | ||
* @returns IDToken combined with JWT Claims. | ||
*/ | ||
export const decodeToken = (token: string): IDToken => { | ||
const payloadPart = token.split('.')[1]; | ||
|
||
if (!payloadPart) { | ||
throw new Error('invalid token'); | ||
} | ||
|
||
const payloadString = payloadPart.replace(/-/g, '+').replace(/_/g, '/'); | ||
const json = decodeURIComponent( | ||
escape(Buffer.from(fullfillBase64(payloadString), 'base64').toString()) | ||
); | ||
|
||
try { | ||
return IDTokenSchema.parse(JSON.parse(json)); | ||
} catch (error: unknown) { | ||
if (error instanceof ZodError) { | ||
throw error; | ||
} | ||
|
||
throw new Error('invalid token: JSON parse failed'); | ||
} | ||
}; |