Skip to content

Commit

Permalink
feat: add JWT verification policies to override timestamp checking (#241
Browse files Browse the repository at this point in the history
)

relates to #239, partially closes it
  • Loading branch information
daniyal-khalil authored Aug 12, 2022
1 parent f694cef commit 2934f4c
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 6 deletions.
37 changes: 32 additions & 5 deletions src/JWT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ export interface JWTVerifyOptions {
skewTime?: number
/** See https://www.w3.org/TR/did-spec-registries/#verification-relationships */
proofPurpose?: ProofPurposeTypes
policies?: JWTVerifyPolicies
}

export interface JWTVerifyPolicies {
now?: number
nbf?: boolean
iat?: boolean
exp?: boolean
}

export interface JWSCreationOptions {
Expand Down Expand Up @@ -141,6 +149,14 @@ export const SUPPORTED_PUBLIC_KEY_TYPES: PublicKeyTypes = {
export const SELF_ISSUED_V2 = 'https://self-issued.me/v2'
export const SELF_ISSUED_V0_1 = 'https://self-issued.me'

// Exporting errorCodes in a machine readable format rather than human readable format to be used in higher level module
export const INVALID_JWT = 'invalid_jwt'
export const INAVLID_CONFIG = 'invalid_config'
export const INVALID_SIGNATURE = 'invalid_signature'
export const NOT_SUPPORTED = 'not_supported'
export const NO_SUITABLE_KEYS = 'no_suitable_keys'
export const RESOLVE_ERROR = 'resolve_error'

type LegacyVerificationMethod = { publicKey?: string }

const defaultAlg = 'ES256K'
Expand Down Expand Up @@ -319,6 +335,12 @@ export async function verifyJWT(
callbackUrl: undefined,
skewTime: undefined,
proofPurpose: undefined,
policies: {
nbf: undefined,
iat: undefined,
exp: undefined,
now: undefined,
},
}
): Promise<JWTVerified> {
if (!options.resolver) throw new Error('missing_resolver: No DID resolver has been configured')
Expand All @@ -328,10 +350,13 @@ export async function verifyJWT(
? 'authentication'
: undefined
: options.proofPurpose

let did = ''

if (!payload.iss) {
throw new Error('invalid_jwt: JWT iss is required')
}
let did = ''

if (payload.iss === SELF_ISSUED_V2) {
if (!payload.sub) {
throw new Error('invalid_jwt: JWT sub is required')
Expand All @@ -349,28 +374,30 @@ export async function verifyJWT(
} else {
did = payload.iss
}

if (!did) {
throw new Error(`invalid_jwt: No DID has been found in the JWT`)
}

const { didResolutionResult, authenticators, issuer }: DIDAuthenticator = await resolveAuthenticator(
options.resolver,
header.alg,
did,
proofPurpose
)
const signer: VerificationMethod = await verifyJWSDecoded({ header, data, signature } as JWSDecoded, authenticators)
const now: number = Math.floor(Date.now() / 1000)
const now: number = options.policies?.now ? options.policies.now : Math.floor(Date.now() / 1000)
const skewTime = typeof options.skewTime !== 'undefined' && options.skewTime >= 0 ? options.skewTime : NBF_SKEW
if (signer) {
const nowSkewed = now + skewTime
if (payload.nbf) {
if (options.policies?.nbf !== false && payload.nbf) {
if (payload.nbf > nowSkewed) {
throw new Error(`invalid_jwt: JWT not valid before nbf: ${payload.nbf}`)
}
} else if (payload.iat && payload.iat > nowSkewed) {
} else if (options.policies?.iat !== false && payload.iat && payload.iat > nowSkewed) {
throw new Error(`invalid_jwt: JWT not valid yet (issued in the future) iat: ${payload.iat}`)
}
if (payload.exp && payload.exp <= now - skewTime) {
if (options.policies?.exp !== false && payload.exp && payload.exp <= now - skewTime) {
throw new Error(`invalid_jwt: JWT has expired: exp: ${payload.exp} < now: ${now}`)
}
if (payload.aud) {
Expand Down
55 changes: 54 additions & 1 deletion src/__tests__/JWT.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,13 +462,45 @@ describe('verifyJWT()', () => {
// const jwt = await createJWT({nbf:FUTURE},{issuer:did,signer})
await expect(verifyJWT(jwt, { resolver })).rejects.toThrowError()
})
it('passes when nbf is in the future and policy for nbf is false', async () => {
expect.assertions(2)
// const jwt = await createJWT({nbf:FUTURE},{issuer:did,signer})

const jwt = await createJWT({ requested: ['name', 'phone'], nbf: new Date().getTime() + 1000000 }, { issuer: did, signer })
expect(verifier.verify(jwt)).toBe(true)

const { payload } = await verifyJWT(jwt, { resolver, policies: { nbf: false } })
return expect(payload).toBeDefined()
})

it('passes when nbf is in the future and now is provided to be higher than nbf', async () => {
expect.assertions(2)
// const jwt = await createJWT({nbf:FUTURE},{issuer:did,signer})

const jwt = await createJWT({ requested: ['name', 'phone'], nbf: new Date().getTime() + 10000 }, { issuer: did, signer })
expect(verifier.verify(jwt)).toBe(true)

const { payload } = await verifyJWT(jwt, { resolver, policies: { now: new Date().getTime() + 100000 } })
return expect(payload).toBeDefined()
})


it('fails when nbf is in the future and iat is in the past', async () => {
expect.assertions(1)
const jwt =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUyNjExMzMsIm5iZiI6MTQ4NTM4MTEzMywiaXNzIjoiZGlkOmV0aHI6MHhmM2JlYWMzMGM0OThkOWUyNjg2NWYzNGZjYWE1N2RiYjkzNWIwZDc0In0.JjEn_huxI9SsBY_3PlD0ShpXvrRgUGFDKAgxJBc1Q5GToVpUTw007-o9BTt7JNi_G2XWmcu2aXXnDn0QFsRIrg'
// const jwt = await createJWT({nbf:FUTURE,iat:PAST},{issuer:did,signer})
await expect(verifyJWT(jwt, { resolver })).rejects.toThrowError()
})
it('passes when nbf is in the future and iat is in the past with nbf policy false', async () => {
expect.assertions(1)
const jwt =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUyNjExMzMsIm5iZiI6MTQ4NTM4MTEzMywiaXNzIjoiZGlkOmV0aHI6MHhmM2JlYWMzMGM0OThkOWUyNjg2NWYzNGZjYWE1N2RiYjkzNWIwZDc0In0.JjEn_huxI9SsBY_3PlD0ShpXvrRgUGFDKAgxJBc1Q5GToVpUTw007-o9BTt7JNi_G2XWmcu2aXXnDn0QFsRIrg'
// const jwt = await createJWT({nbf:FUTURE,iat:PAST},{issuer:did,signer})

const { payload } = await verifyJWT(jwt, { resolver, policies: { nbf: false } })
expect(payload).toBeDefined();
})
it('passes when nbf is missing and iat is in the past', async () => {
expect.assertions(1)
const jwt =
Expand All @@ -483,6 +515,14 @@ describe('verifyJWT()', () => {
// const jwt = await createJWT({iat:FUTURE},{issuer:did,signer})
await expect(verifyJWT(jwt, { resolver })).rejects.toThrowError()
})
it('passes when nbf is missing and iat is in the future with iat policy to be false', async () => {
expect.assertions(1)
const jwt =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUzODExMzMsImlzcyI6ImRpZDpldGhyOjB4ZjNiZWFjMzBjNDk4ZDllMjY4NjVmMzRmY2FhNTdkYmI5MzViMGQ3NCJ9.FJuHvf9Tby7b4I54Cm1nh8CvLg4QH2wt2K0WfyQaLqlr3NKKI5hAdLalgZksI25gLhNrZwQFnC-nzEOs9PI1SQ'
// const jwt = await createJWT({iat:FUTURE},{issuer:did,signer})
const { payload } = await verifyJWT(jwt, { resolver, policies: { iat: false }})
return expect(payload).toBeDefined()
})
it('passes when nbf and iat are both missing', async () => {
expect.assertions(1)
const jwt =
Expand Down Expand Up @@ -586,12 +626,26 @@ describe('verifyJWT()', () => {
await expect(verifyJWT(jwt, { resolver })).rejects.toThrowError(/JWT has expired/)
})

it('accepts an expired JWT with exp policy false', async () => {
expect.assertions(1)
const jwt = await createJWT({ exp: NOW - NBF_SKEW - 1 }, { issuer: did, signer })
const { payload } = await verifyJWT(jwt, { resolver, policies: { exp: false } })
return expect(payload).toBeDefined()
})

it('rejects an expired JWT without skew time', async () => {
expect.assertions(1)
const jwt = await createJWT({ exp: NOW - 1 }, { issuer: did, signer })
await expect(verifyJWT(jwt, { resolver, skewTime: 0 })).rejects.toThrowError(/JWT has expired/)
})

it('accepts an expired JWT without skew time but exp policy false', async () => {
expect.assertions(1)
const jwt = await createJWT({ exp: NOW - 1 }, { issuer: did, signer })
const { payload } = await verifyJWT(jwt, { resolver, skewTime: 0, policies: { exp: false } })
return expect(payload).toBeDefined();
})

it('accepts a valid audience', async () => {
expect.assertions(1)
const jwt = await createJWT({ aud }, { issuer: did, signer })
Expand Down Expand Up @@ -685,7 +739,6 @@ describe('verifyJWT()', () => {
const { payload } = await verifyJWT(jwt, { resolver })
return expect(payload).toBeDefined()
})

it('rejects a self-issued v2 JWT (sub type: jkt) without a header.kid DID', async () => {
expect.assertions(1)
const jwt = await createJWT({ sub: 'sub', sub_jwk: {} }, { issuer: SELF_ISSUED_V2, signer })
Expand Down

0 comments on commit 2934f4c

Please sign in to comment.