Skip to content

Commit

Permalink
feat: enable arbitrary payloads for JWS (#126)
Browse files Browse the repository at this point in the history
* feat: enable arbitrary payloads for JWS
* chore(test): add tests for JWS functions
  • Loading branch information
oed authored Aug 19, 2020
1 parent f0d6e08 commit 5573e63
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 16 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
},
"author": "Pelle Braendgaard <[email protected]>",
"contributors": [
"Mircea Nistor <[email protected]>"
"Mircea Nistor <[email protected]>",
"Joel Thorstensson <[email protected]>"
],
"license": "Apache-2.0",
"jest": {
Expand Down
46 changes: 32 additions & 14 deletions src/JWT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ export interface JWTDecoded {
data: string
}

export interface JWSDecoded {
header: JWTHeader
payload: string
signature: string
data: string
}

export interface JWTVerified {
payload: any
doc: DIDDocument
Expand All @@ -86,6 +93,19 @@ function encodeSection(data: any): string {

export const NBF_SKEW: number = 300

function decodeJWS(jws: string): JWSDecoded {
const parts: RegExpMatchArray = jws.match(/^([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/)
if (parts) {
return {
header: JSON.parse(base64url.decode(parts[1])),
payload: parts[2],
signature: parts[3],
data: `${parts[1]}.${parts[2]}`
}
}
throw new Error('Incorrect format JWS')
}

/** @module did-jwt/JWT */

/**
Expand All @@ -99,16 +119,13 @@ export const NBF_SKEW: number = 300
*/
export function decodeJWT(jwt: string): JWTDecoded {
if (!jwt) throw new Error('no JWT passed into decodeJWT')
const parts: RegExpMatchArray = jwt.match(/^([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/)
if (parts) {
return {
header: JSON.parse(base64url.decode(parts[1])),
payload: JSON.parse(base64url.decode(parts[2])),
signature: parts[3],
data: `${parts[1]}.${parts[2]}`
}
try {
const jws = decodeJWS(jwt)
const decodedJwt: JWTDecoded = Object.assign(jws, { payload: JSON.parse(base64url.decode(jws.payload)) })
return decodedJwt
} catch(e) {
throw new Error('Incorrect format JWT')
}
throw new Error('Incorrect format JWT')
}

/**
Expand All @@ -123,9 +140,10 @@ export function decodeJWT(jwt: string): JWTDecoded {
* @param {Object} header optional object to specify or customize the JWS header
* @return {Promise<Object, Error>} a promise which resolves with a JWS string or rejects with an error
*/
export async function createJWS(payload: any, signer: Signer, header: Partial<JWTHeader> = {}): Promise<string> {
export async function createJWS(payload: string | any, signer: Signer, header: Partial<JWTHeader> = {}): Promise<string> {
if (!header.alg) header.alg = defaultAlg
const signingInput: string = [encodeSection(header), encodeSection(payload)].join('.')
const encodedPayload = typeof payload === 'string' ? payload : encodeSection(payload)
const signingInput: string = [encodeSection(header), encodedPayload].join('.')

const jwtSigner: SignerAlgorithm = SignerAlgorithm(header.alg)
const signature: string = await jwtSigner(signingInput, signer)
Expand Down Expand Up @@ -174,7 +192,7 @@ export async function createJWT(
return createJWS(fullPayload, signer, header)
}

function verifyJWSDecoded({ header, data, signature }: JWTDecoded, pubkeys: PublicKey | PublicKey[]): PublicKey {
function verifyJWSDecoded({ header, data, signature }: JWSDecoded, pubkeys: PublicKey | PublicKey[]): PublicKey {
if (!Array.isArray(pubkeys)) pubkeys = [pubkeys]
const signer: PublicKey = VerifierAlgorithm(header.alg)(data, signature, pubkeys)
return signer
Expand All @@ -192,7 +210,7 @@ function verifyJWSDecoded({ header, data, signature }: JWTDecoded, pubkeys: Publ
* @return {PublicKey} The public key used to sign the JWS
*/
export function verifyJWS(jws: string, pubkeys: PublicKey | PublicKey[]): PublicKey {
const jwsDecoded: JWTDecoded = decodeJWT(jws)
const jwsDecoded: JWSDecoded = decodeJWS(jws)
return verifyJWSDecoded(jwsDecoded, pubkeys)
}

Expand Down Expand Up @@ -234,7 +252,7 @@ export async function verifyJWT(
payload.iss,
options.auth
)
const signer: PublicKey = await verifyJWSDecoded({ header, data, signature } as JWTDecoded, authenticators)
const signer: PublicKey = await verifyJWSDecoded({ header, data, signature } as JWSDecoded, authenticators)
const now: number = Math.floor(Date.now() / 1000)
if (signer) {
const nowSkewed = now + NBF_SKEW
Expand Down
39 changes: 38 additions & 1 deletion src/__tests__/JWT-test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { createJWT, verifyJWT, decodeJWT, resolveAuthenticator, NBF_SKEW } from '../JWT'
import { createJWT, verifyJWT, decodeJWT, createJWS, verifyJWS, resolveAuthenticator, NBF_SKEW } from '../JWT'
import { TokenVerifier } from 'jsontokens'
import SimpleSigner from '../SimpleSigner'
import NaclSigner from '../NaclSigner'
import { verifyJWT as naclVerifyJWT } from 'nacl-did'
import MockDate from 'mockdate'
import base64url from 'base64url'
import { PublicKey } from 'did-resolver'

const NOW = 1485321133
MockDate.set(NOW * 1000 + 123)
Expand Down Expand Up @@ -334,6 +336,41 @@ describe('verifyJWT()', () => {
})
})

describe('JWS', () => {

it('createJWS works with JSON payload', async () => {
const payload = { some: 'data' }
const jws = await createJWS(payload, signer)
expect(jws).toMatchSnapshot()
expect(JSON.parse(base64url.decode(jws.split('.')[1]))).toEqual(payload)
})

it('createJWS works with base64url payload', async () => {
// use the hex public key as an arbitrary payload
const encodedPayload = base64url.encode(Buffer.from(publicKey, 'hex'))
const jws = await createJWS(encodedPayload, signer)
expect(jws).toMatchSnapshot()
expect(jws.split('.')[1]).toEqual(encodedPayload)
})

it('verifyJWS works with JSON payload', async () => {
const payload = { some: 'data' }
const jws = await createJWS(payload, signer)
expect(verifyJWS(jws, { publicKeyHex: publicKey } as PublicKey))
})

it('verifyJWS works with base64url payload', async () => {
const encodedPayload = base64url.encode(Buffer.from(publicKey, 'hex'))
const jws = await createJWS(encodedPayload, signer)
expect(verifyJWS(jws, { publicKeyHex: publicKey } as PublicKey))
})

it('verifyJWS fails with bad input', async () => {
const badJws = 'abrewguer.fjreoiwfoiew.foirheogu.reoguhwehrg'
expect(() => verifyJWS(badJws, { publicKeyHex: publicKey } as PublicKey)).toThrow('Incorrect format JWS')
})
})

describe('resolveAuthenticator()', () => {
const ecKey1 = {
id: `${did}#keys-1`,
Expand Down
4 changes: 4 additions & 0 deletions src/__tests__/__snapshots__/JWT-test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`JWS createJWS works with JSON payload 1`] = `"eyJhbGciOiJFUzI1NksifQ.eyJzb21lIjoiZGF0YSJ9.dblNz-7BVLknOFIBPmt5VTG0MDls_Q69WI8OfQuqNdUp4y50-b8Ubn0xujm1ijfmfqRunpks5TyWqgMsQkR_GQ"`;

exports[`JWS createJWS works with base64url payload 1`] = `"eyJhbGciOiJFUzI1NksifQ.A_3Vet7D1DjqI3_kazPuHgFu2mtYXD4n6mZobC6lNYR5.n5ZZQZe1J7e76TGTLBpQO2R22JFoHDBi5ScfoxHz__Qy7Q6r3R11GdXmY_0ntFx6nC9QbDD19y8tTDMLUM4DAw"`;

exports[`createJWT() ES256K creates a JWT with correct format 1`] = `
Object {
"data": "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUzMjExMzMsInJlcXVlc3RlZCI6WyJuYW1lIiwicGhvbmUiXSwiaXNzIjoiZGlkOmV0aHI6MHhmM2JlYWMzMGM0OThkOWUyNjg2NWYzNGZjYWE1N2RiYjkzNWIwZDc0In0",
Expand Down

0 comments on commit 5573e63

Please sign in to comment.