Skip to content

Commit

Permalink
Update connect flow (#856)
Browse files Browse the repository at this point in the history
There were a few different issues with our sync implementation.

1) Records permissions were not issued as `delegate` grants.  In the future the OIDC flow can accept actual `PermissionRequests` that also signal if the user wants a delegate grant or not. Currently the system is opinionated in that `Records` grants MUST be delegated, and `Messages` grants must not.

2) There are a few different scenarios the user can land at with regards to configuring protocols. Handle the various scenarios and make sure the remote node also has the configured protocol available as the connecting user will need it immediately to use.

Added method to fail if a scope requests a different protocol than expected.

Increase test coverage.
  • Loading branch information
LiranCohen authored Aug 29, 2024
1 parent d5acb56 commit e3408c7
Show file tree
Hide file tree
Showing 5 changed files with 420 additions and 53 deletions.
38 changes: 32 additions & 6 deletions examples/wallet-connect.html
Original file line number Diff line number Diff line change
Expand Up @@ -121,19 +121,34 @@ <h1>Success</h1>
$actions: [
{
who: "anyone",
can: ["create", "update"],
can: ["read"],
},
],
},
},
};

const fooProtocol = {
protocol: "http://foo-protocol.xyz",
published: true,
types: {
foo: {
schema: "http://foo-protocol.xyz/schema/foo",
dataFormats: ["application/json"],
},
},
structure: {
foo: {},
},
};

try {
const { delegateDid } = await Web5.connect({

const { delegateDid, web5 } = await Web5.connect({
walletConnectOptions: {
walletUri: "web5://connect",
connectServerUrl: "http://localhost:3000/connect",
permissionRequests: [{ protocolDefinition: profileProtocol }],
permissionRequests: [{ protocolDefinition: profileProtocol }, { protocolDefinition: fooProtocol }],
onWalletUriReady: generateQRCode,
validatePin: async () => {
goToPinScreen();
Expand All @@ -144,7 +159,18 @@ <h1>Success</h1>
},
});

goToEndScreen(delegateDid);
// attempt to write to the foo protocol
const { record, status } = await web5.dwn.records.create({
data: { fooData: 'Some Foo Data' },
message: {
protocol: fooProtocol.protocol,
protocolPath: 'foo',
schema: fooProtocol.types.foo.schema,
dataFormat: fooProtocol.types.foo.dataFormats[0],
}
});

goToEndScreen(delegateDid, record, status);
} catch (e) {
document.getElementById(
"errorMessage"
Expand Down Expand Up @@ -187,12 +213,12 @@ <h1>Success</h1>
document.getElementById("pinScreen").style.display = "block";
}

function goToEndScreen(delegateDid) {
function goToEndScreen(delegateDid, record, status) {
document.getElementById(
"didInformation"
).innerText = `delegateDid\n:${JSON.stringify(
delegateDid
)}`;
)}\n\n\nRecordsWrite Status:${JSON.stringify(status)}\nRecord:${JSON.stringify(record, null, 2)}`;

document.getElementById("pinScreen").style.display = "none";
document.getElementById("endScreen").style.display = "block";
Expand Down
2 changes: 1 addition & 1 deletion packages/agent/src/connect.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import type { PushedAuthResponse } from './oidc.js';
import type { DwnPermissionScope, DwnProtocolDefinition, Web5ConnectAuthResponse } from './index.js';
import type { DwnPermissionScope, DwnProtocolDefinition, Web5Agent, Web5ConnectAuthResponse } from './index.js';

import {
Oidc,
Expand Down
109 changes: 68 additions & 41 deletions packages/agent/src/oidc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,10 @@ import { concatenateUrl } from './utils.js';
import { xchacha20poly1305 } from '@noble/ciphers/chacha';
import type { ConnectPermissionRequest } from './connect.js';
import { DidDocument, DidJwk, PortableDid, type BearerDid } from '@web5/dids';
import type {
PermissionScope,
RecordsWriteMessage,
} from '@tbd54566975/dwn-sdk-js';
import { DwnInterface, DwnProtocolDefinition } from './types/dwn.js';
import { DwnDataEncodedRecordsWriteMessage, DwnInterface, DwnPermissionScope, DwnProtocolDefinition } from './types/dwn.js';
import { AgentPermissionsApi } from './permissions-api.js';
import type { Web5Agent } from './types/agent.js';
import { isRecordPermissionScope } from './dwn-api.js';

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

/** An auth response that is compatible with both Web5 Connect and (hopefully, WIP) OIDC SIOPv2 */
export type Web5ConnectAuthResponse = {
delegateGrants: DelegateGrant[];
delegateGrants: DwnDataEncodedRecordsWriteMessage[];
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 @@ -615,30 +608,34 @@ async function createPermissionGrants(
selectedDid: string,
delegateBearerDid: BearerDid,
agent: Web5Agent,
scopes: PermissionScope[],
scopes: DwnPermissionScope[],
) {

const permissionsApi = new AgentPermissionsApi({ agent });

// TODO: cleanup all grants if one fails by deleting them from the DWN: https://github.com/TBD54566975/web5-js/issues/849
const permissionGrants = await Promise.all(
scopes.map((scope) =>
permissionsApi.createGrant({
scopes.map((scope) => {

// check if the scope is a records permission scope, if so it is a delegated permission
const delegated = isRecordPermissionScope(scope);
return permissionsApi.createGrant({
delegated,
store : true,
grantedTo : delegateBearerDid.uri,
scope,
dateExpires : '2040-06-25T16:09:16.693356Z',
dateExpires : '2040-06-25T16:09:16.693356Z', // TODO: make dateExpires optional
author : selectedDid,
})
)
});

})
);

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

const data = Convert.base64Url(encodedData).toUint8Array();
const { reply } = await agent.sendDwnRequest({
const { reply } = await agent.sendDwnRequest({
author : selectedDid,
target : selectedDid,
messageType : DwnInterface.RecordsWrite,
Expand All @@ -662,47 +659,63 @@ async function createPermissionGrants(
}

/**
* Installs the protocols required by the Client on the Provider
* if they don't already exist.
*/
async function prepareProtocols(
* Installs the protocol required by the Client on the Provider if it doesn't already exist.
*/
async function prepareProtocol(
selectedDid: string,
agent: Web5Agent,
protocolDefinition: DwnProtocolDefinition
) {
): Promise<void> {

const queryMessage = await agent.processDwnRequest({
author : selectedDid,
messageType : DwnInterface.ProtocolsQuery,
target : selectedDid,
messageParams : { filter: { protocol: protocolDefinition.protocol } },
});

if (queryMessage.reply.status.code === 404) {
const configureMessage = await agent.processDwnRequest({
if ( queryMessage.reply.status.code !== 200) {
// if the query failed, throw an error
throw new Error(
`Could not fetch protocol: ${queryMessage.reply.status.detail}`
);
} else if (queryMessage.reply.entries === undefined || queryMessage.reply.entries.length === 0) {

// send the protocol definition to the remote DWN first, if it passes we can process it locally
const { reply: sendReply, message: configureMessage } = await agent.sendDwnRequest({
author : selectedDid,
messageType : DwnInterface.ProtocolsConfigure,
target : selectedDid,
messageType : DwnInterface.ProtocolsConfigure,
messageParams : { definition: protocolDefinition },
});

if (configureMessage.reply.status.code !== 202) {
throw new Error(`Could not install protocol: ${configureMessage.reply.status.detail}`);
// check if the message was sent successfully, if the remote returns 409 the message may have come through already via sync
if (sendReply.status.code !== 202 && sendReply.status.code !== 409) {
throw new Error(`Could not send protocol: ${sendReply.status.detail}`);
}

// send the configure message to the remote DWN so that the APP can immediately use it without waiting for a sync cycle from the wallet
// process the protocol locally, we don't have to check if it exists as this is just a convenience over waiting for sync.
await agent.processDwnRequest({
author : selectedDid,
target : selectedDid,
messageType : DwnInterface.ProtocolsConfigure,
rawMessage : configureMessage
});

} else {

// the protocol already exists, let's make sure it exists on the remote DWN as the requesting app will need it
const configureMessage = queryMessage.reply.entries![0];
const { reply: sendReply } = await agent.sendDwnRequest({
author : selectedDid,
target : selectedDid,
messageType : DwnInterface.ProtocolsConfigure,
rawMessage : configureMessage.message,
rawMessage : configureMessage,
});

// check if the message was sent successfully, if the remote returns 409 the message may have come through already via sync
if (sendReply.status.code !== 202 && sendReply.status.code !== 409) {
throw new Error(`Could not send protocol: ${sendReply.status.detail}`);
}
} else if (queryMessage.reply.status.code !== 200) {
throw new Error(`Could not fetch protcol: ${queryMessage.reply.status.detail}`);
}
}

Expand All @@ -719,20 +732,34 @@ async function submitAuthResponse(
selectedDid: string,
authRequest: Web5ConnectAuthRequest,
randomPin: string,
agent: Web5Agent,
agent: Web5Agent
) {
const delegateBearerDid = await DidJwk.create();
const delegatePortableDid = await delegateBearerDid.export();

const delegateGrantPromises = authRequest.permissionRequests.map(async (permissionRequest) => {
// TODO: validate to make sure the scopes and definition are assigned to the same protocol
const { protocolDefinition, permissionScopes } = permissionRequest;
// 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) => {
const { protocolDefinition, permissionScopes } = permissionRequest;

await prepareProtocols(selectedDid, agent, protocolDefinition);
const permissionGrants = await Oidc.createPermissionGrants(selectedDid, delegateBearerDid, agent, permissionScopes);
// 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.');
}

return permissionGrants;
});
await prepareProtocol(selectedDid, agent, protocolDefinition);

const permissionGrants = await Oidc.createPermissionGrants(
selectedDid,
delegateBearerDid,
agent,
permissionScopes
);

return permissionGrants;
}
);

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

Expand Down
Loading

0 comments on commit e3408c7

Please sign in to comment.