Skip to content

Commit fea0535

Browse files
authored
connect dwn integration (#850)
* init connect dwn integration * install the protocol in the wallet * cleanup * delete comment * delete console logs * cleanup * fix tests * Update audit-ci.json * fix npm audit * rebase * clean up PR * remove delegate grants return * add changeset * Create tidy-ants-shave.md
1 parent 2f7bbbe commit fea0535

11 files changed

+4410
-8007
lines changed

.changeset/spicy-forks-attack.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@web5/agent": patch
3+
---
4+
5+
integrate dwn grants into connect flow

.changeset/tidy-ants-shave.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@web5/api": minor
3+
---
4+
5+
connect methods now work with dwn and user agent and are no longer stubbed

examples/wallet-connect.html

+8-1
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,11 @@ <h1>Success</h1>
139139
method: "Query",
140140
protocol: "http://profile-protocol.xyz",
141141
},
142+
{
143+
interface: "Records",
144+
method: "Read",
145+
protocol: "http://profile-protocol.xyz",
146+
},
142147
];
143148

144149
try {
@@ -206,7 +211,9 @@ <h1>Success</h1>
206211
}
207212

208213
function goToEndScreen(delegateDid) {
209-
document.getElementById("didInformation").innerText = `${JSON.stringify(
214+
document.getElementById(
215+
"didInformation"
216+
).innerText = `delegateDid\n:${JSON.stringify(
210217
delegateDid
211218
)}`;
212219

package.json

+6-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"build": "pnpm --recursive --stream build",
2020
"test:node": "pnpm --recursive test:node",
2121
"audit-ci": "audit-ci --config ./audit-ci.json",
22-
"wallet:connect:example": "npx http-server & HTTP_SERVER_PID=$! && sleep 2 && open 'http://localhost:8080/examples/wallet-connect.html' && wait $HTTP_SERVER_PID"
22+
"wallet:connect:example": "npx http-server -c-1 & HTTP_SERVER_PID=$! && sleep 2 && open 'http://localhost:8080/examples/wallet-connect.html' && wait $HTTP_SERVER_PID"
2323
},
2424
"repository": {
2525
"type": "git",
@@ -43,7 +43,11 @@
4343
"ws@<8.17.1": ">=8.17.1",
4444
"braces@<3.0.3": ">=3.0.3",
4545
"fast-xml-parser@<4.4.1": ">=4.4.1",
46-
"@75lb/deep-merge@<1.1.2": ">=1.1.2"
46+
"@75lb/deep-merge@<1.1.2": ">=1.1.2",
47+
"elliptic@>=4.0.0 <=6.5.6": ">=6.5.7",
48+
"elliptic@>=2.0.0 <=6.5.6": ">=6.5.7",
49+
"elliptic@>=5.2.1 <=6.5.6": ">=6.5.7",
50+
"micromatch@<4.0.8": ">=4.0.8"
4751
}
4852
}
4953
}

packages/agent/src/connect.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,10 @@ async function initClient({
9090
// a route to its web5 connect provider flow and the params of where to fetch the auth request.
9191
const generatedWalletUri = new URL(walletUri);
9292
generatedWalletUri.searchParams.set('request_uri', parData.request_uri);
93-
generatedWalletUri.searchParams.set('encryption_key', Convert.uint8Array(encryptionKey).toBase64Url());
93+
generatedWalletUri.searchParams.set(
94+
'encryption_key',
95+
Convert.uint8Array(encryptionKey).toBase64Url()
96+
);
9497

9598
// call user's callback so they can send the URI to the wallet as they see fit
9699
onWalletUriReady(generatedWalletUri.toString());
@@ -115,9 +118,9 @@ async function initClient({
115118
})) as Web5ConnectAuthResponse;
116119

117120
return {
118-
delegateGrants : verifiedAuthResponse.delegateGrants,
119-
delegateDid : verifiedAuthResponse.delegateDid,
120-
connectedDid : verifiedAuthResponse.iss,
121+
delegateGrants : verifiedAuthResponse.delegateGrants,
122+
delegatePortableDid : verifiedAuthResponse.delegatePortableDid,
123+
connectedDid : verifiedAuthResponse.iss,
121124
};
122125
}
123126
}

packages/agent/src/oidc.ts

+152-54
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,14 @@ import { xchacha20poly1305 } from '@noble/ciphers/chacha';
1313
import type { ConnectPermissionRequest } from './connect.js';
1414
import { DidDocument, DidJwk, PortableDid, type BearerDid } from '@web5/dids';
1515
import { AgentDwnApi } from './dwn-api.js';
16-
import { DwnInterfaceName, DwnMethodName } from '@tbd54566975/dwn-sdk-js';
17-
import { DwnInterface } from './types/dwn.js';
16+
import {
17+
DwnInterfaceName,
18+
DwnMethodName,
19+
type PermissionScope,
20+
type RecordsWriteMessage,
21+
} from '@tbd54566975/dwn-sdk-js';
22+
import { DwnInterface, DwnProtocolDefinition } from './types/dwn.js';
23+
import { AgentPermissionsApi } from './permissions-api.js';
1824

1925
/**
2026
* Sent to an OIDC server to authorize a client. Allows clients
@@ -157,10 +163,14 @@ export type SIOPv2AuthResponse = {
157163

158164
/** An auth response that is compatible with both Web5 Connect and (hopefully, WIP) OIDC SIOPv2 */
159165
export type Web5ConnectAuthResponse = {
160-
delegateGrants: any[];
161-
delegateDid: PortableDid;
166+
delegateGrants: DelegateGrant[];
167+
delegatePortableDid: PortableDid;
162168
} & SIOPv2AuthResponse;
163169

170+
export type DelegateGrant = (RecordsWriteMessage & {
171+
encodedData: string;
172+
})
173+
164174
/** Represents the different OIDC endpoint types.
165175
* 1. `pushedAuthorizationRequest`: client sends {@link PushedAuthRequest} receives {@link PushedAuthResponse}
166176
* 2. `authorize`: provider gets the {@link Web5ConnectAuthRequest} JWT that was stored by the PAR
@@ -240,10 +250,7 @@ async function generateCodeChallenge() {
240250
async function createAuthRequest(
241251
options: RequireOnly<
242252
Web5ConnectAuthRequest,
243-
| 'client_id'
244-
| 'scope'
245-
| 'redirect_uri'
246-
| 'permissionRequests'
253+
'client_id' | 'scope' | 'redirect_uri' | 'permissionRequests'
247254
>
248255
) {
249256
// Generate a random state value to associate the authorization request with the response.
@@ -306,7 +313,7 @@ async function encryptAuthRequest({
306313
async function createResponseObject(
307314
options: RequireOnly<
308315
Web5ConnectAuthResponse,
309-
'iss' | 'sub' | 'aud' | 'delegateGrants' | 'delegateDid'
316+
'iss' | 'sub' | 'aud' | 'delegateGrants' | 'delegatePortableDid'
310317
>
311318
) {
312319
const currentTimeInSeconds = Math.floor(Date.now() / 1000);
@@ -476,12 +483,12 @@ async function decryptAuthResponse(
476483

477484
// get the delegatedid public key from the header
478485
const header = Convert.base64Url(protectedHeaderB64U).toObject() as Jwk;
479-
const delegateDid = await DidJwk.resolve(header.kid!.split('#')[0]);
486+
const delegateResolvedDid = await DidJwk.resolve(header.kid!.split('#')[0]);
480487

481488
// derive ECDH shared key using the provider's public key and our clientDid private key
482489
const sharedKey = await Oidc.deriveSharedKey(
483490
clientDid,
484-
delegateDid.didDocument!
491+
delegateResolvedDid.didDocument!
485492
);
486493

487494
// add the pin to the AAD
@@ -606,39 +613,117 @@ function encryptAuthResponse({
606613
* Creates the permission grants that assign to the selectedDid the level of
607614
* permissions that the web app requested in the {@link Web5ConnectAuthRequest}
608615
*/
609-
export async function createPermissionGrants(
616+
async function createPermissionGrants(
610617
selectedDid: string,
611-
delegateDid: BearerDid,
612-
dwn: AgentDwnApi
618+
delegateBearerDid: BearerDid,
619+
dwn: AgentDwnApi,
620+
permissionsApi: AgentPermissionsApi,
621+
scopes: PermissionScope[],
622+
protocolUri: string
613623
) {
614-
// TODO: remove mock after adding functionality: https://github.com/TBD54566975/web5-js/issues/827
615-
const permissionRequestData = {
616-
description:
617-
'The app is asking to Records Write to http://profile-protocol.xyz',
618-
scope: {
619-
interface : DwnInterfaceName.Records,
620-
method : DwnMethodName.Write,
621-
protocol : 'http://profile-protocol.xyz',
622-
},
623-
};
624+
const permissionGrants = await Promise.all(
625+
scopes.map((scope) =>
626+
permissionsApi.createGrant({
627+
grantedTo : delegateBearerDid.uri,
628+
scope,
629+
dateExpires : '2040-06-25T16:09:16.693356Z',
630+
author : selectedDid,
631+
})
632+
)
633+
);
634+
635+
// Grant Messages Query and Messages Read for sync to work
636+
permissionGrants.push(
637+
await permissionsApi.createGrant({
638+
grantedTo : delegateBearerDid.uri,
639+
scope : {
640+
interface : DwnInterfaceName.Messages,
641+
method : DwnMethodName.Query,
642+
protocol : protocolUri,
643+
},
644+
dateExpires : '2040-06-25T16:09:16.693356Z',
645+
author : selectedDid,
646+
})
647+
);
648+
permissionGrants.push(
649+
await permissionsApi.createGrant({
650+
grantedTo : delegateBearerDid.uri,
651+
scope : {
652+
interface : DwnInterfaceName.Messages,
653+
method : DwnMethodName.Read,
654+
protocol : protocolUri,
655+
},
656+
dateExpires : '2040-06-25T16:09:16.693356Z',
657+
author : selectedDid,
658+
})
659+
);
660+
661+
const messagePromises = permissionGrants.map(async (grant) => {
662+
// Quirk: we have to pull out encodedData out of the message the schema validator doesnt want it there
663+
const { encodedData, ...rawMessage } = grant.message;
664+
665+
const data = Convert.base64Url(encodedData).toUint8Array();
666+
const params = {
667+
author : selectedDid,
668+
target : selectedDid,
669+
messageType : DwnInterface.RecordsWrite,
670+
dataStream : new Blob([data]),
671+
rawMessage,
672+
};
673+
674+
const message = await dwn.processRequest(params);
675+
const sent = await dwn.sendRequest(params);
676+
677+
// TODO: cleanup all grants if one fails by deleting them from the DWN: https://github.com/TBD54566975/web5-js/issues/849
678+
if (message.reply.status.code !== 202) {
679+
throw new Error(
680+
`Could not process the message. Error details: ${message.reply.status.detail}`
681+
);
682+
}
683+
if (sent.reply.status.code !== 202) {
684+
throw new Error(
685+
`Could not send the message. Error details: ${message.reply.status.detail}`
686+
);
687+
}
688+
689+
return grant.message;
690+
});
691+
692+
const messages = await Promise.all(messagePromises);
624693

625-
// TODO: remove mock after adding functionality: https://github.com/TBD54566975/web5-js/issues/827
626-
const message = await dwn.processRequest({
694+
return messages;
695+
}
696+
697+
/**
698+
* Installs the protocols required by the Client on the Provider
699+
* if they don't already exist.
700+
*/
701+
async function prepareProtocols(
702+
selectedDid: string,
703+
agentDwnApi: AgentDwnApi,
704+
protocolDefinition: DwnProtocolDefinition
705+
) {
706+
const queryMessage = await agentDwnApi.processRequest({
627707
author : selectedDid,
708+
messageType : DwnInterface.ProtocolsQuery,
628709
target : selectedDid,
629-
messageType : DwnInterface.RecordsWrite,
630-
messageParams : {
631-
recipient : delegateDid.uri,
632-
protocolPath : 'grant',
633-
protocol : ' https://tbd.website/dwn/permissions',
634-
dataFormat : 'application/json',
635-
data : Convert.object(permissionRequestData).toUint8Array(),
636-
},
637-
// todo: is it data or datastream?
638-
// dataStream: await Convert.object(permissionRequestData).toBlobAsync(),
710+
messageParams : { filter: { protocol: protocolDefinition.protocol } },
639711
});
640712

641-
return [message];
713+
if (queryMessage.reply.status.code === 404) {
714+
const configureMessage = await agentDwnApi.processRequest({
715+
author : selectedDid,
716+
messageType : DwnInterface.ProtocolsConfigure,
717+
target : selectedDid,
718+
messageParams : { definition: protocolDefinition },
719+
});
720+
721+
if (configureMessage.reply.status.code !== 202) {
722+
throw new Error(`Could not install protocol: ${configureMessage.reply.status.detail}`);
723+
}
724+
} else if (queryMessage.reply.status.code !== 200) {
725+
throw new Error(`Could not fetch protcol: ${queryMessage.reply.status.detail}`);
726+
}
642727
}
643728

644729
/**
@@ -654,46 +739,59 @@ async function submitAuthResponse(
654739
selectedDid: string,
655740
authRequest: Web5ConnectAuthRequest,
656741
randomPin: string,
657-
dwn: AgentDwnApi
742+
agentDwnApi: AgentDwnApi,
743+
agentPermissionsApi: AgentPermissionsApi
658744
) {
659-
const delegateDid = await DidJwk.create();
660-
const delegateDidPortable = await delegateDid.export();
745+
const delegateBearerDid = await DidJwk.create();
746+
const delegatePortableDid = await delegateBearerDid.export();
747+
748+
const delegateGrantPromises = authRequest.permissionRequests.map(async (permissionRequest) => {
749+
await prepareProtocols(selectedDid, agentDwnApi, permissionRequest.protocolDefinition);
750+
751+
// TODO: validate to make sure the scopes and definition are assigned to the same protocol
752+
const permissionGrants = await Oidc.createPermissionGrants(
753+
selectedDid,
754+
delegateBearerDid,
755+
agentDwnApi,
756+
agentPermissionsApi,
757+
permissionRequest.permissionScopes,
758+
permissionRequest.protocolDefinition.protocol
759+
);
661760

662-
const permissionGrants = await Oidc.createPermissionGrants(
663-
selectedDid,
664-
delegateDid,
665-
dwn
666-
);
761+
return permissionGrants;
762+
});
763+
764+
const delegateGrants = (await Promise.all(delegateGrantPromises)).flat();
667765

668766
const responseObject = await Oidc.createResponseObject({
669767
//* the IDP's did that was selected to be connected
670-
iss : selectedDid,
768+
iss : selectedDid,
671769
//* the client's new identity
672-
sub : delegateDid.uri,
770+
sub : delegateBearerDid.uri,
673771
//* the client's temporary ephemeral did used for connect
674-
aud : authRequest.client_id,
772+
aud : authRequest.client_id,
675773
//* the nonce of the original auth request
676-
nonce : authRequest.nonce,
677-
delegateGrants : permissionGrants,
678-
delegateDid : delegateDidPortable,
774+
nonce : authRequest.nonce,
775+
delegateGrants,
776+
delegatePortableDid,
679777
});
680778

681779
// Sign the Response Object using the ephemeral DID's signing key.
682780
const responseObjectJwt = await Oidc.signJwt({
683-
did : delegateDid,
781+
did : delegateBearerDid,
684782
data : responseObject,
685783
});
686784
const clientDid = await DidJwk.resolve(authRequest.client_id);
687785

688786
const sharedKey = await Oidc.deriveSharedKey(
689-
delegateDid,
787+
delegateBearerDid,
690788
clientDid?.didDocument!
691789
);
692790

693791
const encryptedResponse = Oidc.encryptAuthResponse({
694792
jwt : responseObjectJwt!,
695793
encryptionKey : sharedKey,
696-
delegateDidKeyId : delegateDid.document.verificationMethod![0].id,
794+
delegateDidKeyId : delegateBearerDid.document.verificationMethod![0].id,
697795
randomPin,
698796
});
699797

0 commit comments

Comments
 (0)