From df95a37611b41159216ca7d99897fa34a164a292 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Fri, 11 Oct 2024 14:20:55 -0400 Subject: [PATCH 1/7] initial pass at breaking up submit auth response method --- packages/agent/src/oidc.ts | 63 ++++++++------- packages/agent/tests/connect.spec.ts | 117 +++++++++++++++++++-------- 2 files changed, 119 insertions(+), 61 deletions(-) diff --git a/packages/agent/src/oidc.ts b/packages/agent/src/oidc.ts index 076a0b72a..7aec60172 100644 --- a/packages/agent/src/oidc.ts +++ b/packages/agent/src/oidc.ts @@ -624,7 +624,7 @@ function shouldUseDelegatePermission(scope: DwnPermissionScope): boolean { */ async function createPermissionGrants( selectedDid: string, - delegateBearerDid: BearerDid, + delegatedPortableDid: PortableDid, agent: Web5Agent, scopes: DwnPermissionScope[], ) { @@ -639,7 +639,7 @@ async function createPermissionGrants( return permissionsApi.createGrant({ delegated, store : true, - grantedTo : delegateBearerDid.uri, + grantedTo : delegatedPortableDid.uri, scope, dateExpires : '2040-06-25T16:09:16.693356Z', // TODO: make dateExpires optional author : selectedDid, @@ -745,49 +745,53 @@ async function prepareProtocol( } } -/** - * Creates a delegate did which the web app will use as its future indentity. - * Assigns to that DID the level of permissions that the web app requested in - * the {@link Web5ConnectAuthRequest}. Encrypts via ECDH key that the web app - * will have access to because the web app has the public key which it provided - * in the {@link Web5ConnectAuthRequest}. Then sends the ciphertext of this - * {@link Web5ConnectAuthResponse} to the callback endpoint. Which the - * web app will need to retrieve from the token endpoint and decrypt with the pin to access. - */ -async function submitAuthResponse( +async function createAuthResponseGrants( + delegatedPortableDid: PortableDid, selectedDid: string, - authRequest: Web5ConnectAuthRequest, - randomPin: string, + permissionRequests: ConnectPermissionRequest[], agent: Web5Agent ) { - const delegateBearerDid = await DidJwk.create(); - const delegatePortableDid = await delegateBearerDid.export(); - - // TODO: roll back permissions and protocol configurations if an error occurs. Need a way to delete protocols to achieve this. - const delegateGrantPromises = authRequest.permissionRequests.map( - async (permissionRequest) => { + // TODO: roll back permissions and protocol configurations if an error occurs. Need a way to delete protocols to achieve this. + const processGrant = async (permissionRequest: ConnectPermissionRequest): Promise => { const { protocolDefinition, permissionScopes } = permissionRequest; - + // We validate that all permission scopes match the protocol uri of the protocol definition they are provided with. const grantsMatchProtocolUri = permissionScopes.every(scope => 'protocol' in scope && scope.protocol === protocolDefinition.protocol); if (!grantsMatchProtocolUri) { throw new Error('All permission scopes must match the protocol uri they are provided with.'); } - + await prepareProtocol(selectedDid, agent, protocolDefinition); - - const permissionGrants = await Oidc.createPermissionGrants( + + return await Oidc.createPermissionGrants( selectedDid, - delegateBearerDid, + delegatedPortableDid, agent, permissionScopes ); + }; - return permissionGrants; - } - ); + const delegateGrants = await Promise.all(permissionRequests.map(processGrant)); + return delegateGrants .flat(); +} - const delegateGrants = (await Promise.all(delegateGrantPromises)).flat(); +/** + * Creates a delegate did which the web app will use as its future indentity. + * Assigns to that DID the level of permissions that the web app requested in + * the {@link Web5ConnectAuthRequest}. Encrypts via ECDH key that the web app + * will have access to because the web app has the public key which it provided + * in the {@link Web5ConnectAuthRequest}. Then sends the ciphertext of this + * {@link Web5ConnectAuthResponse} to the callback endpoint. Which the + * web app will need to retrieve from the token endpoint and decrypt with the pin to access. + */ +async function submitAuthResponse( + selectedDid: string, + authRequest: Web5ConnectAuthRequest, + randomPin: string, + delegateBearerDid: BearerDid, + delegateGrants: DwnDataEncodedRecordsWriteMessage[] +) { + const delegatePortableDid = await delegateBearerDid.export(); logger.log('Generating auth response object...'); const responseObject = await Oidc.createResponseObject({ @@ -853,5 +857,6 @@ export const Oidc = { verifyJwt, buildOidcUrl, generateCodeChallenge, + createAuthResponseGrants, submitAuthResponse, }; diff --git a/packages/agent/tests/connect.spec.ts b/packages/agent/tests/connect.spec.ts index 9f34a8e10..4c1315f09 100644 --- a/packages/agent/tests/connect.spec.ts +++ b/packages/agent/tests/connect.spec.ts @@ -363,7 +363,6 @@ describe('web5 connect', function () { it('should send the encrypted jwe authresponse to the server', async () => { sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); - sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); const formEncodedRequest = new URLSearchParams({ id_token : authResponseJwe, @@ -388,11 +387,23 @@ describe('web5 connect', function () { ); const selectedDid = providerIdentity.did.uri; + + // generate the DID + const delegatePortableDid = await delegateBearerDid.export(); + + const delegatedGrants = await Oidc.createAuthResponseGrants( + delegatePortableDid, + selectedDid, + authRequest.permissionRequests, + testHarness.agent + ); + await Oidc.submitAuthResponse( selectedDid, authRequest, randomPin, - testHarness.agent + delegateBearerDid, + delegatedGrants ); expect(fetchSpy.calledOnce).to.be.true; }); @@ -499,7 +510,6 @@ describe('web5 connect', function () { sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); - sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); const callbackUrl = Oidc.buildOidcUrl({ baseURL : 'http://localhost:3000', @@ -530,12 +540,22 @@ describe('web5 connect', function () { .stub(testHarness.agent, 'processDwnRequest') .resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' }, entries: [ protocolMessage ]} }); + const delegatePortableDid = await delegateBearerDid.export(); + + const delegatedGrants = await Oidc.createAuthResponseGrants( + delegatePortableDid, + providerIdentity.did.uri, + authRequest.permissionRequests, + testHarness.agent + ); + // call submitAuthResponse await Oidc.submitAuthResponse( providerIdentity.did.uri, authRequest, randomPin, - testHarness.agent + delegateBearerDid, + delegatedGrants ); // expect the process request to only be called once for ProtocolsQuery @@ -555,7 +575,6 @@ describe('web5 connect', function () { sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); - sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); const callbackUrl = Oidc.buildOidcUrl({ baseURL : 'http://localhost:3000', @@ -583,12 +602,23 @@ describe('web5 connect', function () { .stub(testHarness.agent, 'processDwnRequest') .resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' }, entries: [ ] } }); + // generate the DID + const delegatePortableDid = await delegateBearerDid.export(); + + const delegatedGrants = await Oidc.createAuthResponseGrants( + delegatePortableDid, + providerIdentity.did.uri, + authRequest.permissionRequests, + testHarness.agent + ); + // call submitAuthResponse await Oidc.submitAuthResponse( providerIdentity.did.uri, authRequest, randomPin, - testHarness.agent + delegateBearerDid, + delegatedGrants ); // expect the process request to be called for query and configure @@ -607,12 +637,25 @@ describe('web5 connect', function () { // processDwnRequestStub should resolve a 200 with no entires processDwnRequestStub.resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' } } }); + + // generate the DID + const delegateBearerDid2 = await DidJwk.create(); + const delegatePortableDid2 = await delegateBearerDid2.export(); + + const delegatedGrants2 = await Oidc.createAuthResponseGrants( + delegatePortableDid2, + providerIdentity.did.uri, + authRequest.permissionRequests, + testHarness.agent + ); + // call submitAuthResponse await Oidc.submitAuthResponse( providerIdentity.did.uri, authRequest, randomPin, - testHarness.agent + delegateBearerDid2, + delegatedGrants2 ); // expect the process request to be called for query and configure @@ -658,13 +701,16 @@ describe('web5 connect', function () { .resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' } } }); try { - // call submitAuthResponse - await Oidc.submitAuthResponse( - providerIdentity.did.uri, - authRequest, - randomPin, - testHarness.agent - ); + // generate the DID + const delegateBearerDid = await DidJwk.create(); + const delegatePortableDid = await delegateBearerDid.export(); + + await Oidc.createAuthResponseGrants( + delegatePortableDid, + providerIdentity.did.uri, + authRequest.permissionRequests, + testHarness.agent + ); expect.fail('should have thrown an error'); } catch (error: any) { @@ -709,11 +755,14 @@ describe('web5 connect', function () { .resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' }, entries: [ protocolMessage ] } }); try { - // call submitAuthResponse - await Oidc.submitAuthResponse( + // generate the DID + const delegateBearerDid = await DidJwk.create(); + const delegatePortableDid = await delegateBearerDid.export(); + + await Oidc.createAuthResponseGrants( + delegatePortableDid, providerIdentity.did.uri, - authRequest, - randomPin, + authRequest.permissionRequests, testHarness.agent ); @@ -758,14 +807,16 @@ describe('web5 connect', function () { .resolves({ messageCid: '', reply: { status: { code: 500, detail: 'Some Error'}, } }); try { - // call submitAuthResponse - await Oidc.submitAuthResponse( - providerIdentity.did.uri, - authRequest, - randomPin, - testHarness.agent - ); + // generate the DID + const delegateBearerDid = await DidJwk.create(); + const delegatePortableDid = await delegateBearerDid.export(); + await Oidc.createAuthResponseGrants( + delegatePortableDid, + providerIdentity.did.uri, + authRequest.permissionRequests, + testHarness.agent + ); expect.fail('should have thrown an error'); } catch (error: any) { expect(error.message).to.equal('Could not fetch protocol: Some Error'); @@ -799,14 +850,16 @@ describe('web5 connect', function () { authRequest = await Oidc.createAuthRequest(options); try { - // call submitAuthResponse - await Oidc.submitAuthResponse( - providerIdentity.did.uri, - authRequest, - randomPin, - testHarness.agent - ); + // generate the DID + const delegateBearerDid = await DidJwk.create(); + const delegatePortableDid = await delegateBearerDid.export(); + await Oidc.createAuthResponseGrants( + delegatePortableDid, + providerIdentity.did.uri, + authRequest.permissionRequests, + testHarness.agent + ); expect.fail('should have thrown an error'); } catch (error: any) { expect(error.message).to.equal('All permission scopes must match the protocol uri they are provided with.'); From c11e9f617e476c9785e15908f33ed855d30d50d8 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Fri, 11 Oct 2024 14:23:59 -0400 Subject: [PATCH 2/7] fix linter --- packages/agent/src/oidc.ts | 42 ++++++++++++------------ packages/agent/tests/connect.spec.ts | 48 ++++++++++++++-------------- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/packages/agent/src/oidc.ts b/packages/agent/src/oidc.ts index 7aec60172..2e82778e5 100644 --- a/packages/agent/src/oidc.ts +++ b/packages/agent/src/oidc.ts @@ -751,28 +751,28 @@ async function createAuthResponseGrants( permissionRequests: ConnectPermissionRequest[], agent: Web5Agent ) { - // TODO: roll back permissions and protocol configurations if an error occurs. Need a way to delete protocols to achieve this. - const processGrant = async (permissionRequest: ConnectPermissionRequest): Promise => { - const { protocolDefinition, permissionScopes } = permissionRequest; - - // We validate that all permission scopes match the protocol uri of the protocol definition they are provided with. - const grantsMatchProtocolUri = permissionScopes.every(scope => 'protocol' in scope && scope.protocol === protocolDefinition.protocol); - if (!grantsMatchProtocolUri) { - throw new Error('All permission scopes must match the protocol uri they are provided with.'); - } - - await prepareProtocol(selectedDid, agent, protocolDefinition); - - return await Oidc.createPermissionGrants( - selectedDid, - delegatedPortableDid, - agent, - permissionScopes - ); - }; + // TODO: roll back permissions and protocol configurations if an error occurs. Need a way to delete protocols to achieve this. + const processGrant = async (permissionRequest: ConnectPermissionRequest): Promise => { + const { protocolDefinition, permissionScopes } = permissionRequest; + + // We validate that all permission scopes match the protocol uri of the protocol definition they are provided with. + const grantsMatchProtocolUri = permissionScopes.every(scope => 'protocol' in scope && scope.protocol === protocolDefinition.protocol); + if (!grantsMatchProtocolUri) { + throw new Error('All permission scopes must match the protocol uri they are provided with.'); + } + + await prepareProtocol(selectedDid, agent, protocolDefinition); + + return await Oidc.createPermissionGrants( + selectedDid, + delegatedPortableDid, + agent, + permissionScopes + ); + }; - const delegateGrants = await Promise.all(permissionRequests.map(processGrant)); - return delegateGrants .flat(); + const delegateGrants = await Promise.all(permissionRequests.map(processGrant)); + return delegateGrants .flat(); } /** diff --git a/packages/agent/tests/connect.spec.ts b/packages/agent/tests/connect.spec.ts index 4c1315f09..f477efee5 100644 --- a/packages/agent/tests/connect.spec.ts +++ b/packages/agent/tests/connect.spec.ts @@ -702,15 +702,15 @@ describe('web5 connect', function () { try { // generate the DID - const delegateBearerDid = await DidJwk.create(); - const delegatePortableDid = await delegateBearerDid.export(); + const delegateBearerDid = await DidJwk.create(); + const delegatePortableDid = await delegateBearerDid.export(); - await Oidc.createAuthResponseGrants( - delegatePortableDid, - providerIdentity.did.uri, - authRequest.permissionRequests, - testHarness.agent - ); + await Oidc.createAuthResponseGrants( + delegatePortableDid, + providerIdentity.did.uri, + authRequest.permissionRequests, + testHarness.agent + ); expect.fail('should have thrown an error'); } catch (error: any) { @@ -808,15 +808,15 @@ describe('web5 connect', function () { try { // generate the DID - const delegateBearerDid = await DidJwk.create(); - const delegatePortableDid = await delegateBearerDid.export(); + const delegateBearerDid = await DidJwk.create(); + const delegatePortableDid = await delegateBearerDid.export(); - await Oidc.createAuthResponseGrants( - delegatePortableDid, - providerIdentity.did.uri, - authRequest.permissionRequests, - testHarness.agent - ); + await Oidc.createAuthResponseGrants( + delegatePortableDid, + providerIdentity.did.uri, + authRequest.permissionRequests, + testHarness.agent + ); expect.fail('should have thrown an error'); } catch (error: any) { expect(error.message).to.equal('Could not fetch protocol: Some Error'); @@ -851,15 +851,15 @@ describe('web5 connect', function () { try { // generate the DID - const delegateBearerDid = await DidJwk.create(); - const delegatePortableDid = await delegateBearerDid.export(); + const delegateBearerDid = await DidJwk.create(); + const delegatePortableDid = await delegateBearerDid.export(); - await Oidc.createAuthResponseGrants( - delegatePortableDid, - providerIdentity.did.uri, - authRequest.permissionRequests, - testHarness.agent - ); + await Oidc.createAuthResponseGrants( + delegatePortableDid, + providerIdentity.did.uri, + authRequest.permissionRequests, + testHarness.agent + ); expect.fail('should have thrown an error'); } catch (error: any) { expect(error.message).to.equal('All permission scopes must match the protocol uri they are provided with.'); From 4feb245e8db40b550f904c41703e84bf35ff548d Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Fri, 11 Oct 2024 16:06:41 -0400 Subject: [PATCH 3/7] fix tests --- packages/agent/tests/connect.spec.ts | 339 +++++++++++---------------- 1 file changed, 141 insertions(+), 198 deletions(-) diff --git a/packages/agent/tests/connect.spec.ts b/packages/agent/tests/connect.spec.ts index f477efee5..61d5c4082 100644 --- a/packages/agent/tests/connect.spec.ts +++ b/packages/agent/tests/connect.spec.ts @@ -502,6 +502,147 @@ describe('web5 connect', function () { }); }); + + describe('createAuthResponseGrants', () => { + it('should fail if the send request fails for newly configured protocol', async () => { + sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); + sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); + + const permissionRequests = [{ protocolDefinition, permissionScopes }]; + + // spy send request + const sendRequestSpy = sinon.stub(testHarness.agent, 'sendDwnRequest').resolves({ + reply : { status: { code: 500, detail: 'Internal Server Error' } }, + messageCid : '' + }); + + // return without any entries + sinon + .stub(testHarness.agent, 'processDwnRequest') + .resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' } } }); + + try { + // generate the DID + const delegatePortableDid = await delegateBearerDid.export(); + + await Oidc.createAuthResponseGrants( + delegatePortableDid, + providerIdentity.did.uri, + permissionRequests, + testHarness.agent + ); + + expect.fail('should have thrown an error'); + } catch (error: any) { + expect(error.message).to.equal('Could not send protocol: Internal Server Error'); + expect(sendRequestSpy.callCount).to.equal(1); + } + }); + + it('should fail if the send request fails for existing protocol', async () => { + sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); + sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); + + const permissionRequests = [{ protocolDefinition, permissionScopes }]; + + // stub the processDwnRequest method to return a protocol entry + const protocolMessage = {} as DwnMessage[DwnInterface.ProtocolsConfigure]; + + // spy send request + const sendRequestSpy = sinon.stub(testHarness.agent, 'sendDwnRequest').resolves({ + reply : { status: { code: 500, detail: 'Internal Server Error' } }, + messageCid : '' + }); + + // mock returning the protocol entry + const processDwnRequestStub = sinon + .stub(testHarness.agent, 'processDwnRequest') + .resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' }, entries: [ protocolMessage ] } }); + + try { + // generate the DID + const delegatePortableDid = await delegateBearerDid.export(); + + await Oidc.createAuthResponseGrants( + delegatePortableDid, + providerIdentity.did.uri, + permissionRequests, + testHarness.agent + ); + + expect.fail('should have thrown an error'); + } catch (error: any) { + expect(error.message).to.equal('Could not send protocol: Internal Server Error'); + expect(processDwnRequestStub.callCount).to.equal(1); + expect(sendRequestSpy.callCount).to.equal(1); + } + }); + + it('should throw if a grant that is included in the request does not match the protocol definition', async () => { + sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); + sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); + + const callbackUrl = Oidc.buildOidcUrl({ + baseURL : 'http://localhost:3000', + endpoint : 'callback', + }); + + const mismatchedScopes = permissionScopes.map((scope) => ({ ...scope })) as RecordsPermissionScope[]; + mismatchedScopes[0].protocol = 'http://profile-protocol.xyz/other'; + const permissionRequests = [{ protocolDefinition, permissionScopes: mismatchedScopes }]; + + try { + // generate the DID + const delegatePortableDid = await delegateBearerDid.export(); + + await Oidc.createAuthResponseGrants( + delegatePortableDid, + providerIdentity.did.uri, + permissionRequests, + testHarness.agent + ); + expect.fail('should have thrown an error'); + } catch (error: any) { + expect(error.message).to.equal('All permission scopes must match the protocol uri they are provided with.'); + } + }); + + it('should throw if protocol could not be fetched at all', async () => { + sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); + sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); + + const permissionRequests = [{ protocolDefinition, permissionScopes }]; + + // spy send request + const sendRequestSpy = sinon.stub(testHarness.agent, 'sendDwnRequest').resolves({ + reply : { status: { code: 500, detail: 'Internal Server Error' } }, + messageCid : '' + }); + + // mock returning the protocol entry + const processDwnRequestStub = sinon + .stub(testHarness.agent, 'processDwnRequest') + .resolves({ messageCid: '', reply: { status: { code: 500, detail: 'Some Error'}, } }); + + try { + // generate the DID + const delegatePortableDid = await delegateBearerDid.export(); + + await Oidc.createAuthResponseGrants( + delegatePortableDid, + providerIdentity.did.uri, + permissionRequests, + testHarness.agent + ); + expect.fail('should have thrown an error'); + } catch (error: any) { + expect(error.message).to.equal('Could not fetch protocol: Some Error'); + expect(processDwnRequestStub.callCount).to.equal(1); + expect(sendRequestSpy.callCount).to.equal(0); + } + }); + }); + describe('submitAuthResponse', () => { it('should not attempt to configure the protocol if it already exists', async () => { // scenario: the wallet gets a request for a protocol that it already has configured @@ -667,204 +808,6 @@ describe('web5 connect', function () { expect(sendRequestSpy.callCount).to.equal(1); expect(sendRequestSpy.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsConfigure); }); - - it('should fail if the send request fails for newly configured protocol', async () => { - sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); - sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); - sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); - - const callbackUrl = Oidc.buildOidcUrl({ - baseURL : 'http://localhost:3000', - endpoint : 'callback', - }); - - const options = { - displayName : 'Sample App', - client_id : clientEphemeralPortableDid.uri, - scope : 'openid did:jwk', - // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), - // code_challenge_method : 'S256' as const, - permissionRequests : [{ protocolDefinition, permissionScopes }], - redirect_uri : callbackUrl, - }; - authRequest = await Oidc.createAuthRequest(options); - - // spy send request - const sendRequestSpy = sinon.stub(testHarness.agent, 'sendDwnRequest').resolves({ - reply : { status: { code: 500, detail: 'Internal Server Error' } }, - messageCid : '' - }); - - // return without any entries - const processDwnRequestStub = sinon - .stub(testHarness.agent, 'processDwnRequest') - .resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' } } }); - - try { - // generate the DID - const delegateBearerDid = await DidJwk.create(); - const delegatePortableDid = await delegateBearerDid.export(); - - await Oidc.createAuthResponseGrants( - delegatePortableDid, - providerIdentity.did.uri, - authRequest.permissionRequests, - testHarness.agent - ); - - expect.fail('should have thrown an error'); - } catch (error: any) { - expect(error.message).to.equal('Could not send protocol: Internal Server Error'); - expect(sendRequestSpy.callCount).to.equal(1); - } - }); - - it('should fail if the send request fails for existing protocol', async () => { - sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); - sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); - sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); - - const callbackUrl = Oidc.buildOidcUrl({ - baseURL : 'http://localhost:3000', - endpoint : 'callback', - }); - - const options = { - displayName : 'Sample App', - client_id : clientEphemeralPortableDid.uri, - scope : 'openid did:jwk', - // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), - // code_challenge_method : 'S256' as const, - permissionRequests : [{ protocolDefinition, permissionScopes }], - redirect_uri : callbackUrl, - }; - authRequest = await Oidc.createAuthRequest(options); - - // stub the processDwnRequest method to return a protocol entry - const protocolMessage = {} as DwnMessage[DwnInterface.ProtocolsConfigure]; - - // spy send request - const sendRequestSpy = sinon.stub(testHarness.agent, 'sendDwnRequest').resolves({ - reply : { status: { code: 500, detail: 'Internal Server Error' } }, - messageCid : '' - }); - - // mock returning the protocol entry - const processDwnRequestStub = sinon - .stub(testHarness.agent, 'processDwnRequest') - .resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' }, entries: [ protocolMessage ] } }); - - try { - // generate the DID - const delegateBearerDid = await DidJwk.create(); - const delegatePortableDid = await delegateBearerDid.export(); - - await Oidc.createAuthResponseGrants( - delegatePortableDid, - providerIdentity.did.uri, - authRequest.permissionRequests, - testHarness.agent - ); - - expect.fail('should have thrown an error'); - } catch (error: any) { - expect(error.message).to.equal('Could not send protocol: Internal Server Error'); - expect(processDwnRequestStub.callCount).to.equal(1); - expect(sendRequestSpy.callCount).to.equal(1); - } - }); - - it('should throw if protocol could not be fetched at all', async () => { - sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); - sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); - sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); - - const callbackUrl = Oidc.buildOidcUrl({ - baseURL : 'http://localhost:3000', - endpoint : 'callback', - }); - - const options = { - displayName : 'Sample App', - client_id : clientEphemeralPortableDid.uri, - scope : 'openid did:jwk', - // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), - // code_challenge_method : 'S256' as const, - permissionRequests : [{ protocolDefinition, permissionScopes }], - redirect_uri : callbackUrl, - }; - authRequest = await Oidc.createAuthRequest(options); - - // spy send request - const sendRequestSpy = sinon.stub(testHarness.agent, 'sendDwnRequest').resolves({ - reply : { status: { code: 500, detail: 'Internal Server Error' } }, - messageCid : '' - }); - - // mock returning the protocol entry - const processDwnRequestStub = sinon - .stub(testHarness.agent, 'processDwnRequest') - .resolves({ messageCid: '', reply: { status: { code: 500, detail: 'Some Error'}, } }); - - try { - // generate the DID - const delegateBearerDid = await DidJwk.create(); - const delegatePortableDid = await delegateBearerDid.export(); - - await Oidc.createAuthResponseGrants( - delegatePortableDid, - providerIdentity.did.uri, - authRequest.permissionRequests, - testHarness.agent - ); - expect.fail('should have thrown an error'); - } catch (error: any) { - expect(error.message).to.equal('Could not fetch protocol: Some Error'); - expect(processDwnRequestStub.callCount).to.equal(1); - expect(sendRequestSpy.callCount).to.equal(0); - } - }); - - it('should throw if a grant that is included in the request does not match the protocol definition', async () => { - sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); - sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); - sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); - - const callbackUrl = Oidc.buildOidcUrl({ - baseURL : 'http://localhost:3000', - endpoint : 'callback', - }); - - const mismatchedScopes = [...permissionScopes]; - mismatchedScopes[0].protocol = 'http://profile-protocol.xyz/other'; - - const options = { - displayName : 'Sample App', - client_id : clientEphemeralPortableDid.uri, - scope : 'openid did:jwk', - // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), - // code_challenge_method : 'S256' as const, - permissionRequests : [{ protocolDefinition, permissionScopes }], - redirect_uri : callbackUrl, - }; - authRequest = await Oidc.createAuthRequest(options); - - try { - // generate the DID - const delegateBearerDid = await DidJwk.create(); - const delegatePortableDid = await delegateBearerDid.export(); - - await Oidc.createAuthResponseGrants( - delegatePortableDid, - providerIdentity.did.uri, - authRequest.permissionRequests, - testHarness.agent - ); - expect.fail('should have thrown an error'); - } catch (error: any) { - expect(error.message).to.equal('All permission scopes must match the protocol uri they are provided with.'); - } - }); }); describe('createPermissionRequestForProtocol', () => { From afc0c2a0c5e87377be034cca42962d308257d6a7 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Fri, 11 Oct 2024 16:18:49 -0400 Subject: [PATCH 4/7] add changeset --- .changeset/tidy-ladybugs-lick.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/tidy-ladybugs-lick.md diff --git a/.changeset/tidy-ladybugs-lick.md b/.changeset/tidy-ladybugs-lick.md new file mode 100644 index 000000000..c9ec3b1d0 --- /dev/null +++ b/.changeset/tidy-ladybugs-lick.md @@ -0,0 +1,8 @@ +--- +"@web5/agent": minor +"@web5/identity-agent": minor +"@web5/proxy-agent": minor +"@web5/user-agent": minor +--- + +Break apart OIDC `submitAuthResponse` From a6aaab02b90dfebf6a6328111aa4bd1f3e8f28db Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Mon, 14 Oct 2024 14:46:57 -0400 Subject: [PATCH 5/7] make change work with existing oidc function --- packages/agent/src/oidc.ts | 12 +++++++-- packages/agent/tests/connect.spec.ts | 37 ++++------------------------ 2 files changed, 15 insertions(+), 34 deletions(-) diff --git a/packages/agent/src/oidc.ts b/packages/agent/src/oidc.ts index 2e82778e5..3e83bc196 100644 --- a/packages/agent/src/oidc.ts +++ b/packages/agent/src/oidc.ts @@ -788,11 +788,19 @@ async function submitAuthResponse( selectedDid: string, authRequest: Web5ConnectAuthRequest, randomPin: string, - delegateBearerDid: BearerDid, - delegateGrants: DwnDataEncodedRecordsWriteMessage[] + agent: Web5Agent, ) { + const delegateBearerDid = await DidJwk.create(); const delegatePortableDid = await delegateBearerDid.export(); + // Create the permission grants for the selected DID. + const delegateGrants = await Oidc.createAuthResponseGrants( + delegatePortableDid, + selectedDid, + authRequest.permissionRequests, + agent + ); + logger.log('Generating auth response object...'); const responseObject = await Oidc.createResponseObject({ //* the IDP's did that was selected to be connected diff --git a/packages/agent/tests/connect.spec.ts b/packages/agent/tests/connect.spec.ts index 61d5c4082..8a764375b 100644 --- a/packages/agent/tests/connect.spec.ts +++ b/packages/agent/tests/connect.spec.ts @@ -363,6 +363,7 @@ describe('web5 connect', function () { it('should send the encrypted jwe authresponse to the server', async () => { sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); + sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); const formEncodedRequest = new URLSearchParams({ id_token : authResponseJwe, @@ -402,8 +403,7 @@ describe('web5 connect', function () { selectedDid, authRequest, randomPin, - delegateBearerDid, - delegatedGrants + testHarness.agent ); expect(fetchSpy.calledOnce).to.be.true; }); @@ -683,22 +683,13 @@ describe('web5 connect', function () { const delegatePortableDid = await delegateBearerDid.export(); - const delegatedGrants = await Oidc.createAuthResponseGrants( + await Oidc.createAuthResponseGrants( delegatePortableDid, providerIdentity.did.uri, authRequest.permissionRequests, testHarness.agent ); - // call submitAuthResponse - await Oidc.submitAuthResponse( - providerIdentity.did.uri, - authRequest, - randomPin, - delegateBearerDid, - delegatedGrants - ); - // expect the process request to only be called once for ProtocolsQuery expect(processDwnRequestStub.callCount).to.equal(1); expect(processDwnRequestStub.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsQuery); @@ -746,22 +737,13 @@ describe('web5 connect', function () { // generate the DID const delegatePortableDid = await delegateBearerDid.export(); - const delegatedGrants = await Oidc.createAuthResponseGrants( + await Oidc.createAuthResponseGrants( delegatePortableDid, providerIdentity.did.uri, authRequest.permissionRequests, testHarness.agent ); - // call submitAuthResponse - await Oidc.submitAuthResponse( - providerIdentity.did.uri, - authRequest, - randomPin, - delegateBearerDid, - delegatedGrants - ); - // expect the process request to be called for query and configure expect(processDwnRequestStub.callCount).to.equal(2); expect(processDwnRequestStub.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsQuery); @@ -783,22 +765,13 @@ describe('web5 connect', function () { const delegateBearerDid2 = await DidJwk.create(); const delegatePortableDid2 = await delegateBearerDid2.export(); - const delegatedGrants2 = await Oidc.createAuthResponseGrants( + await Oidc.createAuthResponseGrants( delegatePortableDid2, providerIdentity.did.uri, authRequest.permissionRequests, testHarness.agent ); - // call submitAuthResponse - await Oidc.submitAuthResponse( - providerIdentity.did.uri, - authRequest, - randomPin, - delegateBearerDid2, - delegatedGrants2 - ); - // expect the process request to be called for query and configure expect(processDwnRequestStub.callCount).to.equal(2); expect(processDwnRequestStub.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsQuery); From f980ce52cce53d34bc034b4674c97540f9d83b24 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Mon, 14 Oct 2024 14:50:28 -0400 Subject: [PATCH 6/7] move tests to the end of the file --- packages/agent/tests/connect.spec.ts | 326 +++++++-------------------- 1 file changed, 87 insertions(+), 239 deletions(-) diff --git a/packages/agent/tests/connect.spec.ts b/packages/agent/tests/connect.spec.ts index 8a764375b..639ac4f68 100644 --- a/packages/agent/tests/connect.spec.ts +++ b/packages/agent/tests/connect.spec.ts @@ -388,17 +388,6 @@ describe('web5 connect', function () { ); const selectedDid = providerIdentity.did.uri; - - // generate the DID - const delegatePortableDid = await delegateBearerDid.export(); - - const delegatedGrants = await Oidc.createAuthResponseGrants( - delegatePortableDid, - selectedDid, - authRequest.permissionRequests, - testHarness.agent - ); - await Oidc.submitAuthResponse( selectedDid, authRequest, @@ -502,6 +491,93 @@ describe('web5 connect', function () { }); }); + describe('createPermissionRequestForProtocol', () => { + it('should add sync permissions to all requests', async () => { + const protocol:DwnProtocolDefinition = { + published : true, + protocol : 'https://exmaple.org/protocols/social', + types : { + note: { + schema : 'https://example.org/schemas/note', + dataFormats : [ 'application/json', 'text/plain' ], + } + }, + structure: { + note: {} + } + }; + + const permissionRequests = WalletConnect.createPermissionRequestForProtocol({ + definition: protocol, permissions: [] + }); + + expect(permissionRequests.protocolDefinition).to.deep.equal(protocol); + expect(permissionRequests.permissionScopes.length).to.equal(4); // only includes the sync permissions + protocol query permission + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Messages && scope.method === DwnMethodName.Read)).to.not.be.undefined; + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Messages && scope.method === DwnMethodName.Query)).to.not.be.undefined; + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Messages && scope.method === DwnMethodName.Subscribe)).to.not.be.undefined; + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Query)).to.not.be.undefined; + }); + + it('should add requested permissions to the request', async () => { + const protocol:DwnProtocolDefinition = { + published : true, + protocol : 'https://exmaple.org/protocols/social', + types : { + note: { + schema : 'https://example.org/schemas/note', + dataFormats : [ 'application/json', 'text/plain' ], + } + }, + structure: { + note: {} + } + }; + + const permissionRequests = WalletConnect.createPermissionRequestForProtocol({ + definition: protocol, permissions: ['write', 'read'] + }); + + expect(permissionRequests.protocolDefinition).to.deep.equal(protocol); + + // the 3 sync permissions plus the 2 requested permissions, and a protocol query permission + expect(permissionRequests.permissionScopes.length).to.equal(6); + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Read)).to.not.be.undefined; + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Write)).to.not.be.undefined; + }); + + it('supports requesting `read`, `write`, `delete`, `query`, `subscribe` and `configure` permissions', async () => { + const protocol:DwnProtocolDefinition = { + published : true, + protocol : 'https://exmaple.org/protocols/social', + types : { + note: { + schema : 'https://example.org/schemas/note', + dataFormats : [ 'application/json', 'text/plain' ], + } + }, + structure: { + note: {} + } + }; + + const permissionRequests = WalletConnect.createPermissionRequestForProtocol({ + definition: protocol, permissions: ['write', 'read', 'delete', 'query', 'subscribe', 'configure'] + }); + + expect(permissionRequests.protocolDefinition).to.deep.equal(protocol); + + // the 3 sync permissions plus the 5 requested permissions + expect(permissionRequests.permissionScopes.length).to.equal(10); + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Read)).to.not.be.undefined; + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Write)).to.not.be.undefined; + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Delete)).to.not.be.undefined; + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Query)).to.not.be.undefined; + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Subscribe)).to.not.be.undefined; + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Query)).to.not.be.undefined; + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Configure)).to.not.be.undefined; + }); + }); describe('createAuthResponseGrants', () => { it('should fail if the send request fails for newly configured protocol', async () => { @@ -642,232 +718,4 @@ describe('web5 connect', function () { } }); }); - - describe('submitAuthResponse', () => { - it('should not attempt to configure the protocol if it already exists', async () => { - // scenario: the wallet gets a request for a protocol that it already has configured - // the wallet should not attempt to re-configure, but instead ensure that the protocol is - // sent to the remote DWN for the requesting client to be able to sync it down later - - sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); - sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); - - const callbackUrl = Oidc.buildOidcUrl({ - baseURL : 'http://localhost:3000', - endpoint : 'callback', - }); - - const options = { - displayName : 'Sample App', - client_id : clientEphemeralPortableDid.uri, - scope : 'openid did:jwk', - // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), - // code_challenge_method : 'S256' as const, - permissionRequests : [{ protocolDefinition, permissionScopes }], - redirect_uri : callbackUrl, - }; - authRequest = await Oidc.createAuthRequest(options); - - // stub the processDwnRequest method to return a protocol entry - const protocolMessage = {} as DwnMessage[DwnInterface.ProtocolsConfigure]; - - // spy send request - const sendRequestSpy = sinon.stub(testHarness.agent, 'sendDwnRequest').resolves({ - messageCid : '', - reply : { status: { code: 202, detail: 'OK' } } - }); - - const processDwnRequestStub = sinon - .stub(testHarness.agent, 'processDwnRequest') - .resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' }, entries: [ protocolMessage ]} }); - - const delegatePortableDid = await delegateBearerDid.export(); - - await Oidc.createAuthResponseGrants( - delegatePortableDid, - providerIdentity.did.uri, - authRequest.permissionRequests, - testHarness.agent - ); - - // expect the process request to only be called once for ProtocolsQuery - expect(processDwnRequestStub.callCount).to.equal(1); - expect(processDwnRequestStub.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsQuery); - - // send request should be called once as a ProtocolsConfigure - expect(sendRequestSpy.callCount).to.equal(1); - expect(sendRequestSpy.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsConfigure); - }); - - it('should configure the protocol if it does not exist', async () => { - // scenario: the wallet gets a request for a protocol that it does not have configured - // the wallet should attempt to configure the protocol and then send the protocol to the remote DWN - - // looks for a response of 404, empty entries array or missing entries array - - sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); - sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); - - const callbackUrl = Oidc.buildOidcUrl({ - baseURL : 'http://localhost:3000', - endpoint : 'callback', - }); - - const options = { - displayName : 'Sample App', - client_id : clientEphemeralPortableDid.uri, - scope : 'openid did:jwk', - // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), - // code_challenge_method : 'S256' as const, - permissionRequests : [{ protocolDefinition, permissionScopes }], - redirect_uri : callbackUrl, - }; - authRequest = await Oidc.createAuthRequest(options); - - // spy send request - const sendRequestSpy = sinon.stub(testHarness.agent, 'sendDwnRequest').resolves({ - messageCid : '', - reply : { status: { code: 202, detail: 'OK' } } - }); - - const processDwnRequestStub = sinon - .stub(testHarness.agent, 'processDwnRequest') - .resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' }, entries: [ ] } }); - - // generate the DID - const delegatePortableDid = await delegateBearerDid.export(); - - await Oidc.createAuthResponseGrants( - delegatePortableDid, - providerIdentity.did.uri, - authRequest.permissionRequests, - testHarness.agent - ); - - // expect the process request to be called for query and configure - expect(processDwnRequestStub.callCount).to.equal(2); - expect(processDwnRequestStub.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsQuery); - expect(processDwnRequestStub.secondCall.args[0].messageType).to.equal(DwnInterface.ProtocolsConfigure); - - // send request should be called once as a ProtocolsConfigure - expect(sendRequestSpy.callCount).to.equal(1); - expect(sendRequestSpy.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsConfigure); - - // reset the spys - processDwnRequestStub.resetHistory(); - sendRequestSpy.resetHistory(); - - // processDwnRequestStub should resolve a 200 with no entires - processDwnRequestStub.resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' } } }); - - - // generate the DID - const delegateBearerDid2 = await DidJwk.create(); - const delegatePortableDid2 = await delegateBearerDid2.export(); - - await Oidc.createAuthResponseGrants( - delegatePortableDid2, - providerIdentity.did.uri, - authRequest.permissionRequests, - testHarness.agent - ); - - // expect the process request to be called for query and configure - expect(processDwnRequestStub.callCount).to.equal(2); - expect(processDwnRequestStub.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsQuery); - expect(processDwnRequestStub.secondCall.args[0].messageType).to.equal(DwnInterface.ProtocolsConfigure); - - // send request should be called once as a ProtocolsConfigure - expect(sendRequestSpy.callCount).to.equal(1); - expect(sendRequestSpy.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsConfigure); - }); - }); - - describe('createPermissionRequestForProtocol', () => { - it('should add sync permissions to all requests', async () => { - const protocol:DwnProtocolDefinition = { - published : true, - protocol : 'https://exmaple.org/protocols/social', - types : { - note: { - schema : 'https://example.org/schemas/note', - dataFormats : [ 'application/json', 'text/plain' ], - } - }, - structure: { - note: {} - } - }; - - const permissionRequests = WalletConnect.createPermissionRequestForProtocol({ - definition: protocol, permissions: [] - }); - - expect(permissionRequests.protocolDefinition).to.deep.equal(protocol); - expect(permissionRequests.permissionScopes.length).to.equal(4); // only includes the sync permissions + protocol query permission - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Messages && scope.method === DwnMethodName.Read)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Messages && scope.method === DwnMethodName.Query)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Messages && scope.method === DwnMethodName.Subscribe)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Query)).to.not.be.undefined; - }); - - it('should add requested permissions to the request', async () => { - const protocol:DwnProtocolDefinition = { - published : true, - protocol : 'https://exmaple.org/protocols/social', - types : { - note: { - schema : 'https://example.org/schemas/note', - dataFormats : [ 'application/json', 'text/plain' ], - } - }, - structure: { - note: {} - } - }; - - const permissionRequests = WalletConnect.createPermissionRequestForProtocol({ - definition: protocol, permissions: ['write', 'read'] - }); - - expect(permissionRequests.protocolDefinition).to.deep.equal(protocol); - - // the 3 sync permissions plus the 2 requested permissions, and a protocol query permission - expect(permissionRequests.permissionScopes.length).to.equal(6); - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Read)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Write)).to.not.be.undefined; - }); - - it('supports requesting `read`, `write`, `delete`, `query`, `subscribe` and `configure` permissions', async () => { - const protocol:DwnProtocolDefinition = { - published : true, - protocol : 'https://exmaple.org/protocols/social', - types : { - note: { - schema : 'https://example.org/schemas/note', - dataFormats : [ 'application/json', 'text/plain' ], - } - }, - structure: { - note: {} - } - }; - - const permissionRequests = WalletConnect.createPermissionRequestForProtocol({ - definition: protocol, permissions: ['write', 'read', 'delete', 'query', 'subscribe', 'configure'] - }); - - expect(permissionRequests.protocolDefinition).to.deep.equal(protocol); - - // the 3 sync permissions plus the 5 requested permissions - expect(permissionRequests.permissionScopes.length).to.equal(10); - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Read)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Write)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Delete)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Query)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Subscribe)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Query)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Configure)).to.not.be.undefined; - }); - }); }); From dae557576a000a57144fde58e271ea32e07bdd9d Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Mon, 14 Oct 2024 14:55:39 -0400 Subject: [PATCH 7/7] revert updates tests --- packages/agent/tests/connect.spec.ts | 419 +++++++++++++++++++-------- 1 file changed, 301 insertions(+), 118 deletions(-) diff --git a/packages/agent/tests/connect.spec.ts b/packages/agent/tests/connect.spec.ts index 639ac4f68..9f34a8e10 100644 --- a/packages/agent/tests/connect.spec.ts +++ b/packages/agent/tests/connect.spec.ts @@ -491,100 +491,160 @@ describe('web5 connect', function () { }); }); - describe('createPermissionRequestForProtocol', () => { - it('should add sync permissions to all requests', async () => { - const protocol:DwnProtocolDefinition = { - published : true, - protocol : 'https://exmaple.org/protocols/social', - types : { - note: { - schema : 'https://example.org/schemas/note', - dataFormats : [ 'application/json', 'text/plain' ], - } - }, - structure: { - note: {} - } - }; + describe('submitAuthResponse', () => { + it('should not attempt to configure the protocol if it already exists', async () => { + // scenario: the wallet gets a request for a protocol that it already has configured + // the wallet should not attempt to re-configure, but instead ensure that the protocol is + // sent to the remote DWN for the requesting client to be able to sync it down later - const permissionRequests = WalletConnect.createPermissionRequestForProtocol({ - definition: protocol, permissions: [] - }); + sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); + sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); + sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); - expect(permissionRequests.protocolDefinition).to.deep.equal(protocol); - expect(permissionRequests.permissionScopes.length).to.equal(4); // only includes the sync permissions + protocol query permission - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Messages && scope.method === DwnMethodName.Read)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Messages && scope.method === DwnMethodName.Query)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Messages && scope.method === DwnMethodName.Subscribe)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Query)).to.not.be.undefined; - }); + const callbackUrl = Oidc.buildOidcUrl({ + baseURL : 'http://localhost:3000', + endpoint : 'callback', + }); - it('should add requested permissions to the request', async () => { - const protocol:DwnProtocolDefinition = { - published : true, - protocol : 'https://exmaple.org/protocols/social', - types : { - note: { - schema : 'https://example.org/schemas/note', - dataFormats : [ 'application/json', 'text/plain' ], - } - }, - structure: { - note: {} - } + const options = { + displayName : 'Sample App', + client_id : clientEphemeralPortableDid.uri, + scope : 'openid did:jwk', + // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), + // code_challenge_method : 'S256' as const, + permissionRequests : [{ protocolDefinition, permissionScopes }], + redirect_uri : callbackUrl, }; + authRequest = await Oidc.createAuthRequest(options); - const permissionRequests = WalletConnect.createPermissionRequestForProtocol({ - definition: protocol, permissions: ['write', 'read'] + // stub the processDwnRequest method to return a protocol entry + const protocolMessage = {} as DwnMessage[DwnInterface.ProtocolsConfigure]; + + // spy send request + const sendRequestSpy = sinon.stub(testHarness.agent, 'sendDwnRequest').resolves({ + messageCid : '', + reply : { status: { code: 202, detail: 'OK' } } }); - expect(permissionRequests.protocolDefinition).to.deep.equal(protocol); + const processDwnRequestStub = sinon + .stub(testHarness.agent, 'processDwnRequest') + .resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' }, entries: [ protocolMessage ]} }); - // the 3 sync permissions plus the 2 requested permissions, and a protocol query permission - expect(permissionRequests.permissionScopes.length).to.equal(6); - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Read)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Write)).to.not.be.undefined; + // call submitAuthResponse + await Oidc.submitAuthResponse( + providerIdentity.did.uri, + authRequest, + randomPin, + testHarness.agent + ); + + // expect the process request to only be called once for ProtocolsQuery + expect(processDwnRequestStub.callCount).to.equal(1); + expect(processDwnRequestStub.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsQuery); + + // send request should be called once as a ProtocolsConfigure + expect(sendRequestSpy.callCount).to.equal(1); + expect(sendRequestSpy.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsConfigure); }); - it('supports requesting `read`, `write`, `delete`, `query`, `subscribe` and `configure` permissions', async () => { - const protocol:DwnProtocolDefinition = { - published : true, - protocol : 'https://exmaple.org/protocols/social', - types : { - note: { - schema : 'https://example.org/schemas/note', - dataFormats : [ 'application/json', 'text/plain' ], - } - }, - structure: { - note: {} - } + it('should configure the protocol if it does not exist', async () => { + // scenario: the wallet gets a request for a protocol that it does not have configured + // the wallet should attempt to configure the protocol and then send the protocol to the remote DWN + + // looks for a response of 404, empty entries array or missing entries array + + sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); + sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); + sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); + + const callbackUrl = Oidc.buildOidcUrl({ + baseURL : 'http://localhost:3000', + endpoint : 'callback', + }); + + const options = { + displayName : 'Sample App', + client_id : clientEphemeralPortableDid.uri, + scope : 'openid did:jwk', + // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), + // code_challenge_method : 'S256' as const, + permissionRequests : [{ protocolDefinition, permissionScopes }], + redirect_uri : callbackUrl, }; + authRequest = await Oidc.createAuthRequest(options); - const permissionRequests = WalletConnect.createPermissionRequestForProtocol({ - definition: protocol, permissions: ['write', 'read', 'delete', 'query', 'subscribe', 'configure'] + // spy send request + const sendRequestSpy = sinon.stub(testHarness.agent, 'sendDwnRequest').resolves({ + messageCid : '', + reply : { status: { code: 202, detail: 'OK' } } }); - expect(permissionRequests.protocolDefinition).to.deep.equal(protocol); + const processDwnRequestStub = sinon + .stub(testHarness.agent, 'processDwnRequest') + .resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' }, entries: [ ] } }); - // the 3 sync permissions plus the 5 requested permissions - expect(permissionRequests.permissionScopes.length).to.equal(10); - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Read)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Write)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Delete)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Query)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Subscribe)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Query)).to.not.be.undefined; - expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Configure)).to.not.be.undefined; + // call submitAuthResponse + await Oidc.submitAuthResponse( + providerIdentity.did.uri, + authRequest, + randomPin, + testHarness.agent + ); + + // expect the process request to be called for query and configure + expect(processDwnRequestStub.callCount).to.equal(2); + expect(processDwnRequestStub.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsQuery); + expect(processDwnRequestStub.secondCall.args[0].messageType).to.equal(DwnInterface.ProtocolsConfigure); + + // send request should be called once as a ProtocolsConfigure + expect(sendRequestSpy.callCount).to.equal(1); + expect(sendRequestSpy.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsConfigure); + + // reset the spys + processDwnRequestStub.resetHistory(); + sendRequestSpy.resetHistory(); + + // processDwnRequestStub should resolve a 200 with no entires + processDwnRequestStub.resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' } } }); + + // call submitAuthResponse + await Oidc.submitAuthResponse( + providerIdentity.did.uri, + authRequest, + randomPin, + testHarness.agent + ); + + // expect the process request to be called for query and configure + expect(processDwnRequestStub.callCount).to.equal(2); + expect(processDwnRequestStub.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsQuery); + expect(processDwnRequestStub.secondCall.args[0].messageType).to.equal(DwnInterface.ProtocolsConfigure); + + // send request should be called once as a ProtocolsConfigure + expect(sendRequestSpy.callCount).to.equal(1); + expect(sendRequestSpy.firstCall.args[0].messageType).to.equal(DwnInterface.ProtocolsConfigure); }); - }); - describe('createAuthResponseGrants', () => { it('should fail if the send request fails for newly configured protocol', async () => { sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); + sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); - const permissionRequests = [{ protocolDefinition, permissionScopes }]; + const callbackUrl = Oidc.buildOidcUrl({ + baseURL : 'http://localhost:3000', + endpoint : 'callback', + }); + + const options = { + displayName : 'Sample App', + client_id : clientEphemeralPortableDid.uri, + scope : 'openid did:jwk', + // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), + // code_challenge_method : 'S256' as const, + permissionRequests : [{ protocolDefinition, permissionScopes }], + redirect_uri : callbackUrl, + }; + authRequest = await Oidc.createAuthRequest(options); // spy send request const sendRequestSpy = sinon.stub(testHarness.agent, 'sendDwnRequest').resolves({ @@ -593,18 +653,16 @@ describe('web5 connect', function () { }); // return without any entries - sinon + const processDwnRequestStub = sinon .stub(testHarness.agent, 'processDwnRequest') .resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' } } }); try { - // generate the DID - const delegatePortableDid = await delegateBearerDid.export(); - - await Oidc.createAuthResponseGrants( - delegatePortableDid, + // call submitAuthResponse + await Oidc.submitAuthResponse( providerIdentity.did.uri, - permissionRequests, + authRequest, + randomPin, testHarness.agent ); @@ -618,8 +676,23 @@ describe('web5 connect', function () { it('should fail if the send request fails for existing protocol', async () => { sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); + sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); + + const callbackUrl = Oidc.buildOidcUrl({ + baseURL : 'http://localhost:3000', + endpoint : 'callback', + }); - const permissionRequests = [{ protocolDefinition, permissionScopes }]; + const options = { + displayName : 'Sample App', + client_id : clientEphemeralPortableDid.uri, + scope : 'openid did:jwk', + // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), + // code_challenge_method : 'S256' as const, + permissionRequests : [{ protocolDefinition, permissionScopes }], + redirect_uri : callbackUrl, + }; + authRequest = await Oidc.createAuthRequest(options); // stub the processDwnRequest method to return a protocol entry const protocolMessage = {} as DwnMessage[DwnInterface.ProtocolsConfigure]; @@ -636,13 +709,11 @@ describe('web5 connect', function () { .resolves({ messageCid: '', reply: { status: { code: 200, detail: 'OK' }, entries: [ protocolMessage ] } }); try { - // generate the DID - const delegatePortableDid = await delegateBearerDid.export(); - - await Oidc.createAuthResponseGrants( - delegatePortableDid, + // call submitAuthResponse + await Oidc.submitAuthResponse( providerIdentity.did.uri, - permissionRequests, + authRequest, + randomPin, testHarness.agent ); @@ -654,68 +725,180 @@ describe('web5 connect', function () { } }); - it('should throw if a grant that is included in the request does not match the protocol definition', async () => { + it('should throw if protocol could not be fetched at all', async () => { sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); + sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); const callbackUrl = Oidc.buildOidcUrl({ baseURL : 'http://localhost:3000', endpoint : 'callback', }); - const mismatchedScopes = permissionScopes.map((scope) => ({ ...scope })) as RecordsPermissionScope[]; - mismatchedScopes[0].protocol = 'http://profile-protocol.xyz/other'; - const permissionRequests = [{ protocolDefinition, permissionScopes: mismatchedScopes }]; + const options = { + displayName : 'Sample App', + client_id : clientEphemeralPortableDid.uri, + scope : 'openid did:jwk', + // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), + // code_challenge_method : 'S256' as const, + permissionRequests : [{ protocolDefinition, permissionScopes }], + redirect_uri : callbackUrl, + }; + authRequest = await Oidc.createAuthRequest(options); - try { - // generate the DID - const delegatePortableDid = await delegateBearerDid.export(); + // spy send request + const sendRequestSpy = sinon.stub(testHarness.agent, 'sendDwnRequest').resolves({ + reply : { status: { code: 500, detail: 'Internal Server Error' } }, + messageCid : '' + }); + + // mock returning the protocol entry + const processDwnRequestStub = sinon + .stub(testHarness.agent, 'processDwnRequest') + .resolves({ messageCid: '', reply: { status: { code: 500, detail: 'Some Error'}, } }); - await Oidc.createAuthResponseGrants( - delegatePortableDid, + try { + // call submitAuthResponse + await Oidc.submitAuthResponse( providerIdentity.did.uri, - permissionRequests, + authRequest, + randomPin, testHarness.agent ); + expect.fail('should have thrown an error'); } catch (error: any) { - expect(error.message).to.equal('All permission scopes must match the protocol uri they are provided with.'); + expect(error.message).to.equal('Could not fetch protocol: Some Error'); + expect(processDwnRequestStub.callCount).to.equal(1); + expect(sendRequestSpy.callCount).to.equal(0); } }); - it('should throw if protocol could not be fetched at all', async () => { + it('should throw if a grant that is included in the request does not match the protocol definition', async () => { sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); + sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); - const permissionRequests = [{ protocolDefinition, permissionScopes }]; - - // spy send request - const sendRequestSpy = sinon.stub(testHarness.agent, 'sendDwnRequest').resolves({ - reply : { status: { code: 500, detail: 'Internal Server Error' } }, - messageCid : '' + const callbackUrl = Oidc.buildOidcUrl({ + baseURL : 'http://localhost:3000', + endpoint : 'callback', }); - // mock returning the protocol entry - const processDwnRequestStub = sinon - .stub(testHarness.agent, 'processDwnRequest') - .resolves({ messageCid: '', reply: { status: { code: 500, detail: 'Some Error'}, } }); + const mismatchedScopes = [...permissionScopes]; + mismatchedScopes[0].protocol = 'http://profile-protocol.xyz/other'; - try { - // generate the DID - const delegatePortableDid = await delegateBearerDid.export(); + const options = { + displayName : 'Sample App', + client_id : clientEphemeralPortableDid.uri, + scope : 'openid did:jwk', + // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), + // code_challenge_method : 'S256' as const, + permissionRequests : [{ protocolDefinition, permissionScopes }], + redirect_uri : callbackUrl, + }; + authRequest = await Oidc.createAuthRequest(options); - await Oidc.createAuthResponseGrants( - delegatePortableDid, + try { + // call submitAuthResponse + await Oidc.submitAuthResponse( providerIdentity.did.uri, - permissionRequests, + authRequest, + randomPin, testHarness.agent ); + expect.fail('should have thrown an error'); } catch (error: any) { - expect(error.message).to.equal('Could not fetch protocol: Some Error'); - expect(processDwnRequestStub.callCount).to.equal(1); - expect(sendRequestSpy.callCount).to.equal(0); + expect(error.message).to.equal('All permission scopes must match the protocol uri they are provided with.'); } }); }); + + describe('createPermissionRequestForProtocol', () => { + it('should add sync permissions to all requests', async () => { + const protocol:DwnProtocolDefinition = { + published : true, + protocol : 'https://exmaple.org/protocols/social', + types : { + note: { + schema : 'https://example.org/schemas/note', + dataFormats : [ 'application/json', 'text/plain' ], + } + }, + structure: { + note: {} + } + }; + + const permissionRequests = WalletConnect.createPermissionRequestForProtocol({ + definition: protocol, permissions: [] + }); + + expect(permissionRequests.protocolDefinition).to.deep.equal(protocol); + expect(permissionRequests.permissionScopes.length).to.equal(4); // only includes the sync permissions + protocol query permission + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Messages && scope.method === DwnMethodName.Read)).to.not.be.undefined; + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Messages && scope.method === DwnMethodName.Query)).to.not.be.undefined; + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Messages && scope.method === DwnMethodName.Subscribe)).to.not.be.undefined; + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Query)).to.not.be.undefined; + }); + + it('should add requested permissions to the request', async () => { + const protocol:DwnProtocolDefinition = { + published : true, + protocol : 'https://exmaple.org/protocols/social', + types : { + note: { + schema : 'https://example.org/schemas/note', + dataFormats : [ 'application/json', 'text/plain' ], + } + }, + structure: { + note: {} + } + }; + + const permissionRequests = WalletConnect.createPermissionRequestForProtocol({ + definition: protocol, permissions: ['write', 'read'] + }); + + expect(permissionRequests.protocolDefinition).to.deep.equal(protocol); + + // the 3 sync permissions plus the 2 requested permissions, and a protocol query permission + expect(permissionRequests.permissionScopes.length).to.equal(6); + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Read)).to.not.be.undefined; + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Write)).to.not.be.undefined; + }); + + it('supports requesting `read`, `write`, `delete`, `query`, `subscribe` and `configure` permissions', async () => { + const protocol:DwnProtocolDefinition = { + published : true, + protocol : 'https://exmaple.org/protocols/social', + types : { + note: { + schema : 'https://example.org/schemas/note', + dataFormats : [ 'application/json', 'text/plain' ], + } + }, + structure: { + note: {} + } + }; + + const permissionRequests = WalletConnect.createPermissionRequestForProtocol({ + definition: protocol, permissions: ['write', 'read', 'delete', 'query', 'subscribe', 'configure'] + }); + + expect(permissionRequests.protocolDefinition).to.deep.equal(protocol); + + // the 3 sync permissions plus the 5 requested permissions + expect(permissionRequests.permissionScopes.length).to.equal(10); + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Read)).to.not.be.undefined; + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Write)).to.not.be.undefined; + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Delete)).to.not.be.undefined; + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Query)).to.not.be.undefined; + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Subscribe)).to.not.be.undefined; + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Query)).to.not.be.undefined; + expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Configure)).to.not.be.undefined; + }); + }); });