Skip to content

Commit

Permalink
connect dwn integration (#850)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
shamilovtim committed Aug 26, 2024
1 parent 2f7bbbe commit fea0535
Show file tree
Hide file tree
Showing 11 changed files with 4,410 additions and 8,007 deletions.
5 changes: 5 additions & 0 deletions .changeset/spicy-forks-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@web5/agent": patch
---

integrate dwn grants into connect flow
5 changes: 5 additions & 0 deletions .changeset/tidy-ants-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@web5/api": minor
---

connect methods now work with dwn and user agent and are no longer stubbed
9 changes: 8 additions & 1 deletion examples/wallet-connect.html
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ <h1>Success</h1>
method: "Query",
protocol: "http://profile-protocol.xyz",
},
{
interface: "Records",
method: "Read",
protocol: "http://profile-protocol.xyz",
},
];

try {
Expand Down Expand Up @@ -206,7 +211,9 @@ <h1>Success</h1>
}

function goToEndScreen(delegateDid) {
document.getElementById("didInformation").innerText = `${JSON.stringify(
document.getElementById(
"didInformation"
).innerText = `delegateDid\n:${JSON.stringify(
delegateDid
)}`;

Expand Down
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"build": "pnpm --recursive --stream build",
"test:node": "pnpm --recursive test:node",
"audit-ci": "audit-ci --config ./audit-ci.json",
"wallet:connect:example": "npx http-server & HTTP_SERVER_PID=$! && sleep 2 && open 'http://localhost:8080/examples/wallet-connect.html' && wait $HTTP_SERVER_PID"
"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"
},
"repository": {
"type": "git",
Expand All @@ -43,7 +43,11 @@
"ws@<8.17.1": ">=8.17.1",
"braces@<3.0.3": ">=3.0.3",
"fast-xml-parser@<4.4.1": ">=4.4.1",
"@75lb/deep-merge@<1.1.2": ">=1.1.2"
"@75lb/deep-merge@<1.1.2": ">=1.1.2",
"elliptic@>=4.0.0 <=6.5.6": ">=6.5.7",
"elliptic@>=2.0.0 <=6.5.6": ">=6.5.7",
"elliptic@>=5.2.1 <=6.5.6": ">=6.5.7",
"micromatch@<4.0.8": ">=4.0.8"
}
}
}
11 changes: 7 additions & 4 deletions packages/agent/src/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ async function initClient({
// a route to its web5 connect provider flow and the params of where to fetch the auth request.
const generatedWalletUri = new URL(walletUri);
generatedWalletUri.searchParams.set('request_uri', parData.request_uri);
generatedWalletUri.searchParams.set('encryption_key', Convert.uint8Array(encryptionKey).toBase64Url());
generatedWalletUri.searchParams.set(
'encryption_key',
Convert.uint8Array(encryptionKey).toBase64Url()
);

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

return {
delegateGrants : verifiedAuthResponse.delegateGrants,
delegateDid : verifiedAuthResponse.delegateDid,
connectedDid : verifiedAuthResponse.iss,
delegateGrants : verifiedAuthResponse.delegateGrants,
delegatePortableDid : verifiedAuthResponse.delegatePortableDid,
connectedDid : verifiedAuthResponse.iss,
};
}
}
Expand Down
206 changes: 152 additions & 54 deletions packages/agent/src/oidc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@ import { xchacha20poly1305 } from '@noble/ciphers/chacha';
import type { ConnectPermissionRequest } from './connect.js';
import { DidDocument, DidJwk, PortableDid, type BearerDid } from '@web5/dids';
import { AgentDwnApi } from './dwn-api.js';
import { DwnInterfaceName, DwnMethodName } from '@tbd54566975/dwn-sdk-js';
import { DwnInterface } from './types/dwn.js';
import {
DwnInterfaceName,
DwnMethodName,
type PermissionScope,
type RecordsWriteMessage,
} from '@tbd54566975/dwn-sdk-js';
import { DwnInterface, DwnProtocolDefinition } from './types/dwn.js';
import { AgentPermissionsApi } from './permissions-api.js';

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

/** An auth response that is compatible with both Web5 Connect and (hopefully, WIP) OIDC SIOPv2 */
export type Web5ConnectAuthResponse = {
delegateGrants: any[];
delegateDid: PortableDid;
delegateGrants: DelegateGrant[];
delegatePortableDid: PortableDid;
} & SIOPv2AuthResponse;

export type DelegateGrant = (RecordsWriteMessage & {
encodedData: string;
})

/** Represents the different OIDC endpoint types.
* 1. `pushedAuthorizationRequest`: client sends {@link PushedAuthRequest} receives {@link PushedAuthResponse}
* 2. `authorize`: provider gets the {@link Web5ConnectAuthRequest} JWT that was stored by the PAR
Expand Down Expand Up @@ -240,10 +250,7 @@ async function generateCodeChallenge() {
async function createAuthRequest(
options: RequireOnly<
Web5ConnectAuthRequest,
| 'client_id'
| 'scope'
| 'redirect_uri'
| 'permissionRequests'
'client_id' | 'scope' | 'redirect_uri' | 'permissionRequests'
>
) {
// Generate a random state value to associate the authorization request with the response.
Expand Down Expand Up @@ -306,7 +313,7 @@ async function encryptAuthRequest({
async function createResponseObject(
options: RequireOnly<
Web5ConnectAuthResponse,
'iss' | 'sub' | 'aud' | 'delegateGrants' | 'delegateDid'
'iss' | 'sub' | 'aud' | 'delegateGrants' | 'delegatePortableDid'
>
) {
const currentTimeInSeconds = Math.floor(Date.now() / 1000);
Expand Down Expand Up @@ -476,12 +483,12 @@ async function decryptAuthResponse(

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

// derive ECDH shared key using the provider's public key and our clientDid private key
const sharedKey = await Oidc.deriveSharedKey(
clientDid,
delegateDid.didDocument!
delegateResolvedDid.didDocument!
);

// add the pin to the AAD
Expand Down Expand Up @@ -606,39 +613,117 @@ function encryptAuthResponse({
* Creates the permission grants that assign to the selectedDid the level of
* permissions that the web app requested in the {@link Web5ConnectAuthRequest}
*/
export async function createPermissionGrants(
async function createPermissionGrants(
selectedDid: string,
delegateDid: BearerDid,
dwn: AgentDwnApi
delegateBearerDid: BearerDid,
dwn: AgentDwnApi,
permissionsApi: AgentPermissionsApi,
scopes: PermissionScope[],
protocolUri: string
) {
// TODO: remove mock after adding functionality: https://github.com/TBD54566975/web5-js/issues/827
const permissionRequestData = {
description:
'The app is asking to Records Write to http://profile-protocol.xyz',
scope: {
interface : DwnInterfaceName.Records,
method : DwnMethodName.Write,
protocol : 'http://profile-protocol.xyz',
},
};
const permissionGrants = await Promise.all(
scopes.map((scope) =>
permissionsApi.createGrant({
grantedTo : delegateBearerDid.uri,
scope,
dateExpires : '2040-06-25T16:09:16.693356Z',
author : selectedDid,
})
)
);

// Grant Messages Query and Messages Read for sync to work
permissionGrants.push(
await permissionsApi.createGrant({
grantedTo : delegateBearerDid.uri,
scope : {
interface : DwnInterfaceName.Messages,
method : DwnMethodName.Query,
protocol : protocolUri,
},
dateExpires : '2040-06-25T16:09:16.693356Z',
author : selectedDid,
})
);
permissionGrants.push(
await permissionsApi.createGrant({
grantedTo : delegateBearerDid.uri,
scope : {
interface : DwnInterfaceName.Messages,
method : DwnMethodName.Read,
protocol : protocolUri,
},
dateExpires : '2040-06-25T16:09:16.693356Z',
author : selectedDid,
})
);

const messagePromises = permissionGrants.map(async (grant) => {
// Quirk: we have to pull out encodedData out of the message the schema validator doesnt want it there
const { encodedData, ...rawMessage } = grant.message;

const data = Convert.base64Url(encodedData).toUint8Array();
const params = {
author : selectedDid,
target : selectedDid,
messageType : DwnInterface.RecordsWrite,
dataStream : new Blob([data]),
rawMessage,
};

const message = await dwn.processRequest(params);
const sent = await dwn.sendRequest(params);

// TODO: cleanup all grants if one fails by deleting them from the DWN: https://github.com/TBD54566975/web5-js/issues/849
if (message.reply.status.code !== 202) {
throw new Error(
`Could not process the message. Error details: ${message.reply.status.detail}`
);
}
if (sent.reply.status.code !== 202) {
throw new Error(
`Could not send the message. Error details: ${message.reply.status.detail}`
);
}

return grant.message;
});

const messages = await Promise.all(messagePromises);

// TODO: remove mock after adding functionality: https://github.com/TBD54566975/web5-js/issues/827
const message = await dwn.processRequest({
return messages;
}

/**
* Installs the protocols required by the Client on the Provider
* if they don't already exist.
*/
async function prepareProtocols(
selectedDid: string,
agentDwnApi: AgentDwnApi,
protocolDefinition: DwnProtocolDefinition
) {
const queryMessage = await agentDwnApi.processRequest({
author : selectedDid,
messageType : DwnInterface.ProtocolsQuery,
target : selectedDid,
messageType : DwnInterface.RecordsWrite,
messageParams : {
recipient : delegateDid.uri,
protocolPath : 'grant',
protocol : ' https://tbd.website/dwn/permissions',
dataFormat : 'application/json',
data : Convert.object(permissionRequestData).toUint8Array(),
},
// todo: is it data or datastream?
// dataStream: await Convert.object(permissionRequestData).toBlobAsync(),
messageParams : { filter: { protocol: protocolDefinition.protocol } },
});

return [message];
if (queryMessage.reply.status.code === 404) {
const configureMessage = await agentDwnApi.processRequest({
author : selectedDid,
messageType : DwnInterface.ProtocolsConfigure,
target : selectedDid,
messageParams : { definition: protocolDefinition },
});

if (configureMessage.reply.status.code !== 202) {
throw new Error(`Could not install protocol: ${configureMessage.reply.status.detail}`);
}
} else if (queryMessage.reply.status.code !== 200) {
throw new Error(`Could not fetch protcol: ${queryMessage.reply.status.detail}`);
}
}

/**
Expand All @@ -654,46 +739,59 @@ async function submitAuthResponse(
selectedDid: string,
authRequest: Web5ConnectAuthRequest,
randomPin: string,
dwn: AgentDwnApi
agentDwnApi: AgentDwnApi,
agentPermissionsApi: AgentPermissionsApi
) {
const delegateDid = await DidJwk.create();
const delegateDidPortable = await delegateDid.export();
const delegateBearerDid = await DidJwk.create();
const delegatePortableDid = await delegateBearerDid.export();

const delegateGrantPromises = authRequest.permissionRequests.map(async (permissionRequest) => {
await prepareProtocols(selectedDid, agentDwnApi, permissionRequest.protocolDefinition);

// TODO: validate to make sure the scopes and definition are assigned to the same protocol
const permissionGrants = await Oidc.createPermissionGrants(
selectedDid,
delegateBearerDid,
agentDwnApi,
agentPermissionsApi,
permissionRequest.permissionScopes,
permissionRequest.protocolDefinition.protocol
);

const permissionGrants = await Oidc.createPermissionGrants(
selectedDid,
delegateDid,
dwn
);
return permissionGrants;
});

const delegateGrants = (await Promise.all(delegateGrantPromises)).flat();

const responseObject = await Oidc.createResponseObject({
//* the IDP's did that was selected to be connected
iss : selectedDid,
iss : selectedDid,
//* the client's new identity
sub : delegateDid.uri,
sub : delegateBearerDid.uri,
//* the client's temporary ephemeral did used for connect
aud : authRequest.client_id,
aud : authRequest.client_id,
//* the nonce of the original auth request
nonce : authRequest.nonce,
delegateGrants : permissionGrants,
delegateDid : delegateDidPortable,
nonce : authRequest.nonce,
delegateGrants,
delegatePortableDid,
});

// Sign the Response Object using the ephemeral DID's signing key.
const responseObjectJwt = await Oidc.signJwt({
did : delegateDid,
did : delegateBearerDid,
data : responseObject,
});
const clientDid = await DidJwk.resolve(authRequest.client_id);

const sharedKey = await Oidc.deriveSharedKey(
delegateDid,
delegateBearerDid,
clientDid?.didDocument!
);

const encryptedResponse = Oidc.encryptAuthResponse({
jwt : responseObjectJwt!,
encryptionKey : sharedKey,
delegateDidKeyId : delegateDid.document.verificationMethod![0].id,
delegateDidKeyId : delegateBearerDid.document.verificationMethod![0].id,
randomPin,
});

Expand Down
Loading

0 comments on commit fea0535

Please sign in to comment.