Skip to content

Commit

Permalink
tests: added tests that use node:crypto
Browse files Browse the repository at this point in the history
Signed-off-by: Berend Sliedrecht <sliedrecht@berend.io>
  • Loading branch information
berendsliedrecht committed Jul 11, 2024
1 parent df7aea3 commit 2e6050f
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 36 deletions.
139 changes: 139 additions & 0 deletions packages/core/__tests__/e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import assert from 'node:assert'
import { subtle } from 'node:crypto'
import { describe, it } from 'node:test'

import nock from 'nock'

import { createEntityConfiguration, fetchEntityConfiguration } from '../src/entityConfiguration'
import { type EntityStatementClaims, createEntityStatement, fetchEntityStatement } from '../src/entityStatement'
import type { SignCallback, VerifyCallback } from '../src/utils'

describe('End To End', async () => {
const key = await subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify'])
const exportedKey = await subtle.exportKey('jwk', key.publicKey)
const publicKeyJwk = {
kid: 'some-id',
kty: 'EC',
key_ops: exportedKey.key_ops,
x: exportedKey.x,
y: exportedKey.y,
}

const signJwtCallback: SignCallback = async ({ toBeSigned }) =>
new Uint8Array(await subtle.sign({ hash: 'SHA-256', name: 'ECDSA' }, key.privateKey, toBeSigned))

const verifyJwtCallback: VerifyCallback = async ({ signature, data }) =>
subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, key.publicKey, signature, data)

it('should fetch an entity configuration', async () => {
const iss = 'https://example.org'

const claims = {
iss,
sub: iss,
exp: new Date(),
iat: new Date(),
jwks: {
keys: [publicKeyJwk],
},
}

const entityConfigurationJwt = await createEntityConfiguration({
signJwtCallback,
claims,
header: {
kid: 'some-id',
typ: 'entity-statement+jwt',
},
})

const scope = nock(iss).get('/.well-known/openid-federation').reply(200, entityConfigurationJwt, {
'content-type': 'application/entity-statement+jwt',
})

const fetchedEntityConfigurationClaims = await fetchEntityConfiguration({
entityId: iss,
verifyJwtCallback,
})

assert.deepStrictEqual(fetchedEntityConfigurationClaims, claims)

scope.done()
})

it('should fetch an entity statement', async () => {
const iss = 'https://example.org'
const sub = 'https://sub.example.org'

const entityConfigurationJwt = await createEntityConfiguration({
signJwtCallback,
claims: {
iss,
sub: iss,
exp: new Date(),
iat: new Date(),
jwks: {
keys: [publicKeyJwk],
},
source_endpoint: `${iss}/fetch`,
},
header: {
kid: 'some-id',
typ: 'entity-statement+jwt',
},
})

const entityStamentClaims: EntityStatementClaims = {
iss,
sub: iss,
exp: new Date(),
iat: new Date(),
jwks: {
keys: [],
},
authority_hints: [iss],
metadata: {
federation_entity: {
organization_name: 'my org!',
},
},
}

const entityStatementJwt = await createEntityStatement({
signJwtCallback,
claims: entityStamentClaims,
header: {
kid: 'some-id',
typ: 'entity-statement+jwt',
},
jwk: publicKeyJwk,
})

const scope = nock(iss)
.get('/.well-known/openid-federation')
.reply(200, entityConfigurationJwt, {
'content-type': 'application/entity-statement+jwt',
})
.get('/fetch')
.query({ iss, sub })
.reply(200, entityStatementJwt, {
'content-type': 'application/entity-statement+jwt',
})

const fetchedEntityConfigurationClaims = await fetchEntityConfiguration({
entityId: iss,
verifyJwtCallback,
})

const fetchedEntityStatementClaims = await fetchEntityStatement({
iss,
sub,
issEntityConfiguration: fetchedEntityConfigurationClaims,
verifyJwtCallback,
})

assert.deepStrictEqual(fetchedEntityStatementClaims, entityStamentClaims)

scope.done()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ export const fetchEntityConfiguration = async ({ entityId, verifyJwtCallback }:
requiredContentType: 'application/entity-statement+jwt',
})

// Parse the JWT into its claims and header claims
const { claims, header, signature } = entityConfigurationJwtSchema.parse(entityConfigurationJwt)
// Parse the JWT into its claims
const { claims } = entityConfigurationJwtSchema.parse(entityConfigurationJwt)

await verifyJsonWebToken({ signature, verifyJwtCallback, header, claims })
await verifyJsonWebToken({ jwt: entityConfigurationJwt, verifyJwtCallback })

return claims
}
8 changes: 3 additions & 5 deletions packages/core/src/entityStatement/fetchEntityStatement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,12 @@ export const fetchEntityStatement = async ({
})

// Parse the JWT into its claims and header claims
const { claims, header, signature } = entityStatementJwtSchema.parse(entityStatementJwt)
const { claims } = entityStatementJwtSchema.parse(entityStatementJwt)

await verifyJsonWebToken({
signature,
verifyJwtCallback,
header,
claims,
claimsThatContainTheKid: issEntityConfigurationClaims,
jwt: entityStatementJwt,
jwks: issEntityConfigurationClaims.jwks,
})

return claims
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/entityStatement/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './entityStatementClaims'
export * from './createEntityStatement'
export * from './fetchEntityStatement'
26 changes: 14 additions & 12 deletions packages/core/src/jsonWeb/jsonWebKey.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { z } from 'zod'

export const jsonWebKeySchema = z.object({
kty: z.string(),
// TODO: spec mentions kid may be undefined, but we always need a key id for open id federation
kid: z.string(),
use: z.string().optional(),
key_ops: z.string().optional(),
alg: z.string().optional(),
x5u: z.string().optional(),
x5c: z.string().optional(),
x5t: z.string().optional(),
'x5t#S256': z.string().optional(),
})
export const jsonWebKeySchema = z
.object({
kty: z.string(),
// TODO: spec mentions kid may be undefined, but we always need a key id for open id federation
kid: z.string(),
use: z.string().optional(),
key_ops: z.array(z.string()).optional(),
alg: z.string().optional(),
x5u: z.string().optional(),
x5c: z.string().optional(),
x5t: z.string().optional(),
'x5t#S256': z.string().optional(),
})
.passthrough()

export type JsonWebKey = z.input<typeof jsonWebKeySchema>
35 changes: 35 additions & 0 deletions packages/core/src/jsonWeb/parseJsonWebToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Buffer } from 'node:buffer'

type JsonWebTokenParts = {
header: Record<string, unknown>
claims: Record<string, unknown>
signature: Uint8Array
signableInput: Uint8Array
}

export const parseJsonWebToken = (jwt: string): JsonWebTokenParts => {
const [encodedHeader, encodedClaims, encodedSignature] = jwt.split('.')

if (!encodedHeader) {
throw new Error('could not find the header in the JWT')
}

if (!encodedClaims) {
throw new Error('could not find the claims in the JWT')
}

if (!encodedSignature) {
throw new Error('could not find the signature in the JWT')
}

const header = JSON.parse(Buffer.from(encodedHeader, 'base64url').toString())
const claims = JSON.parse(Buffer.from(encodedClaims, 'base64url').toString())
const signature = new Uint8Array(Buffer.from(encodedSignature, 'base64url'))

return {
header,
claims,
signature,
signableInput: new Uint8Array(Buffer.from(`${encodedHeader}.${encodedClaims}`)),
}
}
26 changes: 10 additions & 16 deletions packages/core/src/jsonWeb/verifyJsonWebToken.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,26 @@
import type { VerifyCallback } from '../utils'
import { createJwtSignableInput } from './createJsonWebTokenSignableInput'
import { getUsedJsonWebKey } from './getUsedJsonWebKey'
import type { JsonWebKeySet } from './jsonWebKeySet'
import { parseJsonWebToken } from './parseJsonWebToken'

type VerifyJsonWebTokenOptions = {
verifyJwtCallback: VerifyCallback
header: Record<string, unknown>
claims: Record<string, unknown>
claimsThatContainTheKid?: Record<string, unknown>
signature: Uint8Array
jwks?: JsonWebKeySet
jwt: string
}

export const verifyJsonWebToken = async ({
claims,
claimsThatContainTheKid = claims,
header,
signature,
verifyJwtCallback,
}: VerifyJsonWebTokenOptions) => {
const jwk = getUsedJsonWebKey(header, claimsThatContainTheKid)
export const verifyJsonWebToken = async ({ jwt, jwks, verifyJwtCallback }: VerifyJsonWebTokenOptions) => {
const { header, signature, claims, signableInput } = parseJsonWebToken(jwt)

// Create a byte array of the data to be verified
const toBeVerified = createJwtSignableInput(header, claims)
const jsonWebKeySetClaims = jwks ? { jwks } : claims

const jwk = getUsedJsonWebKey(header, jsonWebKeySetClaims)

try {
const isValid = await verifyJwtCallback({
signature,
jwk,
data: toBeVerified,
data: signableInput,
})

// TODO: better error message
Expand Down

0 comments on commit 2e6050f

Please sign in to comment.