-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: fetch an entity configuration chain via authority_hints
Signed-off-by: Berend Sliedrecht <[email protected]>
- Loading branch information
1 parent
89c4dd0
commit 86af531
Showing
6 changed files
with
446 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
319 changes: 319 additions & 0 deletions
319
packages/core/__tests__/fetchEntityConfigurationChains.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,319 @@ | ||
import assert from 'node:assert/strict' | ||
import { describe, it } from 'node:test' | ||
import nock from 'nock' | ||
import { fetchEntityConfigurationChains } from '../src/entityConfiguration' | ||
import type { SignCallback, VerifyCallback } from '../src/utils' | ||
import { setupConfigurationChain } from './utils/setupConfigurationChain' | ||
|
||
describe('fetch entity configuration chains', () => { | ||
const signJwtCallback: SignCallback = () => Promise.resolve(new Uint8Array(10).fill(42)) | ||
const verifyJwtCallback: VerifyCallback = () => Promise.resolve(true) | ||
|
||
it('should fetch a basic entity configuration chain of 2 entities', async () => { | ||
const leafEntityId = 'https://leaf.example.org' | ||
const trustAnchorEntityId = 'https://trust.example.org' | ||
|
||
const scopes = [] | ||
const claims = [] | ||
|
||
const configurations = await setupConfigurationChain( | ||
[{ entityId: leafEntityId, authorityHints: [trustAnchorEntityId] }, { entityId: trustAnchorEntityId }], | ||
signJwtCallback | ||
) | ||
|
||
for (const { entityId, jwt, claims: configurationClaims } of configurations) { | ||
const scope = nock(entityId).get('/.well-known/openid-federation').reply(200, jwt, { | ||
'content-type': 'application/entity-statement+jwt', | ||
}) | ||
|
||
scopes.push(scope) | ||
claims.push(configurationClaims) | ||
} | ||
|
||
const trustChains = await fetchEntityConfigurationChains({ | ||
verifyJwtCallback, | ||
leafEntityId, | ||
trustAnchorEntityIds: [trustAnchorEntityId], | ||
}) | ||
|
||
assert.strictEqual(trustChains.length, 1) | ||
assert.strictEqual(trustChains[0].length, 2) | ||
|
||
assert.deepStrictEqual(trustChains[0][0], claims[0]) | ||
assert.deepStrictEqual(trustChains[0][1], claims[1]) | ||
|
||
for (const scope of scopes) { | ||
scope.done() | ||
} | ||
}) | ||
|
||
it('should fetch a basic entity configuration chain of 3 entities', async () => { | ||
const leafEntityId = 'https://leaf.example.org' | ||
const intermediateEntityId = 'https://intermediate.example.org' | ||
const trustAnchorEntityId = 'https://trust.example.org' | ||
|
||
const scopes = [] | ||
const claims = [] | ||
|
||
const configurations = await setupConfigurationChain( | ||
[ | ||
{ entityId: leafEntityId, authorityHints: [intermediateEntityId] }, | ||
{ | ||
entityId: intermediateEntityId, | ||
authorityHints: [trustAnchorEntityId], | ||
}, | ||
{ entityId: trustAnchorEntityId }, | ||
], | ||
signJwtCallback | ||
) | ||
|
||
for (const { entityId, jwt, claims: configurationClaims } of configurations) { | ||
const scope = nock(entityId).get('/.well-known/openid-federation').reply(200, jwt, { | ||
'content-type': 'application/entity-statement+jwt', | ||
}) | ||
|
||
scopes.push(scope) | ||
claims.push(configurationClaims) | ||
} | ||
|
||
const trustChains = await fetchEntityConfigurationChains({ | ||
verifyJwtCallback, | ||
leafEntityId, | ||
trustAnchorEntityIds: [trustAnchorEntityId], | ||
}) | ||
|
||
assert.strictEqual(trustChains.length, 1) | ||
assert.strictEqual(trustChains[0].length, 3) | ||
|
||
assert.deepStrictEqual(trustChains[0][0], claims[0]) | ||
assert.deepStrictEqual(trustChains[0][1], claims[1]) | ||
assert.deepStrictEqual(trustChains[0][2], claims[2]) | ||
|
||
for (const scope of scopes) { | ||
scope.done() | ||
} | ||
}) | ||
|
||
it('should stop to fetch the entity configurations for a chain when a trust anchor is hit', async () => { | ||
const leafEntityId = 'https://leaf.example.org' | ||
const trustAnchorEntityId = 'https://trust.example.org' | ||
const superiorTrustAnchorEntityId = 'https://trust.superior.example.org' | ||
|
||
const scopes = [] | ||
const claims = [] | ||
|
||
const configurations = await setupConfigurationChain( | ||
[ | ||
{ entityId: leafEntityId, authorityHints: [trustAnchorEntityId] }, | ||
{ | ||
entityId: trustAnchorEntityId, | ||
authorityHints: [superiorTrustAnchorEntityId], | ||
}, | ||
{ entityId: superiorTrustAnchorEntityId }, | ||
], | ||
signJwtCallback | ||
) | ||
|
||
for (const { entityId, jwt, claims: configurationClaims } of configurations) { | ||
const scope = nock(entityId).get('/.well-known/openid-federation').reply(200, jwt, { | ||
'content-type': 'application/entity-statement+jwt', | ||
}) | ||
|
||
scopes.push(scope) | ||
claims.push(configurationClaims) | ||
} | ||
|
||
const trustChains = await fetchEntityConfigurationChains({ | ||
verifyJwtCallback, | ||
leafEntityId, | ||
trustAnchorEntityIds: [trustAnchorEntityId], | ||
}) | ||
|
||
assert.strictEqual(trustChains.length, 1) | ||
assert.strictEqual(trustChains[0].length, 2) | ||
|
||
assert.deepStrictEqual(trustChains[0][0], claims[0]) | ||
assert.deepStrictEqual(trustChains[0][1], claims[1]) | ||
|
||
for (const scope of scopes) { | ||
scope.done() | ||
} | ||
}) | ||
|
||
it('should fetch two entity configuration chains of 2 entities each', async () => { | ||
const leafEntityId = 'https://leaf.example.org' | ||
const intermediateOneEntityId = 'https://intermediate.one.example.org' | ||
const intermediateTwoEntityId = 'https://intermediate.two.example.org' | ||
const trustAnchorOneEntityId = 'https://trust.one.example.org' | ||
const trustAnchorTwoEntityId = 'https://trust.two.example.org' | ||
|
||
const scopes = [] | ||
const claims = [] | ||
|
||
const configurations = await setupConfigurationChain( | ||
[ | ||
{ | ||
entityId: leafEntityId, | ||
authorityHints: [intermediateOneEntityId, intermediateTwoEntityId], | ||
}, | ||
{ | ||
entityId: intermediateOneEntityId, | ||
authorityHints: [trustAnchorOneEntityId], | ||
}, | ||
{ | ||
entityId: intermediateTwoEntityId, | ||
authorityHints: [trustAnchorTwoEntityId], | ||
}, | ||
{ entityId: trustAnchorOneEntityId }, | ||
{ entityId: trustAnchorTwoEntityId }, | ||
], | ||
signJwtCallback | ||
) | ||
|
||
for (const { entityId, jwt, claims: configurationClaims } of configurations) { | ||
const scope = nock(entityId).get('/.well-known/openid-federation').reply(200, jwt, { | ||
'content-type': 'application/entity-statement+jwt', | ||
}) | ||
|
||
scopes.push(scope) | ||
claims.push(configurationClaims) | ||
} | ||
|
||
const trustChains = await fetchEntityConfigurationChains({ | ||
verifyJwtCallback, | ||
leafEntityId, | ||
trustAnchorEntityIds: [trustAnchorOneEntityId, trustAnchorTwoEntityId], | ||
}) | ||
|
||
assert.strictEqual(trustChains.length, 2) | ||
assert.strictEqual(trustChains[0].length, 3) | ||
assert.strictEqual(trustChains[1].length, 3) | ||
|
||
for (const scope of scopes) { | ||
scope.done() | ||
} | ||
}) | ||
|
||
it('should fetch one entity configuration chains when one trust anchor is provided', async () => { | ||
const leafEntityId = 'https://leaf.example.org' | ||
const intermediateOneEntityId = 'https://intermediate.one.example.org' | ||
const intermediateTwoEntityId = 'https://intermediate.two.example.org' | ||
const trustAnchorOneEntityId = 'https://trust.one.example.org' | ||
const trustAnchorTwoEntityId = 'https://trust.two.example.org' | ||
|
||
const scopes = [] | ||
const claims = [] | ||
|
||
const configurations = await setupConfigurationChain( | ||
[ | ||
{ | ||
entityId: leafEntityId, | ||
authorityHints: [intermediateOneEntityId, intermediateTwoEntityId], | ||
}, | ||
{ | ||
entityId: intermediateOneEntityId, | ||
authorityHints: [trustAnchorOneEntityId], | ||
}, | ||
{ | ||
entityId: intermediateTwoEntityId, | ||
authorityHints: [trustAnchorTwoEntityId], | ||
}, | ||
{ entityId: trustAnchorOneEntityId }, | ||
{ entityId: trustAnchorTwoEntityId }, | ||
], | ||
signJwtCallback | ||
) | ||
|
||
for (const { entityId, jwt, claims: configurationClaims } of configurations) { | ||
const scope = nock(entityId).get('/.well-known/openid-federation').reply(200, jwt, { | ||
'content-type': 'application/entity-statement+jwt', | ||
}) | ||
|
||
scopes.push(scope) | ||
claims.push(configurationClaims) | ||
} | ||
|
||
const trustChains = await fetchEntityConfigurationChains({ | ||
verifyJwtCallback, | ||
leafEntityId, | ||
trustAnchorEntityIds: [trustAnchorOneEntityId], | ||
}) | ||
|
||
assert.strictEqual(trustChains.length, 1) | ||
assert.strictEqual(trustChains[0].length, 3) | ||
|
||
for (const scope of scopes) { | ||
scope.done() | ||
} | ||
}) | ||
|
||
it('should not fetch an entity configuration chain when no authority_hints are found', async () => { | ||
const scopes = [] | ||
|
||
const configurations = await setupConfigurationChain([{ entityId: 'https://leaf.example.org' }], signJwtCallback) | ||
|
||
for (const { entityId, jwt } of configurations) { | ||
const scope = nock(entityId).get('/.well-known/openid-federation').reply(200, jwt, { | ||
'content-type': 'application/entity-statement+jwt', | ||
}) | ||
|
||
scopes.push(scope) | ||
} | ||
|
||
const trustChains = await fetchEntityConfigurationChains({ | ||
verifyJwtCallback, | ||
leafEntityId: configurations[0].entityId, | ||
trustAnchorEntityIds: ['https://trust.example.org'], | ||
}) | ||
|
||
assert.strictEqual(trustChains.length, 0) | ||
|
||
for (const scope of scopes) { | ||
scope.done() | ||
} | ||
}) | ||
|
||
it('should not fetch an entity configuration chain when a loop is found', async () => { | ||
const scopes = [] | ||
|
||
const leafEntityId = 'https://leaf.example.org' | ||
const intermediateOneEntityId = 'https://intermediate.one.example.org' | ||
const intermediateTwoEntityId = 'https://intermediate.two.example.org' | ||
const trustAnchorEntityId = 'https://trust.example.org' | ||
|
||
const configurations = await setupConfigurationChain( | ||
[ | ||
{ entityId: leafEntityId, authorityHints: [intermediateOneEntityId] }, | ||
{ | ||
entityId: intermediateOneEntityId, | ||
authorityHints: [intermediateTwoEntityId], | ||
}, | ||
{ | ||
entityId: intermediateTwoEntityId, | ||
authorityHints: [intermediateOneEntityId], | ||
}, | ||
], | ||
signJwtCallback | ||
) | ||
|
||
for (const { entityId, jwt } of configurations) { | ||
const scope = nock(entityId).get('/.well-known/openid-federation').reply(200, jwt, { | ||
'content-type': 'application/entity-statement+jwt', | ||
}) | ||
|
||
scopes.push(scope) | ||
} | ||
|
||
const trustChains = await fetchEntityConfigurationChains({ | ||
verifyJwtCallback, | ||
leafEntityId: configurations[0].entityId, | ||
trustAnchorEntityIds: [trustAnchorEntityId], | ||
}) | ||
|
||
assert.strictEqual(trustChains.length, 0) | ||
|
||
for (const scope of scopes) { | ||
scope.done() | ||
} | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { | ||
type EntityConfigurationClaimsOptions, | ||
type EntityConfigurationHeaderOptions, | ||
createEntityConfiguration, | ||
} from '../../src/entityConfiguration' | ||
import type { JsonWebKeySetOptions } from '../../src/jsonWeb' | ||
import type { SignCallback } from '../../src/utils' | ||
|
||
type SetupConfigurationChainOptions = { | ||
entityId: string | ||
authorityHints?: Array<string> | ||
jwks?: JsonWebKeySetOptions | ||
kid?: string | ||
} | ||
|
||
export const setupConfigurationChain = async ( | ||
options: Array<SetupConfigurationChainOptions>, | ||
signJwtCallback: SignCallback | ||
) => { | ||
const chainData: Array<{ | ||
claims: EntityConfigurationClaimsOptions | ||
jwt: string | ||
entityId: string | ||
}> = [] | ||
for (const { entityId, authorityHints, jwks, kid } of options) { | ||
const claims: EntityConfigurationClaimsOptions = { | ||
iss: entityId, | ||
sub: entityId, | ||
exp: new Date(), | ||
iat: new Date(), | ||
jwks: jwks ?? { keys: [{ kid: 'a', kty: 'EC' }] }, | ||
authority_hints: authorityHints, | ||
} | ||
|
||
// fix so `undefined` is not in the expected claims | ||
if (!authorityHints) delete claims.authority_hints | ||
|
||
const header: EntityConfigurationHeaderOptions = { | ||
kid: kid ?? 'a', | ||
typ: 'entity-statement+jwt', | ||
} | ||
|
||
const jwt = await createEntityConfiguration({ | ||
claims, | ||
header, | ||
signJwtCallback, | ||
}) | ||
|
||
chainData.push({ claims, jwt, entityId }) | ||
} | ||
|
||
return chainData | ||
} |
Oops, something went wrong.