Skip to content

Commit

Permalink
feat: validate expiration on jwtVerify
Browse files Browse the repository at this point in the history
  • Loading branch information
marcomontalbano committed Jul 2, 2024
1 parent 86aa08b commit e5aef3b
Show file tree
Hide file tree
Showing 8 changed files with 70 additions and 11 deletions.
4 changes: 3 additions & 1 deletion packages/js-auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,9 @@ const auth = await authenticate('client_credentials', {
scope: 'market:code:europe'
})
const decodedJWT = await jwtVerify(auth.accessToken)
const decodedJWT = await jwtVerify(auth.accessToken, {
ignoreExpiration: true
})
if (jwtIsSalesChannel(decodedJWT.payload)) {
console.log('organization slug is', decodedJWT.payload.organization.slug)
Expand Down
6 changes: 6 additions & 0 deletions packages/js-auth/src/errors/TokenError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class TokenError extends Error {
constructor(message: string) {
super(message)
this.name = 'TokenError'
}
}
6 changes: 6 additions & 0 deletions packages/js-auth/src/errors/TokenExpiredError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class TokenExpiredError extends Error {
constructor() {
super('Token expired')
this.name = 'TokenExpiredError'
}
}
3 changes: 3 additions & 0 deletions packages/js-auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ export type {
RevokeOptions,
RevokeReturn
} from './types/index.js'

export { TokenError } from './errors/TokenError.js'
export { TokenExpiredError } from './errors/TokenExpiredError.js'
5 changes: 4 additions & 1 deletion packages/js-auth/src/jwtDecode.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { TokenError } from './errors/TokenError.js'
import { jwtDecode } from './jwtDecode.js'

describe('jwtDecode', () => {
it('should throw when the access token is not valid.', () => {
const accessToken = 'hello-world'
expect(() => jwtDecode(accessToken)).toThrowError('Invalid JWT format')

expect(() => jwtDecode(accessToken)).toThrow(TokenError)
expect(() => jwtDecode(accessToken)).toThrow('Invalid token format')
})

it('should throw when the access token is malformed.', () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/js-auth/src/jwtDecode.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TokenError } from './errors/TokenError.js'
import { decodeBase64URLSafe } from './utils/base64.js'

/**
Expand All @@ -10,7 +11,7 @@ export function jwtDecode(accessToken: string): CommerceLayerJWT {
const [encodedHeader, encodedPayload, signature] = `${accessToken}`.split('.')

if (encodedHeader == null || encodedPayload == null || signature == null) {
throw new Error('Invalid JWT format')
throw new TokenError('Invalid token format')
}

return {
Expand Down
32 changes: 28 additions & 4 deletions packages/js-auth/src/jwtVerify.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import jwt from 'jsonwebtoken'
import { TokenExpiredError } from './errors/TokenExpiredError.js'
import { jwtDecode } from './jwtDecode.js'
import { jwtVerify } from './jwtVerify.js'
import { encodeBase64URLSafe } from './utils/base64.js'
import { TokenError } from './errors/TokenError.js'

describe('jwtVerify', () => {
it('should throw when token expired.', async () => {
void expect(async () => await jwtVerify(accessToken)).rejects.toThrow(
TokenExpiredError
)
})

it('should be able to verify a JWT.', async () => {
const jsonwebtokenDecoded = jwt.decode(accessToken, {
complete: true
})

const verification = await jwtVerify(accessToken)
const verification = await jwtVerify(accessToken, {
ignoreExpiration: true
})

expect(verification).toStrictEqual(jsonwebtokenDecoded)
})
Expand All @@ -26,7 +36,11 @@ describe('jwtVerify', () => {
signature
].join('.')

expect(await jwtVerify(newAccessToken)).toStrictEqual(decodedJWT)
expect(
await jwtVerify(newAccessToken, {
ignoreExpiration: true
})
).toStrictEqual(decodedJWT)
})

it('should reject when the payload has been changed', async () => {
Expand All @@ -44,8 +58,18 @@ describe('jwtVerify', () => {
].join('.')

void expect(
async () => await jwtVerify(newAccessToken)
).rejects.toThrowError('Invalid signature')
async () =>
await jwtVerify(newAccessToken, {
ignoreExpiration: true
})
).rejects.toThrow(TokenError)

void expect(
async () =>
await jwtVerify(newAccessToken, {
ignoreExpiration: true
})
).rejects.toThrow('Invalid signature')
})
})
})
Expand Down
22 changes: 18 additions & 4 deletions packages/js-auth/src/jwtVerify.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { TokenError } from './errors/TokenError.js'
import { TokenExpiredError } from './errors/TokenExpiredError.js'
import { jwtDecode, type CommerceLayerJWT } from './jwtDecode.js'
import { decodeBase64URLSafe } from './utils/base64.js'

Expand All @@ -7,14 +9,21 @@ import { decodeBase64URLSafe } from './utils/base64.js'
*/
export async function jwtVerify(
accessToken: string,
options: JwtVerifyOptions = {}
{ ignoreExpiration = false, domain }: JwtVerifyOptions = {}
): Promise<CommerceLayerJWT> {
const decodedJWT = jwtDecode(accessToken)

const jsonWebKey = await getJsonWebKey(decodedJWT.header.kid, options)
const jsonWebKey = await getJsonWebKey(decodedJWT.header.kid, {
domain,
ignoreExpiration
})

if (jsonWebKey == null) {
throw new Error('Invalid token "kid"')
throw new TokenError('Invalid token "kid"')
}

if (!ignoreExpiration && Date.now() >= decodedJWT.payload.exp * 1000) {
throw new TokenExpiredError()
}

const algorithm: RsaHashedImportParams = {
Expand Down Expand Up @@ -48,7 +57,7 @@ export async function jwtVerify(
)

if (!isValid) {
throw new Error('Invalid signature')
throw new TokenError('Invalid signature')
}

return decodedJWT
Expand All @@ -61,6 +70,11 @@ interface JwtVerifyOptions {
* The Commerce Layer's domain.
*/
domain?: string
/**
* Do not validate the token expiration when set to `true`.
* @default false
*/
ignoreExpiration?: boolean
}

/**
Expand Down

0 comments on commit e5aef3b

Please sign in to comment.