Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduced Policies to override Timestamp checking #241

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
mirceanis marked this conversation as resolved.
Show resolved Hide resolved
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