From c36b57891489c10b198273c3c4baf36aaad105be Mon Sep 17 00:00:00 2001 From: Tim Shamilov Date: Wed, 23 Oct 2024 14:29:59 -0400 Subject: [PATCH 01/11] add prerequisites for expansion of connect flow --- packages/agent/src/connect.ts | 267 ++++++++++++++++----------- packages/agent/src/oidc.ts | 245 ++++++++++++++---------- packages/agent/tests/connect.spec.ts | 102 ++++++---- packages/api/src/web5.ts | 94 ++++------ packages/api/tests/web5.spec.ts | 24 +-- 5 files changed, 416 insertions(+), 316 deletions(-) diff --git a/packages/agent/src/connect.ts b/packages/agent/src/connect.ts index fb19b9983..38d850226 100644 --- a/packages/agent/src/connect.ts +++ b/packages/agent/src/connect.ts @@ -1,10 +1,11 @@ - import type { PushedAuthResponse } from './oidc.js'; -import type { DwnPermissionScope, DwnProtocolDefinition, Web5Agent, Web5ConnectAuthResponse } from './index.js'; +import type { + DwnPermissionScope, + DwnProtocolDefinition, + Web5ConnectAuthResponse, +} from './index.js'; -import { - Oidc, -} from './oidc.js'; +import { Oidc } from './oidc.js'; import { pollWithTtl } from './utils.js'; import { Convert, logger } from '@web5/common'; @@ -13,8 +14,95 @@ import { DidJwk } from '@web5/dids'; import { DwnInterfaceName, DwnMethodName } from '@tbd54566975/dwn-sdk-js'; /** - * Initiates the wallet connect process. Used when a client wants to obtain - * a did from a provider. + * Settings provided by users who wish to allow their DWA to connect to a wallet + * and either transfer their DID to that wallet (when `exported: true`) + * or transfer a DID from their wallet (without `exported: true`). + */ +export type WalletConnectOptions = { + /** The user friendly name of the app to be displayed when prompting end-user with permission requests. */ + displayName: string; + + /** The URL of the intermediary server which relays messages between the client and provider */ + connectServerUrl: string; + + /** + * The URI of the Provider (wallet).The `onWalletUriReady` will take this wallet + * uri and add a payload to it which will be used to obtain and decrypt from the `request_uri`. + * @example `web5://` or `http://localhost:3000/`. + */ + walletUri: string; + + /** + * The protocols of permissions requested, along with the definition and + * permission scopes for each protocol. + * If `exported` is true these will be created automatically. + */ + permissionRequests: ConnectUserPermissionRequest[]; + + /** + * Can be set to true if the DWA wants to transfer its identity to the wallet + * instead of get an identity from the wallet + */ + exported?: boolean; + + /** + * The Web5 API provides a URI to the wallet based on the `walletUri` plus a query params payload valid for 5 minutes. + * The link can either be used as a deep link on the same device or a QR code for cross device or both. + * The query params are `{ request_uri: string; encryption_key: string; }` + * The wallet will use the `request_uri to contact the intermediary server's `authorize` endpoint + * and pull down the {@link Web5ConnectAuthRequest} and use the `encryption_key` to decrypt it. + * + * @param uri - The URI returned by the web5 connect API to be passed to a provider. + */ + onWalletUriReady: (uri: string) => void; + + /** + * Function that must be provided to submit the pin entered by the user on the client. + * The pin is used to decrypt the {@link Web5ConnectAuthResponse} that was retrieved from the + * token endpoint by the client inside of web5 connect. + * + * @returns A promise that resolves to the PIN as a string. + */ + validatePin: () => Promise; +}; + +/** Used by the WalletConnect protocol to provision a Wallet for the exact permissions its needs */ +export type ConnectPermissionRequest = { + /** + * The definition of the protocol the permissions are being requested for. + * In the event that the protocol is not already installed, the wallet will install this given protocol definition. + */ + protocolDefinition: DwnProtocolDefinition; + + /** The scope of the permissions being requested for the given protocol */ + permissionScopes: DwnPermissionScope[]; +}; + +/** Convenience object passed in by users and normalized to the internally used {@link ConnectPermissionRequest} */ +export type ConnectUserPermissionRequest = Omit< + ConnectPermissionRequest, + 'permissionScopes' +> & { + /** + * Used to create a {@link DwnPermissionScope} for each option provided in this param. + * If undefined defaults to requesting all permissions. + * `configure` is not included by default, as this gives the application a lot of control over the protocol. + */ + permissions?: Permission[]; +}; + +/** Shorthand for the types of permissions that can be requested. */ +type Permission = + | 'write' + | 'read' + | 'delete' + | 'query' + | 'subscribe' + | 'configure'; + +/** + * Called by the DWA. In this workflow the wallet provisions a DID to the DWA. + * The DWA will have access to the data of the DID and be able to act as that DID. */ async function initClient({ displayName, @@ -24,9 +112,25 @@ async function initClient({ onWalletUriReady, validatePin, }: WalletConnectOptions) { - // ephemeral client did for ECDH, signing, verification - // TODO: use separate keys for ECDH vs. sign/verify. could maybe use secp256k1. - const clientDid = await DidJwk.create(); + const normalizedPermissionRequests = permissionRequests.map( + ({ protocolDefinition, permissions }) => + WalletConnect.createPermissionRequestForProtocol({ + definition : protocolDefinition, + permissions : permissions ?? [ + 'read', + 'write', + 'delete', + 'query', + 'subscribe', + ], + }) + ); + + // ephemeral did used for signing, verification + const clientSigningDid = await DidJwk.create(); + + // ephemeral did used for ECDH only + const clientEcdhDid = await DidJwk.create(); // TODO: properly implement PKCE. this implementation is lacking server side validations and more. // https://github.com/TBD54566975/web5-js/issues/829 @@ -40,33 +144,37 @@ async function initClient({ baseURL : connectServerUrl, endpoint : 'callback', }); + console.log('after callback endpoint'); // build the PAR request const request = await Oidc.createAuthRequest({ - client_id : clientDid.uri, + client_id : clientSigningDid.uri, scope : 'openid did:jwk', redirect_uri : callbackEndpoint, - // custom properties: + client_name : displayName, // code_challenge : codeChallengeBase64Url, // code_challenge_method : 'S256', - permissionRequests : permissionRequests, - displayName, + // custom properties: + permissionRequests : normalizedPermissionRequests, }); // Sign the Request Object using the Client DID's signing key. const requestJwt = await Oidc.signJwt({ - did : clientDid, + did : clientSigningDid, data : request, }); if (!requestJwt) { throw new Error('Unable to sign requestObject'); } - // Encrypt the Request Object JWT using the code challenge. + + // Encrypt with symmetric randomBytes and tell counterparty about the future ecdh pub did kid const requestObjectJwe = await Oidc.encryptAuthRequest({ - jwt: requestJwt, + jwt : requestJwt, + kid : clientEcdhDid.document.verificationMethod![0].id, encryptionKey, }); + console.log('after requestobjecjtwe'); // Convert the encrypted Request Object to URLSearchParams for form encoding. const formEncodedRequest = new URLSearchParams({ @@ -85,6 +193,7 @@ async function initClient({ 'Content-Type': 'application/x-www-form-urlencoded', }, }); + console.log('after par'); if (!parResponse.ok) { throw new Error(`${parResponse.status}: ${parResponse.statusText}`); @@ -101,6 +210,7 @@ async function initClient({ 'encryption_key', Convert.uint8Array(encryptionKey).toBase64Url() ); + console.log('after generatedwalleturi'); // call user's callback so they can send the URI to the wallet as they see fit onWalletUriReady(generatedWalletUri.toString()); @@ -119,11 +229,13 @@ async function initClient({ // get the pin from the user and use it as AAD to decrypt const pin = await validatePin(); - const jwt = await Oidc.decryptAuthResponse(clientDid, jwe, pin); + const jwt = await Oidc.decryptWithPin(clientEcdhDid, jwe, pin); const verifiedAuthResponse = (await Oidc.verifyJwt({ jwt, })) as Web5ConnectAuthResponse; + // TODO: export insertion point + return { delegateGrants : verifiedAuthResponse.delegateGrants, delegatePortableDid : verifiedAuthResponse.delegatePortableDid, @@ -133,88 +245,20 @@ async function initClient({ } /** - * Initiates the wallet connect process. Used when a client wants to obtain - * a did from a provider. - */ -export type WalletConnectOptions = { - /** The user friendly name of the client/app to be displayed when prompting end-user with permission requests. */ - displayName: string; - - /** The URL of the intermediary server which relays messages between the client and provider. */ - connectServerUrl: string; - - /** - * The URI of the Provider (wallet).The `onWalletUriReady` will take this wallet - * uri and add a payload to it which will be used to obtain and decrypt from the `request_uri`. - * @example `web5://` or `http://localhost:3000/`. - */ - walletUri: string; - - /** - * The protocols of permissions requested, along with the definition and - * permission scopes for each protocol. The key is the protocol URL and - * the value is an object with the protocol definition and the permission scopes. - */ - permissionRequests: ConnectPermissionRequest[]; - - /** - * The Web5 API provides a URI to the wallet based on the `walletUri` plus a query params payload valid for 5 minutes. - * The link can either be used as a deep link on the same device or a QR code for cross device or both. - * The query params are `{ request_uri: string; encryption_key: string; }` - * The wallet will use the `request_uri to contact the intermediary server's `authorize` endpoint - * and pull down the {@link Web5ConnectAuthRequest} and use the `encryption_key` to decrypt it. - * - * @param uri - The URI returned by the web5 connect API to be passed to a provider. - */ - onWalletUriReady: (uri: string) => void; - - /** - * Function that must be provided to submit the pin entered by the user on the client. - * The pin is used to decrypt the {@link Web5ConnectAuthResponse} that was retrieved from the - * token endpoint by the client inside of web5 connect. - * - * @returns A promise that resolves to the PIN as a string. - */ - validatePin: () => Promise; -}; - -/** - * The protocols of permissions requested, along with the definition and permission scopes for each protocol. - */ -export type ConnectPermissionRequest = { - /** - * The definition of the protocol the permissions are being requested for. - * In the event that the protocol is not already installed, the wallet will install this given protocol definition. - */ - protocolDefinition: DwnProtocolDefinition; - - /** The scope of the permissions being requested for the given protocol */ - permissionScopes: DwnPermissionScope[]; -}; - -/** - * Shorthand for the types of permissions that can be requested. - */ -export type Permission = 'write' | 'read' | 'delete' | 'query' | 'subscribe' | 'configure'; - -/** - * The options for creating a permission request for a given protocol. + * An internal utility that simplifies the API for permission requests by allowing + * users to pass simple strings (any of {@link Permission}) and will create the + * appropriate {@link DwnPermissionScope} for each string provided. */ -export type ProtocolPermissionOptions = { +function createPermissionRequestForProtocol({ + definition, + permissions, +}: { /** The protocol definition for the protocol being requested */ definition: DwnProtocolDefinition; /** The permissions being requested for the protocol */ permissions: Permission[]; -}; - -/** - * Creates a set of Dwn Permission Scopes to request for a given protocol. - * - * If no permissions are provided, the default is to request all relevant record permissions (write, read, delete, query, subscribe). - * 'configure' is not included by default, as this gives the application a lot of control over the protocol. - */ -function createPermissionRequestForProtocol({ definition, permissions }: ProtocolPermissionOptions): ConnectPermissionRequest { +}) { const requests: DwnPermissionScope[] = []; // Add the ability to query for the specific protocol @@ -225,19 +269,23 @@ function createPermissionRequestForProtocol({ definition, permissions }: Protoco }); // In order to enable sync, we must request permissions for `MessagesQuery`, `MessagesRead` and `MessagesSubscribe` - requests.push({ - protocol : definition.protocol, - interface : DwnInterfaceName.Messages, - method : DwnMethodName.Read, - }, { - protocol : definition.protocol, - interface : DwnInterfaceName.Messages, - method : DwnMethodName.Query, - }, { - protocol : definition.protocol, - interface : DwnInterfaceName.Messages, - method : DwnMethodName.Subscribe, - }); + requests.push( + { + protocol : definition.protocol, + interface : DwnInterfaceName.Messages, + method : DwnMethodName.Read, + }, + { + protocol : definition.protocol, + interface : DwnInterfaceName.Messages, + method : DwnMethodName.Query, + }, + { + protocol : definition.protocol, + interface : DwnInterfaceName.Messages, + method : DwnMethodName.Subscribe, + } + ); // We also request any additional permissions the user has requested for this protocol for (const permission of permissions) { @@ -293,4 +341,7 @@ function createPermissionRequestForProtocol({ definition, permissions }: Protoco }; } -export const WalletConnect = { initClient, createPermissionRequestForProtocol }; +export const WalletConnect = { + initClient, + createPermissionRequestForProtocol, +}; diff --git a/packages/agent/src/oidc.ts b/packages/agent/src/oidc.ts index 076a0b72a..25e9762c9 100644 --- a/packages/agent/src/oidc.ts +++ b/packages/agent/src/oidc.ts @@ -7,12 +7,18 @@ import { Sha256, X25519, CryptoUtils, + JweHeaderParams, } from '@web5/crypto'; 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 { DwnDataEncodedRecordsWriteMessage, DwnInterface, DwnPermissionScope, DwnProtocolDefinition } from './types/dwn.js'; +import { DidDocument, DidJwk, DidResolutionResult, PortableDid, type BearerDid } from '@web5/dids'; +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'; @@ -52,6 +58,9 @@ export type SIOPv2AuthRequest = { /** The DID of the client (RP) */ client_id: string; + /** The user friendly name of the client (RP) */ + client_name?: string; + /** The scope of the access request (e.g., `openid profile`). */ scope: string; @@ -128,11 +137,10 @@ export type SIOPv2AuthRequest = { * The contents of this are inserted into a JWT inside of the {@link PushedAuthRequest}. */ export type Web5ConnectAuthRequest = { - /** The user friendly name of the client/app to be displayed when prompting end-user with permission requests. */ - displayName: string; - /** PermissionGrants that are to be sent to the provider */ - permissionRequests: ConnectPermissionRequest[]; + permissionRequests?: ConnectPermissionRequest[]; + /** Instead of receiving a DID from the wallet the DWA will export its DID to the wallet */ + exported?: boolean; } & SIOPv2AuthRequest; /** The fields for an OIDC SIOPv2 Auth Repsonse */ @@ -171,12 +179,20 @@ export type Web5ConnectAuthResponse = { * 2. `authorize`: provider gets the {@link Web5ConnectAuthRequest} JWT that was stored by the PAR * 3. `callback`: provider sends {@link Web5ConnectAuthResponse} to this endpoint * 4. `token`: client gets {@link Web5ConnectAuthResponse} from this endpoint + * 5. `export`: (if `exported` is true) client will POST a {@link PortableDid} + * 6. `retrieve`: (if `exported` is true) wallet will GET the {@link PortableDid} + * 7. `export-token`: (if `exported` is true) wallet will POST the grants in order to finalize the flow. + * 8. `retrieve-token`: (if `exported` is true) client will GET the grants in order to finalize the flow. */ type OidcEndpoint = | 'pushedAuthorizationRequest' | 'authorize' | 'callback' - | 'token'; + | 'token' + | 'export' + | 'retrieve' + | 'export-token' + | 'retrieve-token'; /** * Gets the correct OIDC endpoint out of the {@link OidcEndpoint} options provided. @@ -213,13 +229,16 @@ function buildOidcUrl({ /** 3. provider sends {@link Web5ConnectAuthResponse} */ case 'callback': return concatenateUrl(baseURL, `callback`); - /** 4. client gets {@link Web5ConnectAuthResponse */ + /** 4. client gets {@link Web5ConnectAuthResponse} */ case 'token': if (!tokenParam) throw new Error( `tokenParam must be providied when building a token URL` ); return concatenateUrl(baseURL, `token/${tokenParam}.jwt`); + /** 5. (if `exported` is true) client will POST a {@link PortableDid} for import to the wallet. */ + case 'export': + return concatenateUrl(baseURL, `export`); // TODO: metadata endpoints? default: throw new Error(`No matches for endpoint specified: ${endpoint}`); @@ -245,7 +264,7 @@ async function generateCodeChallenge() { async function createAuthRequest( options: RequireOnly< Web5ConnectAuthRequest, - 'client_id' | 'scope' | 'redirect_uri' | 'permissionRequests' | 'displayName' + 'client_id' | 'scope' | 'redirect_uri' > ) { // Generate a random state value to associate the authorization request with the response. @@ -272,15 +291,18 @@ async function createAuthRequest( async function encryptAuthRequest({ jwt, encryptionKey, + kid }: { jwt: string; encryptionKey: Uint8Array; + kid: string; }) { - const protectedHeader = { + const protectedHeader: JweHeaderParams = { alg : 'dir', cty : 'JWT', enc : 'XC20P', typ : 'JWT', + kid }; const nonce = CryptoUtils.randomBytes(24); const additionalData = Convert.object(protectedHeader).toUint8Array(); @@ -400,6 +422,7 @@ async function verifyJwt({ jwt }: { jwt: string }) { } /** + * Called by the wallet first to get the authRequest. * Fetches the {@Web5ConnectAuthRequest} from the authorize endpoint and decrypts it * using the encryption key passed via QR code. */ @@ -414,7 +437,12 @@ const getAuthRequest = async (request_uri: string, encryption_key: string) => { jwt, })) as Web5ConnectAuthRequest; - return web5ConnectAuthRequest; + // get the pub DID that represents the client in ECDH and deriving a shared key + const header = Convert.base64Url(jwe.split('.')[0]).toObject() as JweHeaderParams; + + const clientEcdhDid = await DidJwk.resolve(header.kid!.split('#')[0]); + + return { authRequest: web5ConnectAuthRequest, clientEcdhDid }; }; /** Take the encrypted JWE, decrypt using the code challenge and return a JWT string which will need to be verified */ @@ -436,6 +464,7 @@ function decryptAuthRequest({ const encryptionKeyBytes = Convert.base64Url(encryption_key).toUint8Array(); const protectedHeader = Convert.base64Url(protectedHeaderB64U).toUint8Array(); const additionalData = protectedHeader; + const additionalDataObj = Convert.base64Url(protectedHeaderB64U).toObject(); const nonce = Convert.base64Url(nonceB64U).toUint8Array(); const ciphertext = Convert.base64Url(ciphertextB64U).toUint8Array(); const authenticationTag = Convert.base64Url( @@ -457,17 +486,8 @@ function decryptAuthRequest({ /** * The client uses to decrypt the jwe obtained from the auth server which contains * the {@link Web5ConnectAuthResponse} that was sent by the provider to the auth server. - * - * @async - * @param {BearerDid} clientDid - The did that was initially used by the client for ECDH at connect init. - * @param {string} jwe - The encrypted data as a jwe. - * @param {string} pin - The pin that was obtained from the user. */ -async function decryptAuthResponse( - clientDid: BearerDid, - jwe: string, - pin: string -) { +async function decryptWithPin(clientDid: BearerDid, jwe: string, pin: string) { const [ protectedHeaderB64U, , @@ -478,12 +498,18 @@ async function decryptAuthResponse( // get the delegatedid public key from the header const header = Convert.base64Url(protectedHeaderB64U).toObject() as Jwk; - const delegateResolvedDid = await DidJwk.resolve(header.kid!.split('#')[0]); + + // get ECDH pub did kid for provider encrypted jwe + const jweProviderEcdhDidKid = await DidJwk.resolve(header.kid!.split('#')[0]); + + if (!jweProviderEcdhDidKid.didDocument) { + throw new Error('Could not resolve provider\'s didd document for shared key derivation'); + } // derive ECDH shared key using the provider's public key and our clientDid private key const sharedKey = await Oidc.deriveSharedKey( clientDid, - delegateResolvedDid.didDocument! + jweProviderEcdhDidKid.didDocument ); // add the pin to the AAD @@ -557,26 +583,30 @@ async function deriveSharedKey( /** * Encrypts the auth response jwt. Requires a randomPin is added to the AAD of the * encryption algorithm in order to prevent man in the middle and eavesdropping attacks. - * The keyid of the delegate did is used to pass the public key to the client in order + * The keyid of the encrypting did is used to pass the public key to the client in order * for the client to derive the shared ECDH private key. */ -function encryptAuthResponse({ +function encryptWithPin({ jwt, encryptionKey, - delegateDidKeyId, + pubDidKid, randomPin, }: { + /** JWT of data to encrypt */ jwt: string; + /** the ECDH did priv key */ encryptionKey: Uint8Array; - delegateDidKeyId: string; + /** the DID URI of the encrypting DID, NOT the shared key */ + pubDidKid: string; + /** cryptographically secure pin */ randomPin: string; }) { - const protectedHeader = { + const protectedHeader: JweHeaderParams = { alg : 'dir', cty : 'JWT', enc : 'XC20P', typ : 'JWT', - kid : delegateDidKeyId, + kid : pubDidKid, }; const nonce = CryptoUtils.randomBytes(24); const additionalData = Convert.object({ @@ -626,12 +656,11 @@ async function createPermissionGrants( selectedDid: string, delegateBearerDid: BearerDid, agent: Web5Agent, - scopes: DwnPermissionScope[], + 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 - logger.log(`Creating permission grants for ${scopes.length} scopes given...`); const permissionGrants = await Promise.all( scopes.map((scope) => { // check if the scope is a records permission scope, or a protocol configure scope, if so it should use a delegated permission. @@ -698,7 +727,7 @@ async function prepareProtocol( messageParams : { filter: { protocol: protocolDefinition.protocol } }, }); - if ( queryMessage.reply.status.code !== 200) { + if (queryMessage.reply.status.code !== 200) { // if the query failed, throw an error throw new Error( `Could not fetch protocol: ${queryMessage.reply.status.detail}` @@ -724,9 +753,8 @@ async function prepareProtocol( author : selectedDid, target : selectedDid, messageType : DwnInterface.ProtocolsConfigure, - rawMessage : configureMessage + rawMessage : configureMessage, }); - } else { logger.log(`Protocol already exists: ${protocolDefinition.protocol}`); @@ -758,85 +786,100 @@ async function submitAuthResponse( selectedDid: string, authRequest: Web5ConnectAuthRequest, randomPin: string, + clientEcdhDid: DidResolutionResult, agent: Web5Agent ) { + // ephemeral provider did for signing + const providerSigningDid = await DidJwk.create(); + + // ephemeral provider did for ECDH + const providerEcdhDid = await DidJwk.create(); + + // delegate did for persistent use + // not used for signing or encryption during wallet connect 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) => { - 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.'); + if (authRequest.permissionRequests) { + const delegateGrantPromises = authRequest.permissionRequests.map( + async (permissionRequest) => { + 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( + selectedDid, + delegateBearerDid, + agent, + permissionScopes + ); + + return permissionGrants; } + ); - await prepareProtocol(selectedDid, agent, protocolDefinition); + const delegateGrants = (await Promise.all(delegateGrantPromises)).flat(); + const responseObject = await Oidc.createResponseObject({ + //* the IDP's did that was selected to be connected + iss : selectedDid, + //* the client's new identity + sub : delegateBearerDid.uri, + //* the client's temporary ephemeral did used for connect + aud : authRequest.client_id, + //* the nonce of the original auth request + nonce : authRequest.nonce, + delegateGrants, + delegatePortableDid, + }); - const permissionGrants = await Oidc.createPermissionGrants( - selectedDid, - delegateBearerDid, - agent, - permissionScopes - ); + // Sign using the signing key + const responseObjectJwt = await Oidc.signJwt({ + did : providerSigningDid, + data : responseObject, + }); - return permissionGrants; + if (!clientEcdhDid.didDocument?.verificationMethod?.[0].id) { + throw new Error('Unable to resolve the encryption DID used by the client for ECDH'); } - ); - const delegateGrants = (await Promise.all(delegateGrantPromises)).flat(); - - logger.log('Generating auth response object...'); - const responseObject = await Oidc.createResponseObject({ - //* the IDP's did that was selected to be connected - iss : selectedDid, - //* the client's new identity - sub : delegateBearerDid.uri, - //* the client's temporary ephemeral did used for connect - aud : authRequest.client_id, - //* the nonce of the original auth request - nonce : authRequest.nonce, - delegateGrants, - delegatePortableDid, - }); - - // Sign the Response Object using the ephemeral DID's signing key. - logger.log('Signing auth response object...'); - const responseObjectJwt = await Oidc.signJwt({ - did : delegateBearerDid, - data : responseObject, - }); - const clientDid = await DidJwk.resolve(authRequest.client_id); - - const sharedKey = await Oidc.deriveSharedKey( - delegateBearerDid, - clientDid?.didDocument! - ); + const sharedKey = await Oidc.deriveSharedKey( + providerEcdhDid, + clientEcdhDid?.didDocument + ); - logger.log('Encrypting auth response object...'); - const encryptedResponse = Oidc.encryptAuthResponse({ - jwt : responseObjectJwt!, - encryptionKey : sharedKey, - delegateDidKeyId : delegateBearerDid.document.verificationMethod![0].id, - randomPin, - }); + const encryptedResponse = Oidc.encryptWithPin({ + jwt : responseObjectJwt, + encryptionKey : sharedKey, + pubDidKid : providerEcdhDid.document.verificationMethod![0].id, + randomPin, + }); - const formEncodedRequest = new URLSearchParams({ - id_token : encryptedResponse, - state : authRequest.state, - }).toString(); - - logger.log(`Sending auth response object to Web5 Connect server: ${authRequest.redirect_uri}`); - await fetch(authRequest.redirect_uri, { - body : formEncodedRequest, - method : 'POST', - headers : { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }); + const formEncodedRequest = new URLSearchParams({ + id_token : encryptedResponse, + state : authRequest.state, + }).toString(); + + await fetch(authRequest.redirect_uri, { + body : formEncodedRequest, + method : 'POST', + headers : { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); + } } export const Oidc = { @@ -846,8 +889,8 @@ export const Oidc = { decryptAuthRequest, createPermissionGrants, createResponseObject, - encryptAuthResponse, - decryptAuthResponse, + encryptWithPin, + decryptWithPin, deriveSharedKey, signJwt, verifyJwt, diff --git a/packages/agent/tests/connect.spec.ts b/packages/agent/tests/connect.spec.ts index 9f34a8e10..4c3eee84c 100644 --- a/packages/agent/tests/connect.spec.ts +++ b/packages/agent/tests/connect.spec.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import sinon from 'sinon'; import { CryptoUtils } from '@web5/crypto'; -import { type BearerDid, DidDht, DidJwk, PortableDid } from '@web5/dids'; +import { type BearerDid, DidDht, DidJwk, type DidResolutionResult, type PortableDid } from '@web5/dids'; import { Convert } from '@web5/common'; import { Oidc, @@ -11,16 +11,25 @@ import { import { PlatformAgentTestHarness } from '../src/test-harness.js'; import { TestAgent } from './utils/test-agent.js'; import { testDwnUrl } from './utils/test-config.js'; -import { BearerIdentity, DwnInterface, DwnMessage, DwnProtocolDefinition, WalletConnect } from '../src/index.js'; +import { BearerIdentity, DwnInterface, DwnMessage, DwnProtocolDefinition, WalletConnect } from '../src/index.js'; import { RecordsPermissionScope } from '@tbd54566975/dwn-sdk-js'; import { DwnInterfaceName, DwnMethodName } from '@tbd54566975/dwn-sdk-js'; describe('web5 connect', function () { this.timeout(20000); - /** The temporary DID that web5 connect created on behalf of the client */ - let clientEphemeralBearerDid: BearerDid; - let clientEphemeralPortableDid: PortableDid; + let clientSigningBearerDid: BearerDid; + let clientSigningPortableDid: PortableDid; + + let clientEcdhBearerDid: BearerDid; + let clientEcdhPortableDid: PortableDid; + let clientEcdhDidAsResolvedByWallet: DidResolutionResult; + + let providerSigningBearerDid: BearerDid; + let providerSigningPortableDid: PortableDid; + + let providerEcdhBearerDid: BearerDid; + let providerEcdhPortableDid: PortableDid; /** The real tenant (identity) of the DWN that the provider had chosen to connect */ let providerIdentity: BearerIdentity; @@ -189,8 +198,13 @@ describe('web5 connect', function () { testDwnUrls : [testDwnUrl], }); - clientEphemeralBearerDid = await DidJwk.create(); - clientEphemeralPortableDid = await clientEphemeralBearerDid.export(); + clientEcdhBearerDid = await DidJwk.create(); + providerEcdhBearerDid = await DidJwk.create(); + + clientSigningBearerDid = await DidJwk.create(); + clientSigningPortableDid = await clientSigningBearerDid.export(); + + providerSigningBearerDid = await DidJwk.create(); delegateBearerDid = await DidJwk.create(); delegatePortableDid = await delegateBearerDid.export(); @@ -225,7 +239,7 @@ describe('web5 connect', function () { const options = { displayName : 'Sample App', - client_id : clientEphemeralPortableDid.uri, + client_id : clientSigningPortableDid.uri, scope : 'openid did:jwk', // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), // code_challenge_method : 'S256' as const, @@ -243,16 +257,17 @@ describe('web5 connect', function () { it('should construct a signed jwt of an authrequest', async () => { authRequestJwt = await Oidc.signJwt({ - did : clientEphemeralBearerDid, + did : clientSigningBearerDid, data : authRequest, }); expect(authRequestJwt).to.be.a('string'); }); - it('should encrypt an authrequest using the code challenge', async () => { + it('should encrypt an authrequest using the random static encryptionKey', async () => { authRequestJwe = await Oidc.encryptAuthRequest({ jwt : authRequestJwt, - encryptionKey : authRequestEncryptionKey + encryptionKey : authRequestEncryptionKey, + kid : clientEcdhBearerDid.document.verificationMethod![0].id }); expect(authRequestJwe).to.be.a('string'); expect(authRequestJwe.split('.')).to.have.lengthOf(5); @@ -282,7 +297,9 @@ describe('web5 connect', function () { authorizeUrl, Convert.uint8Array(authRequestEncryptionKey).toBase64Url() ); - expect(result).to.deep.equal(authRequest); + expect(result.authRequest).to.deep.equal(authRequest); + expect(result.clientEcdhDid.didDocument?.id).to.equal(clientEcdhBearerDid.uri); + clientEcdhDidAsResolvedByWallet = result.clientEcdhDid; }); // TODO: waiting for DWN feature complete @@ -317,20 +334,21 @@ describe('web5 connect', function () { it('should sign the authresponse with its provider did', async () => { authResponseJwt = await Oidc.signJwt({ - did : delegateBearerDid, + did : providerSigningBearerDid, data : authResponse, }); + expect(authResponseJwt).to.be.a('string'); }); it('should derive a valid ECDH private key for both provider and client which is identical', async () => { const providerECDHDerivedPrivateKey = await Oidc.deriveSharedKey( - delegateBearerDid, - clientEphemeralBearerDid.document + providerEcdhBearerDid, + clientEcdhBearerDid.document ); const clientECDHDerivedPrivateKey = await Oidc.deriveSharedKey( - clientEphemeralBearerDid, - delegateBearerDid.document + clientEcdhBearerDid, + providerEcdhBearerDid.document ); expect(providerECDHDerivedPrivateKey).to.be.instanceOf(Uint8Array); @@ -350,11 +368,12 @@ describe('web5 connect', function () { const randomBytesStub = sinon .stub(CryptoUtils, 'randomBytes') .returns(encryptionNonce); - authResponseJwe = Oidc.encryptAuthResponse({ - jwt : authResponseJwt, - encryptionKey : sharedECDHPrivateKey, + + authResponseJwe = Oidc.encryptWithPin({ + jwt : authResponseJwt, + encryptionKey : sharedECDHPrivateKey, randomPin, - delegateDidKeyId : delegateBearerDid.document.verificationMethod![0].id, + pubDidKid : providerEcdhBearerDid.document.verificationMethod![0].id, }); expect(authResponseJwe).to.be.a('string'); expect(randomBytesStub.calledOnce).to.be.true; @@ -363,7 +382,11 @@ 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 didJwkStub = sinon.stub(DidJwk, 'create'); + didJwkStub.onFirstCall().resolves(providerSigningBearerDid); + didJwkStub.onSecondCall().resolves(providerEcdhBearerDid); + didJwkStub.onThirdCall().resolves(delegateBearerDid); const formEncodedRequest = new URLSearchParams({ id_token : authResponseJwe, @@ -392,6 +415,7 @@ describe('web5 connect', function () { selectedDid, authRequest, randomPin, + clientEcdhDidAsResolvedByWallet, testHarness.agent ); expect(fetchSpy.calledOnce).to.be.true; @@ -400,8 +424,8 @@ describe('web5 connect', function () { describe('client pin entry final phase', function () { it('should get the authresponse from server and decrypt the jwe using the pin', async () => { - const result = await Oidc.decryptAuthResponse( - clientEphemeralBearerDid, + const result = await Oidc.decryptWithPin( + clientEcdhBearerDid, authResponseJwe, randomPin ); @@ -411,8 +435,8 @@ describe('web5 connect', function () { it('should fail decrypting the jwe if the wrong pin is entered', async () => { try { - await Oidc.decryptAuthResponse( - clientEphemeralBearerDid, + await Oidc.decryptWithPin( + clientEcdhBearerDid, authResponseJwe, '87383837583757835737537734783' ); @@ -435,7 +459,9 @@ describe('web5 connect', function () { it('should complete the whole connect flow with the correct pin', async function () { const fetchStub = sinon.stub(globalThis, 'fetch'); const onWalletUriReadySpy = sinon.spy(); - sinon.stub(DidJwk, 'create').resolves(clientEphemeralBearerDid); + const didJwkStub = sinon.stub(DidJwk, 'create'); + didJwkStub.onFirstCall().resolves(clientSigningBearerDid); + didJwkStub.onSecondCall().resolves(clientEcdhBearerDid); const par = { expires_in : 3600000, @@ -463,8 +489,7 @@ describe('web5 connect', function () { connectServerUrl : 'http://localhost:3000/connect', permissionRequests : [ { - protocolDefinition : {} as any, - permissionScopes : {} as any, + protocolDefinition: {} as any, }, ], onWalletUriReady : (uri) => onWalletUriReadySpy(uri), @@ -508,7 +533,7 @@ describe('web5 connect', function () { const options = { displayName : 'Sample App', - client_id : clientEphemeralPortableDid.uri, + client_id : clientSigningPortableDid.uri, scope : 'openid did:jwk', // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), // code_challenge_method : 'S256' as const, @@ -535,6 +560,7 @@ describe('web5 connect', function () { providerIdentity.did.uri, authRequest, randomPin, + clientEcdhDidAsResolvedByWallet, testHarness.agent ); @@ -564,7 +590,7 @@ describe('web5 connect', function () { const options = { displayName : 'Sample App', - client_id : clientEphemeralPortableDid.uri, + client_id : clientSigningPortableDid.uri, scope : 'openid did:jwk', // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), // code_challenge_method : 'S256' as const, @@ -588,6 +614,7 @@ describe('web5 connect', function () { providerIdentity.did.uri, authRequest, randomPin, + clientEcdhDidAsResolvedByWallet, testHarness.agent ); @@ -612,6 +639,7 @@ describe('web5 connect', function () { providerIdentity.did.uri, authRequest, randomPin, + clientEcdhDidAsResolvedByWallet, testHarness.agent ); @@ -637,7 +665,7 @@ describe('web5 connect', function () { const options = { displayName : 'Sample App', - client_id : clientEphemeralPortableDid.uri, + client_id : clientSigningPortableDid.uri, scope : 'openid did:jwk', // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), // code_challenge_method : 'S256' as const, @@ -663,6 +691,7 @@ describe('web5 connect', function () { providerIdentity.did.uri, authRequest, randomPin, + clientEcdhDidAsResolvedByWallet, testHarness.agent ); @@ -685,7 +714,7 @@ describe('web5 connect', function () { const options = { displayName : 'Sample App', - client_id : clientEphemeralPortableDid.uri, + client_id : clientSigningPortableDid.uri, scope : 'openid did:jwk', // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), // code_challenge_method : 'S256' as const, @@ -714,6 +743,7 @@ describe('web5 connect', function () { providerIdentity.did.uri, authRequest, randomPin, + clientEcdhDidAsResolvedByWallet, testHarness.agent ); @@ -737,7 +767,7 @@ describe('web5 connect', function () { const options = { displayName : 'Sample App', - client_id : clientEphemeralPortableDid.uri, + client_id : clientSigningPortableDid.uri, scope : 'openid did:jwk', // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), // code_challenge_method : 'S256' as const, @@ -763,6 +793,7 @@ describe('web5 connect', function () { providerIdentity.did.uri, authRequest, randomPin, + clientEcdhDidAsResolvedByWallet, testHarness.agent ); @@ -789,7 +820,7 @@ describe('web5 connect', function () { const options = { displayName : 'Sample App', - client_id : clientEphemeralPortableDid.uri, + client_id : clientSigningPortableDid.uri, scope : 'openid did:jwk', // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), // code_challenge_method : 'S256' as const, @@ -804,6 +835,7 @@ describe('web5 connect', function () { providerIdentity.did.uri, authRequest, randomPin, + clientEcdhDidAsResolvedByWallet, testHarness.agent ); diff --git a/packages/api/src/web5.ts b/packages/api/src/web5.ts index bc5ee95bd..40bc4662c 100644 --- a/packages/api/src/web5.ts +++ b/packages/api/src/web5.ts @@ -8,16 +8,14 @@ import type { BearerIdentity, DwnDataEncodedRecordsWriteMessage, DwnMessagesPermissionScope, - DwnProtocolDefinition, DwnRecordsPermissionScope, HdIdentityVault, - Permission, WalletConnectOptions, Web5Agent, } from '@web5/agent'; import { Web5UserAgent } from '@web5/user-agent'; -import { DwnInterface, DwnRegistrar, WalletConnect } from '@web5/agent'; +import { DwnRegistrar, WalletConnect } from '@web5/agent'; import { DidApi } from './did-api.js'; import { DwnApi } from './dwn-api.js'; @@ -36,37 +34,6 @@ export type DidCreateOptions = { dwnEndpoints?: string[]; } -/** - * Represents a permission request for a protocol definition. - */ -export type ConnectPermissionRequest = { - /** - * The protocol definition for the protocol being requested. - */ - protocolDefinition: DwnProtocolDefinition; - - /** - * The permissions being requested for the protocol. If none are provided, the default is to request all permissions. - */ - permissions?: Permission[]; -} - -/** - * Options for connecting to a Web5 agent. This includes the ability to connect to an external wallet. - * - * NOTE: the returned `ConnectPermissionRequest` type is different to the `ConnectPermissionRequest` type in the `@web5/agent` package. - */ -export type ConnectOptions = Omit & { - /** The user friendly name of the client/app to be displayed when prompting end-user with permission requests. */ - displayName: string; - - /** - * The permissions that are being requested for the connected DID. - * This is used to create the {@link ConnectPermissionRequest} for the wallet connect flow. - */ - permissionRequests: ConnectPermissionRequest[]; -} - /** Optional overrides that can be provided when calling {@link Web5.connect}. */ export type Web5ConnectOptions = { /** @@ -74,7 +41,7 @@ export type Web5ConnectOptions = { * This param currently will not work in apps that are currently connected. * It must only be invoked at registration with a reset and empty DWN and agent. */ - walletConnectOptions?: ConnectOptions; + walletConnectOptions?: WalletConnectOptions; /** * Provide a {@link Web5Agent} implementation. Defaults to creating a local @@ -290,15 +257,20 @@ export class Web5 { recoveryPhrase = await userAgent.initialize({ password, recoveryPhrase, dwnEndpoints: serviceEndpointNodes }); } await userAgent.start({ password }); - // Attempt to retrieve the connected Identity if it exists. - const connectedIdentity: BearerIdentity = await userAgent.identity.connectedIdentity(); + + // Attempt to retrieve the connected Identity if it exists + const connectedIdentity = await userAgent.identity.connectedIdentity(); let identity: BearerIdentity; let connectedProtocols: string[] = []; + + const isWalletConnect = walletConnectOptions && !walletConnectOptions.exported; + const isWalletExportedConnect = walletConnectOptions && walletConnectOptions.exported; + if (connectedIdentity) { - // if a connected identity is found, use it // TODO: In the future, implement a way to re-connect an already connected identity and apply additional grants/protocols identity = connectedIdentity; - } else if (walletConnectOptions) { + } else if (isWalletConnect) { + console.log('IN walletConnect (non export) case'); if (sync === 'off') { // Currently we require sync to be enabled when using WalletConnect // This is to ensure a connected app is not in a disjointed state from any other clients/app using the connectedDid @@ -310,18 +282,9 @@ export class Web5 { // No connected identity found and connectOptions are provided, attempt to import a delegated DID from an external wallet try { - const { permissionRequests, ...connectOptions } = walletConnectOptions; - const walletPermissionRequests = permissionRequests.map(({ protocolDefinition, permissions }) => WalletConnect.createPermissionRequestForProtocol({ - definition : protocolDefinition, - permissions : permissions ?? [ - 'read', 'write', 'delete', 'query', 'subscribe' - ]} - )); - - const { delegatePortableDid, connectedDid, delegateGrants } = await WalletConnect.initClient({ - ...connectOptions, - permissionRequests: walletPermissionRequests, - }); + console.log('before initclient'); + const { delegatePortableDid, connectedDid, delegateGrants } = await WalletConnect.initClient(walletConnectOptions); + console.log('after initclient'); // Import the delegated DID as an Identity in the User Agent. // Setting the connectedDID in the metadata applies a relationship between the signer identity and the one it is impersonating. @@ -334,6 +297,7 @@ export class Web5 { tenant : agent.agentDid.uri, } }}); + console.log('does have an identity'); // Attempts to process the connected grants to be used by the delegateDID // If the process fails, we want to clean up the identity @@ -345,7 +309,28 @@ export class Web5 { await this.cleanUpIdentity({ identity, userAgent }); throw new Error(`Failed to connect to wallet: ${error.message}`); } + } else if (isWalletExportedConnect) { + console.log('IN EXPORT case'); + if (sync === 'off') { + // sync must be enabled when using WalletConnect to ensure a connected app + // is not in a disjointed state from any other clients using the connectedDid + throw new Error('Sync must not be disabled when using WalletConnect'); + } + + // Since we are connecting a new identity, we will want to register sync for the connectedDid + registerSync = true; + + try { + // TODO: do the exported connect + } catch (error:any) { + // clean up the DID and Identity if import fails and throw + // TODO: Implement the ability to purge all of our messages as a tenant + await this.cleanUpIdentity({ identity, userAgent }); + throw new Error(`Failed to connect to wallet: ${error.message}`); + } + // else connecting to a locally held DID } else { + console.log('IN else case'); // No connected identity found and no connectOptions provided, use local Identities // Query the Agent's DWN tenant for identity records. const identities = await userAgent.identity.list(); @@ -436,7 +421,7 @@ export class Web5 { } }); - if(walletConnectOptions !== undefined) { + if (walletConnectOptions !== undefined) { // If we are using WalletConnect, we should do a one-shot sync to pull down any messages that are associated with the connectedDid await userAgent.sync.sync('pull'); } @@ -484,9 +469,8 @@ export class Web5 { } /** - * A static method to process connected grants for a delegate DID. - * - * This will store the grants as the DWN owner to be used later when impersonating the connected DID. + * Processes connected grants for a delegate DID. + * Stores the grants as the DWN owner to be used later when impersonating the connected DID. */ static async processConnectedGrants({ grants, agent, delegateDid }: { grants: DwnDataEncodedRecordsWriteMessage[], diff --git a/packages/api/tests/web5.spec.ts b/packages/api/tests/web5.spec.ts index 4576989c4..7a5e61ab0 100644 --- a/packages/api/tests/web5.spec.ts +++ b/packages/api/tests/web5.spec.ts @@ -798,12 +798,7 @@ describe('web5 api', () => { sinon.stub(Web5UserAgent, 'create').resolves(testHarness.agent as Web5UserAgent); - // spy on the WalletConnect createPermissionRequestForProtocol method - const requestPermissionsSpy = sinon.spy(WalletConnect, 'createPermissionRequestForProtocol'); - - // We throw and spy on the initClient method to avoid the actual WalletConnect initialization - // but to still be able to spy on the passed parameters - sinon.stub(WalletConnect, 'initClient').throws('Error'); + const createPermissionRequestForProtocolSpy = sinon.spy(WalletConnect, 'createPermissionRequestForProtocol'); // stub the cleanUpIdentity method to avoid actual cleanup sinon.stub(Web5 as any, 'cleanUpIdentity').resolves(); @@ -837,12 +832,11 @@ describe('web5 api', () => { expect.fail('Should have thrown an error'); } catch(error: any) { - // we expect an error because we stubbed the initClient method to throw it - expect(error.message).to.include('Sinon-provided Error'); + // we expect an error because we aren't testing the whole e2e flow + expect(error.message).to.include('Failed to connect to wallet'); - // The `createPermissionRequestForProtocol` method should have been called once for the provided protocol - expect(requestPermissionsSpy.callCount).to.equal(1); - const call = requestPermissionsSpy.getCall(0); + expect(createPermissionRequestForProtocolSpy.callCount).to.equal(1); + const call = createPermissionRequestForProtocolSpy.getCall(0); // since no explicit permissions were provided, all permissions should be requested expect(call.args[0].permissions).to.have.members([ @@ -858,10 +852,6 @@ describe('web5 api', () => { // spy on the WalletConnect createPermissionRequestForProtocol method const requestPermissionsSpy = sinon.spy(WalletConnect, 'createPermissionRequestForProtocol'); - // We throw and spy on the initClient method to avoid the actual WalletConnect initialization - // but to still be able to spy on the passed parameters - sinon.stub(WalletConnect, 'initClient').throws('Error'); - // stub the cleanUpIdentity method to avoid actual cleanup sinon.stub(Web5 as any, 'cleanUpIdentity').resolves(); @@ -912,8 +902,8 @@ describe('web5 api', () => { expect.fail('Should have thrown an error'); } catch(error: any) { - // we expect an error because we stubbed the initClient method to throw it - expect(error.message).to.include('Sinon-provided Error'); + // we expect an error because we aren't testing the whole e2e flow + expect(error.message).to.include('Failed to connect to wallet'); // The `createPermissionRequestForProtocol` method should have been called once for each provided request expect(requestPermissionsSpy.callCount).to.equal(2); From 30b21096811089c0376fbe9d98f5236542d5d86f Mon Sep 17 00:00:00 2001 From: Tim Shamilov Date: Wed, 23 Oct 2024 15:50:53 -0400 Subject: [PATCH 02/11] tiny cleanup --- packages/agent/src/connect.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/agent/src/connect.ts b/packages/agent/src/connect.ts index 38d850226..43c81def3 100644 --- a/packages/agent/src/connect.ts +++ b/packages/agent/src/connect.ts @@ -115,14 +115,8 @@ async function initClient({ const normalizedPermissionRequests = permissionRequests.map( ({ protocolDefinition, permissions }) => WalletConnect.createPermissionRequestForProtocol({ - definition : protocolDefinition, - permissions : permissions ?? [ - 'read', - 'write', - 'delete', - 'query', - 'subscribe', - ], + definition: protocolDefinition, + permissions, }) ); @@ -256,9 +250,17 @@ function createPermissionRequestForProtocol({ /** The protocol definition for the protocol being requested */ definition: DwnProtocolDefinition; - /** The permissions being requested for the protocol */ - permissions: Permission[]; + /** The permissions being requested for the protocol. Defaults to all. */ + permissions?: Permission[]; }) { + permissions ??= [ + 'read', + 'write', + 'delete', + 'query', + 'subscribe', + ]; + const requests: DwnPermissionScope[] = []; // Add the ability to query for the specific protocol From e0a16cf8048823e171dfb28b8f4e4eeefffd523c Mon Sep 17 00:00:00 2001 From: Tim Shamilov Date: Wed, 23 Oct 2024 16:21:39 -0400 Subject: [PATCH 03/11] fix test --- packages/api/tests/web5.spec.ts | 58 ++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/packages/api/tests/web5.spec.ts b/packages/api/tests/web5.spec.ts index 7a5e61ab0..7a971e6c7 100644 --- a/packages/api/tests/web5.spec.ts +++ b/packages/api/tests/web5.spec.ts @@ -795,7 +795,6 @@ describe('web5 api', () => { }); it('should request all permissions for a protocol if no specific permissions are provided', async () => { - sinon.stub(Web5UserAgent, 'create').resolves(testHarness.agent as Web5UserAgent); const createPermissionRequestForProtocolSpy = sinon.spy(WalletConnect, 'createPermissionRequestForProtocol'); @@ -818,7 +817,6 @@ describe('web5 api', () => { }; try { - await Web5.connect({ walletConnectOptions: { displayName : 'Sample App', @@ -836,21 +834,23 @@ describe('web5 api', () => { expect(error.message).to.include('Failed to connect to wallet'); expect(createPermissionRequestForProtocolSpy.callCount).to.equal(1); - const call = createPermissionRequestForProtocolSpy.getCall(0); - - // since no explicit permissions were provided, all permissions should be requested - expect(call.args[0].permissions).to.have.members([ - 'read', 'write', 'delete', 'query', 'subscribe' + const result = createPermissionRequestForProtocolSpy.getCall(0).returnValue; + + // Check if all permissions are included in the result + expect(result.permissionScopes).to.deep.include.members([ + { protocol: protocolDefinition.protocol, interface: DwnInterfaceName.Records, method: DwnMethodName.Read }, + { protocol: protocolDefinition.protocol, interface: DwnInterfaceName.Records, method: DwnMethodName.Write }, + { protocol: protocolDefinition.protocol, interface: DwnInterfaceName.Records, method: DwnMethodName.Delete }, + { protocol: protocolDefinition.protocol, interface: DwnInterfaceName.Records, method: DwnMethodName.Query }, + { protocol: protocolDefinition.protocol, interface: DwnInterfaceName.Records, method: DwnMethodName.Subscribe } ]); } }); it('should only request the specified permissions for a protocol', async () => { - sinon.stub(Web5UserAgent, 'create').resolves(testHarness.agent as Web5UserAgent); - // spy on the WalletConnect createPermissionRequestForProtocol method - const requestPermissionsSpy = sinon.spy(WalletConnect, 'createPermissionRequestForProtocol'); + const createPermissionRequestForProtocolSpy = sinon.spy(WalletConnect, 'createPermissionRequestForProtocol'); // stub the cleanUpIdentity method to avoid actual cleanup sinon.stub(Web5 as any, 'cleanUpIdentity').resolves(); @@ -883,9 +883,7 @@ describe('web5 api', () => { } }; - try { - await Web5.connect({ walletConnectOptions: { displayName : 'Sample App', @@ -894,8 +892,8 @@ describe('web5 api', () => { validatePin : async () => { return '1234'; }, onWalletUriReady : (_walletUri: string) => {}, permissionRequests : [ - { protocolDefinition: protocol1Definition }, // no permissions provided, expect all permissions to be requested - { protocolDefinition: protocol2Definition, permissions: ['read', 'write'] } // only read and write permissions provided + { protocolDefinition: protocol1Definition }, + { protocolDefinition: protocol2Definition, permissions: ['read', 'write'] } ] } }); @@ -905,20 +903,28 @@ describe('web5 api', () => { // we expect an error because we aren't testing the whole e2e flow expect(error.message).to.include('Failed to connect to wallet'); - // The `createPermissionRequestForProtocol` method should have been called once for each provided request - expect(requestPermissionsSpy.callCount).to.equal(2); - const call1 = requestPermissionsSpy.getCall(0); - - // since no explicit permissions were provided for the first protocol, all permissions should be requested - expect(call1.args[0].permissions).to.have.members([ - 'read', 'write', 'delete', 'query', 'subscribe' + expect(createPermissionRequestForProtocolSpy.callCount).to.equal(2); + const result1 = createPermissionRequestForProtocolSpy.getCall(0).returnValue; + const result2 = createPermissionRequestForProtocolSpy.getCall(1).returnValue; + + // Check if all permissions are included for the first protocol + expect(result1.permissionScopes).to.deep.include.members([ + { protocol: protocol1Definition.protocol, interface: DwnInterfaceName.Records, method: DwnMethodName.Read }, + { protocol: protocol1Definition.protocol, interface: DwnInterfaceName.Records, method: DwnMethodName.Write }, + { protocol: protocol1Definition.protocol, interface: DwnInterfaceName.Records, method: DwnMethodName.Delete }, + { protocol: protocol1Definition.protocol, interface: DwnInterfaceName.Records, method: DwnMethodName.Query }, + { protocol: protocol1Definition.protocol, interface: DwnInterfaceName.Records, method: DwnMethodName.Subscribe } ]); - const call2 = requestPermissionsSpy.getCall(1); - - // only the provided permissions should be requested for the second protocol - expect(call2.args[0].permissions).to.have.members([ - 'read', 'write' + // Check if only read and write permissions are included for the second protocol + expect(result2.permissionScopes).to.deep.include.members([ + { protocol: protocol2Definition.protocol, interface: DwnInterfaceName.Records, method: DwnMethodName.Read }, + { protocol: protocol2Definition.protocol, interface: DwnInterfaceName.Records, method: DwnMethodName.Write } + ]); + expect(result2.permissionScopes).to.not.deep.include.members([ + { protocol: protocol2Definition.protocol, interface: DwnInterfaceName.Records, method: DwnMethodName.Delete }, + { protocol: protocol2Definition.protocol, interface: DwnInterfaceName.Records, method: DwnMethodName.Query }, + { protocol: protocol2Definition.protocol, interface: DwnInterfaceName.Records, method: DwnMethodName.Subscribe } ]); } }); From 78fd373a42d958186c437cefb3400cba7918845e Mon Sep 17 00:00:00 2001 From: Tim Shamilov Date: Wed, 23 Oct 2024 16:22:50 -0400 Subject: [PATCH 04/11] remove dead code --- packages/agent/src/oidc.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/agent/src/oidc.ts b/packages/agent/src/oidc.ts index 25e9762c9..975ec86c0 100644 --- a/packages/agent/src/oidc.ts +++ b/packages/agent/src/oidc.ts @@ -464,7 +464,6 @@ function decryptAuthRequest({ const encryptionKeyBytes = Convert.base64Url(encryption_key).toUint8Array(); const protectedHeader = Convert.base64Url(protectedHeaderB64U).toUint8Array(); const additionalData = protectedHeader; - const additionalDataObj = Convert.base64Url(protectedHeaderB64U).toObject(); const nonce = Convert.base64Url(nonceB64U).toUint8Array(); const ciphertext = Convert.base64Url(ciphertextB64U).toUint8Array(); const authenticationTag = Convert.base64Url( From 58bb0c9e3bc3ef20574ab427debd35998ae5cf86 Mon Sep 17 00:00:00 2001 From: Tim Shamilov Date: Wed, 23 Oct 2024 16:32:38 -0400 Subject: [PATCH 05/11] cleanup around future export --- packages/api/src/web5.ts | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/packages/api/src/web5.ts b/packages/api/src/web5.ts index 40bc4662c..36ebc99eb 100644 --- a/packages/api/src/web5.ts +++ b/packages/api/src/web5.ts @@ -280,11 +280,8 @@ export class Web5 { // Since we are connecting a new identity, we will want to register sync for the connectedDid registerSync = true; - // No connected identity found and connectOptions are provided, attempt to import a delegated DID from an external wallet try { - console.log('before initclient'); const { delegatePortableDid, connectedDid, delegateGrants } = await WalletConnect.initClient(walletConnectOptions); - console.log('after initclient'); // Import the delegated DID as an Identity in the User Agent. // Setting the connectedDID in the metadata applies a relationship between the signer identity and the one it is impersonating. @@ -310,25 +307,7 @@ export class Web5 { throw new Error(`Failed to connect to wallet: ${error.message}`); } } else if (isWalletExportedConnect) { - console.log('IN EXPORT case'); - if (sync === 'off') { - // sync must be enabled when using WalletConnect to ensure a connected app - // is not in a disjointed state from any other clients using the connectedDid - throw new Error('Sync must not be disabled when using WalletConnect'); - } - - // Since we are connecting a new identity, we will want to register sync for the connectedDid - registerSync = true; - - try { - // TODO: do the exported connect - } catch (error:any) { - // clean up the DID and Identity if import fails and throw - // TODO: Implement the ability to purge all of our messages as a tenant - await this.cleanUpIdentity({ identity, userAgent }); - throw new Error(`Failed to connect to wallet: ${error.message}`); - } - // else connecting to a locally held DID + throw new Error('Exported connect will be implemented in a separate PR'); } else { console.log('IN else case'); // No connected identity found and no connectOptions provided, use local Identities From b09fa761b4ac1ef87dc4b014932ad421115e337e Mon Sep 17 00:00:00 2001 From: Tim Shamilov Date: Thu, 24 Oct 2024 12:01:57 -0400 Subject: [PATCH 06/11] cleanup --- packages/agent/src/connect.ts | 4 ---- packages/api/src/web5.ts | 3 --- 2 files changed, 7 deletions(-) diff --git a/packages/agent/src/connect.ts b/packages/agent/src/connect.ts index 43c81def3..48a19ef43 100644 --- a/packages/agent/src/connect.ts +++ b/packages/agent/src/connect.ts @@ -138,7 +138,6 @@ async function initClient({ baseURL : connectServerUrl, endpoint : 'callback', }); - console.log('after callback endpoint'); // build the PAR request const request = await Oidc.createAuthRequest({ @@ -168,7 +167,6 @@ async function initClient({ kid : clientEcdhDid.document.verificationMethod![0].id, encryptionKey, }); - console.log('after requestobjecjtwe'); // Convert the encrypted Request Object to URLSearchParams for form encoding. const formEncodedRequest = new URLSearchParams({ @@ -187,7 +185,6 @@ async function initClient({ 'Content-Type': 'application/x-www-form-urlencoded', }, }); - console.log('after par'); if (!parResponse.ok) { throw new Error(`${parResponse.status}: ${parResponse.statusText}`); @@ -204,7 +201,6 @@ async function initClient({ 'encryption_key', Convert.uint8Array(encryptionKey).toBase64Url() ); - console.log('after generatedwalleturi'); // call user's callback so they can send the URI to the wallet as they see fit onWalletUriReady(generatedWalletUri.toString()); diff --git a/packages/api/src/web5.ts b/packages/api/src/web5.ts index 36ebc99eb..cd7e6f1ce 100644 --- a/packages/api/src/web5.ts +++ b/packages/api/src/web5.ts @@ -270,7 +270,6 @@ export class Web5 { // TODO: In the future, implement a way to re-connect an already connected identity and apply additional grants/protocols identity = connectedIdentity; } else if (isWalletConnect) { - console.log('IN walletConnect (non export) case'); if (sync === 'off') { // Currently we require sync to be enabled when using WalletConnect // This is to ensure a connected app is not in a disjointed state from any other clients/app using the connectedDid @@ -294,7 +293,6 @@ export class Web5 { tenant : agent.agentDid.uri, } }}); - console.log('does have an identity'); // Attempts to process the connected grants to be used by the delegateDID // If the process fails, we want to clean up the identity @@ -309,7 +307,6 @@ export class Web5 { } else if (isWalletExportedConnect) { throw new Error('Exported connect will be implemented in a separate PR'); } else { - console.log('IN else case'); // No connected identity found and no connectOptions provided, use local Identities // Query the Agent's DWN tenant for identity records. const identities = await userAgent.identity.list(); From 88c5bbe2ff1cff833bf6b59792e035733c8e102d Mon Sep 17 00:00:00 2001 From: Tim Shamilov Date: Thu, 24 Oct 2024 12:13:18 -0400 Subject: [PATCH 07/11] format --- packages/agent/src/connect.ts | 8 +- packages/agent/src/oidc.ts | 56 +++-- packages/agent/tests/connect.spec.ts | 353 ++++++++++++++++++++------- packages/api/src/web5.ts | 141 +++++++---- 4 files changed, 393 insertions(+), 165 deletions(-) diff --git a/packages/agent/src/connect.ts b/packages/agent/src/connect.ts index 48a19ef43..8798ed4b3 100644 --- a/packages/agent/src/connect.ts +++ b/packages/agent/src/connect.ts @@ -249,13 +249,7 @@ function createPermissionRequestForProtocol({ /** The permissions being requested for the protocol. Defaults to all. */ permissions?: Permission[]; }) { - permissions ??= [ - 'read', - 'write', - 'delete', - 'query', - 'subscribe', - ]; + permissions ??= ['read', 'write', 'delete', 'query', 'subscribe']; const requests: DwnPermissionScope[] = []; diff --git a/packages/agent/src/oidc.ts b/packages/agent/src/oidc.ts index 975ec86c0..147602742 100644 --- a/packages/agent/src/oidc.ts +++ b/packages/agent/src/oidc.ts @@ -12,7 +12,13 @@ import { import { concatenateUrl } from './utils.js'; import { xchacha20poly1305 } from '@noble/ciphers/chacha'; import type { ConnectPermissionRequest } from './connect.js'; -import { DidDocument, DidJwk, DidResolutionResult, PortableDid, type BearerDid } from '@web5/dids'; +import { + DidDocument, + DidJwk, + DidResolutionResult, + PortableDid, + type BearerDid, +} from '@web5/dids'; import { DwnDataEncodedRecordsWriteMessage, DwnInterface, @@ -291,7 +297,7 @@ async function createAuthRequest( async function encryptAuthRequest({ jwt, encryptionKey, - kid + kid, }: { jwt: string; encryptionKey: Uint8Array; @@ -302,7 +308,7 @@ async function encryptAuthRequest({ cty : 'JWT', enc : 'XC20P', typ : 'JWT', - kid + kid, }; const nonce = CryptoUtils.randomBytes(24); const additionalData = Convert.object(protectedHeader).toUint8Array(); @@ -438,7 +444,9 @@ const getAuthRequest = async (request_uri: string, encryption_key: string) => { })) as Web5ConnectAuthRequest; // get the pub DID that represents the client in ECDH and deriving a shared key - const header = Convert.base64Url(jwe.split('.')[0]).toObject() as JweHeaderParams; + const header = Convert.base64Url( + jwe.split('.')[0] + ).toObject() as JweHeaderParams; const clientEcdhDid = await DidJwk.resolve(header.kid!.split('#')[0]); @@ -502,7 +510,9 @@ async function decryptWithPin(clientDid: BearerDid, jwe: string, pin: string) { const jweProviderEcdhDidKid = await DidJwk.resolve(header.kid!.split('#')[0]); if (!jweProviderEcdhDidKid.didDocument) { - throw new Error('Could not resolve provider\'s didd document for shared key derivation'); + throw new Error( + 'Could not resolve provider\'s didd document for shared key derivation' + ); } // derive ECDH shared key using the provider's public key and our clientDid private key @@ -638,7 +648,10 @@ function shouldUseDelegatePermission(scope: DwnPermissionScope): boolean { // In the future only methods that modify state will be delegated and the rest will be normal permissions if (isRecordPermissionScope(scope)) { return true; - } else if (scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Configure) { + } else if ( + scope.interface === DwnInterfaceName.Protocols && + scope.method === DwnMethodName.Configure + ) { // ProtocolConfigure messages are also delegated, as they modify state return true; } @@ -675,7 +688,9 @@ async function createPermissionGrants( }) ); - logger.log(`Sending ${permissionGrants.length} permission grants to remote DWN...`); + logger.log( + `Sending ${permissionGrants.length} permission grants to remote DWN...` + ); 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; @@ -718,7 +733,6 @@ async function prepareProtocol( agent: Web5Agent, protocolDefinition: DwnProtocolDefinition ): Promise { - const queryMessage = await agent.processDwnRequest({ author : selectedDid, messageType : DwnInterface.ProtocolsQuery, @@ -731,16 +745,22 @@ async function prepareProtocol( throw new Error( `Could not fetch protocol: ${queryMessage.reply.status.detail}` ); - } else if (queryMessage.reply.entries === undefined || queryMessage.reply.entries.length === 0) { - logger.log(`Protocol does not exist, creating: ${protocolDefinition.protocol}`); + } else if ( + queryMessage.reply.entries === undefined || + queryMessage.reply.entries.length === 0 + ) { + logger.log( + `Protocol does not exist, creating: ${protocolDefinition.protocol}` + ); // 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, - target : selectedDid, - messageType : DwnInterface.ProtocolsConfigure, - messageParams : { definition: protocolDefinition }, - }); + const { reply: sendReply, message: configureMessage } = + await agent.sendDwnRequest({ + author : selectedDid, + target : selectedDid, + messageType : DwnInterface.ProtocolsConfigure, + messageParams : { definition: protocolDefinition }, + }); // 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) { @@ -851,7 +871,9 @@ async function submitAuthResponse( }); if (!clientEcdhDid.didDocument?.verificationMethod?.[0].id) { - throw new Error('Unable to resolve the encryption DID used by the client for ECDH'); + throw new Error( + 'Unable to resolve the encryption DID used by the client for ECDH' + ); } const sharedKey = await Oidc.deriveSharedKey( diff --git a/packages/agent/tests/connect.spec.ts b/packages/agent/tests/connect.spec.ts index 4c3eee84c..3ba2d9bec 100644 --- a/packages/agent/tests/connect.spec.ts +++ b/packages/agent/tests/connect.spec.ts @@ -1,7 +1,13 @@ import { expect } from 'chai'; import sinon from 'sinon'; import { CryptoUtils } from '@web5/crypto'; -import { type BearerDid, DidDht, DidJwk, type DidResolutionResult, type PortableDid } from '@web5/dids'; +import { + type BearerDid, + DidDht, + DidJwk, + type DidResolutionResult, + type PortableDid, +} from '@web5/dids'; import { Convert } from '@web5/common'; import { Oidc, @@ -11,7 +17,13 @@ import { import { PlatformAgentTestHarness } from '../src/test-harness.js'; import { TestAgent } from './utils/test-agent.js'; import { testDwnUrl } from './utils/test-config.js'; -import { BearerIdentity, DwnInterface, DwnMessage, DwnProtocolDefinition, WalletConnect } from '../src/index.js'; +import { + BearerIdentity, + DwnInterface, + DwnMessage, + DwnProtocolDefinition, + WalletConnect, +} from '../src/index.js'; import { RecordsPermissionScope } from '@tbd54566975/dwn-sdk-js'; import { DwnInterfaceName, DwnMethodName } from '@tbd54566975/dwn-sdk-js'; @@ -22,14 +34,11 @@ describe('web5 connect', function () { let clientSigningPortableDid: PortableDid; let clientEcdhBearerDid: BearerDid; - let clientEcdhPortableDid: PortableDid; let clientEcdhDidAsResolvedByWallet: DidResolutionResult; let providerSigningBearerDid: BearerDid; - let providerSigningPortableDid: PortableDid; let providerEcdhBearerDid: BearerDid; - let providerEcdhPortableDid: PortableDid; /** The real tenant (identity) of the DWN that the provider had chosen to connect */ let providerIdentity: BearerIdentity; @@ -181,7 +190,11 @@ describe('web5 connect', function () { const encryptionNonce = CryptoUtils.randomBytes(24); const randomPin = '9999'; + let clock: sinon.SinonFakeTimers; + before(async () => { + clock = sinon.useFakeTimers(new Date('2050-01-01T06:36:37.675Z').getTime()); + providerIdentityBearerDid = await DidDht.import({ portableDid: providerIdentityPortableDid, }); @@ -211,6 +224,7 @@ describe('web5 connect', function () { }); after(async () => { + clock.restore(); sinon.restore(); await testHarness.clearStorage(); await testHarness.closeStorage(); @@ -267,7 +281,7 @@ describe('web5 connect', function () { authRequestJwe = await Oidc.encryptAuthRequest({ jwt : authRequestJwt, encryptionKey : authRequestEncryptionKey, - kid : clientEcdhBearerDid.document.verificationMethod![0].id + kid : clientEcdhBearerDid.document.verificationMethod![0].id, }); expect(authRequestJwe).to.be.a('string'); expect(authRequestJwe.split('.')).to.have.lengthOf(5); @@ -298,7 +312,9 @@ describe('web5 connect', function () { Convert.uint8Array(authRequestEncryptionKey).toBase64Url() ); expect(result.authRequest).to.deep.equal(authRequest); - expect(result.clientEcdhDid.didDocument?.id).to.equal(clientEcdhBearerDid.uri); + expect(result.clientEcdhDid.didDocument?.id).to.equal( + clientEcdhBearerDid.uri + ); clientEcdhDidAsResolvedByWallet = result.clientEcdhDid; }); @@ -380,7 +396,9 @@ 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(Oidc, 'createPermissionGrants') + .resolves(permissionGrants as any); sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); const didJwkStub = sinon.stub(DidJwk, 'create'); @@ -522,7 +540,9 @@ describe('web5 connect', function () { // 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(Oidc, 'createPermissionGrants') + .resolves(permissionGrants as any); sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); @@ -546,14 +566,22 @@ describe('web5 connect', function () { 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 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 ]} }); + .resolves({ + messageCid : '', + reply : { + status : { code: 200, detail: 'OK' }, + entries : [protocolMessage], + }, + }); // call submitAuthResponse await Oidc.submitAuthResponse( @@ -566,11 +594,15 @@ describe('web5 connect', function () { // 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); + 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); + expect(sendRequestSpy.firstCall.args[0].messageType).to.equal( + DwnInterface.ProtocolsConfigure + ); }); it('should configure the protocol if it does not exist', async () => { @@ -579,7 +611,9 @@ describe('web5 connect', function () { // looks for a response of 404, empty entries array or missing entries array - sinon.stub(Oidc, 'createPermissionGrants').resolves(permissionGrants as any); + sinon + .stub(Oidc, 'createPermissionGrants') + .resolves(permissionGrants as any); sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); @@ -600,14 +634,19 @@ describe('web5 connect', function () { authRequest = await Oidc.createAuthRequest(options); // spy send request - const sendRequestSpy = sinon.stub(testHarness.agent, 'sendDwnRequest').resolves({ - messageCid : '', - reply : { status: { code: 202, detail: 'OK' } } - }); + 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: [ ] } }); + .resolves({ + messageCid : '', + reply : { status: { code: 200, detail: 'OK' }, entries: [] }, + }); // call submitAuthResponse await Oidc.submitAuthResponse( @@ -620,19 +659,28 @@ describe('web5 connect', function () { // 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); + 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); + 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' } } }); + processDwnRequestStub.resolves({ + messageCid : '', + reply : { status: { code: 200, detail: 'OK' } }, + }); // call submitAuthResponse await Oidc.submitAuthResponse( @@ -645,16 +693,24 @@ describe('web5 connect', function () { // 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); + 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); + 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(Oidc, 'createPermissionGrants') + .resolves(permissionGrants as any); sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); @@ -675,15 +731,20 @@ describe('web5 connect', function () { 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 : '' - }); + 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' } } }); + .resolves({ + messageCid : '', + reply : { status: { code: 200, detail: 'OK' } }, + }); try { // call submitAuthResponse @@ -697,13 +758,17 @@ describe('web5 connect', function () { expect.fail('should have thrown an error'); } catch (error: any) { - expect(error.message).to.equal('Could not send protocol: Internal Server Error'); + 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(Oidc, 'createPermissionGrants') + .resolves(permissionGrants as any); sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); @@ -727,15 +792,23 @@ describe('web5 connect', function () { 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 : '' - }); + 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 ] } }); + .resolves({ + messageCid : '', + reply : { + status : { code: 200, detail: 'OK' }, + entries : [protocolMessage], + }, + }); try { // call submitAuthResponse @@ -749,14 +822,18 @@ describe('web5 connect', function () { expect.fail('should have thrown an error'); } catch (error: any) { - expect(error.message).to.equal('Could not send protocol: Internal Server Error'); + 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(Oidc, 'createPermissionGrants') + .resolves(permissionGrants as any); sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); @@ -777,15 +854,20 @@ describe('web5 connect', function () { 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 : '' - }); + 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'}, } }); + .resolves({ + messageCid : '', + reply : { status: { code: 500, detail: 'Some Error' } }, + }); try { // call submitAuthResponse @@ -806,7 +888,9 @@ describe('web5 connect', function () { }); 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(Oidc, 'createPermissionGrants') + .resolves(permissionGrants as any); sinon.stub(CryptoUtils, 'randomBytes').returns(encryptionNonce); sinon.stub(DidJwk, 'create').resolves(delegateBearerDid); @@ -841,96 +925,189 @@ describe('web5 connect', function () { 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( + '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 = { + const protocol: DwnProtocolDefinition = { published : true, protocol : 'https://exmaple.org/protocols/social', types : { note: { schema : 'https://example.org/schemas/note', - dataFormats : [ 'application/json', 'text/plain' ], - } + dataFormats : ['application/json', 'text/plain'], + }, }, structure: { - note: {} - } + note: {}, + }, }; - const permissionRequests = WalletConnect.createPermissionRequestForProtocol({ - definition: protocol, permissions: [] - }); + 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; + 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 = { + const protocol: DwnProtocolDefinition = { published : true, protocol : 'https://exmaple.org/protocols/social', types : { note: { schema : 'https://example.org/schemas/note', - dataFormats : [ 'application/json', 'text/plain' ], - } + dataFormats : ['application/json', 'text/plain'], + }, }, structure: { - note: {} - } + note: {}, + }, }; - const permissionRequests = WalletConnect.createPermissionRequestForProtocol({ - definition: protocol, permissions: ['write', 'read'] - }); + 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; + 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 = { + const protocol: DwnProtocolDefinition = { published : true, protocol : 'https://exmaple.org/protocols/social', types : { note: { schema : 'https://example.org/schemas/note', - dataFormats : [ 'application/json', 'text/plain' ], - } + dataFormats : ['application/json', 'text/plain'], + }, }, structure: { - note: {} - } + note: {}, + }, }; - const permissionRequests = WalletConnect.createPermissionRequestForProtocol({ - definition: protocol, permissions: ['write', 'read', 'delete', 'query', 'subscribe', 'configure'] - }); + 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; + 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; }); }); }); diff --git a/packages/api/src/web5.ts b/packages/api/src/web5.ts index cd7e6f1ce..2d3ffd2a3 100644 --- a/packages/api/src/web5.ts +++ b/packages/api/src/web5.ts @@ -32,7 +32,7 @@ export type TechPreviewOptions = { export type DidCreateOptions = { /** Override default dwnEndpoints provided during DID creation. */ dwnEndpoints?: string[]; -} +}; /** Optional overrides that can be provided when calling {@link Web5.connect}. */ export type Web5ConnectOptions = { @@ -126,13 +126,13 @@ export type Web5ConnectOptions = { * If registration fails, the `onFailure` callback will be called with the error. * If registration is successful, the `onSuccess` callback will be called. */ - registration? : { + registration?: { /** Called when all of the DWN registrations are successful */ onSuccess: () => void; /** Called when any of the DWN registrations fail */ onFailure: (error: any) => void; - } -} + }; +}; /** * Represents the result of the Web5 connection process, including the Web5 instance, @@ -241,20 +241,25 @@ export class Web5 { password = 'insecure-static-phrase'; console.warn( '%cSECURITY WARNING:%c ' + - 'You have not set a password, which defaults to a static, guessable value. ' + - 'This significantly compromises the security of your data. ' + - 'Please configure a secure, unique password.', + 'You have not set a password, which defaults to a static, guessable value. ' + + 'This significantly compromises the security of your data. ' + + 'Please configure a secure, unique password.', 'font-weight: bold; color: red;', 'font-weight: normal; color: inherit;' ); } // Use the specified DWN endpoints or the latest TBD hosted DWN - const serviceEndpointNodes = techPreview?.dwnEndpoints ?? didCreateOptions?.dwnEndpoints ?? ['https://dwn.tbddev.org/beta']; + const serviceEndpointNodes = techPreview?.dwnEndpoints ?? + didCreateOptions?.dwnEndpoints ?? ['https://dwn.tbddev.org/beta']; // Initialize, if necessary, and start the agent. if (await userAgent.firstLaunch()) { - recoveryPhrase = await userAgent.initialize({ password, recoveryPhrase, dwnEndpoints: serviceEndpointNodes }); + recoveryPhrase = await userAgent.initialize({ + password, + recoveryPhrase, + dwnEndpoints: serviceEndpointNodes, + }); } await userAgent.start({ password }); @@ -263,8 +268,10 @@ export class Web5 { let identity: BearerIdentity; let connectedProtocols: string[] = []; - const isWalletConnect = walletConnectOptions && !walletConnectOptions.exported; - const isWalletExportedConnect = walletConnectOptions && walletConnectOptions.exported; + const isWalletConnect = + walletConnectOptions && !walletConnectOptions.exported; + const isWalletExportedConnect = + walletConnectOptions && walletConnectOptions.exported; if (connectedIdentity) { // TODO: In the future, implement a way to re-connect an already connected identity and apply additional grants/protocols @@ -280,32 +287,41 @@ export class Web5 { registerSync = true; try { - const { delegatePortableDid, connectedDid, delegateGrants } = await WalletConnect.initClient(walletConnectOptions); + const { delegatePortableDid, connectedDid, delegateGrants } = + await WalletConnect.initClient(walletConnectOptions); // Import the delegated DID as an Identity in the User Agent. // Setting the connectedDID in the metadata applies a relationship between the signer identity and the one it is impersonating. - identity = await userAgent.identity.import({ portableIdentity: { - portableDid : delegatePortableDid, - metadata : { - connectedDid, - name : 'Default', - uri : delegatePortableDid.uri, - tenant : agent.agentDid.uri, - } - }}); + identity = await userAgent.identity.import({ + portableIdentity: { + portableDid : delegatePortableDid, + metadata : { + connectedDid, + name : 'Default', + uri : delegatePortableDid.uri, + tenant : agent.agentDid.uri, + }, + }, + }); // Attempts to process the connected grants to be used by the delegateDID // If the process fails, we want to clean up the identity // the connected grants will return a de-duped array of protocol URIs that are used to register sync for those protocols - connectedProtocols = await this.processConnectedGrants({ agent, delegateDid: delegatePortableDid.uri, grants: delegateGrants }); - } catch (error:any) { + connectedProtocols = await this.processConnectedGrants({ + agent, + delegateDid : delegatePortableDid.uri, + grants : delegateGrants, + }); + } catch (error: any) { // clean up the DID and Identity if import fails and throw // TODO: Implement the ability to purge all of our messages as a tenant await this.cleanUpIdentity({ identity, userAgent }); throw new Error(`Failed to connect to wallet: ${error.message}`); } } else if (isWalletExportedConnect) { - throw new Error('Exported connect will be implemented in a separate PR'); + throw new Error( + 'Exported connect will be implemented in a separate PR' + ); } else { // No connected identity found and no connectOptions provided, use local Identities // Query the Agent's DWN tenant for identity records. @@ -329,23 +345,22 @@ export class Web5 { serviceEndpoint : serviceEndpointNodes, enc : '#enc', sig : '#sig', - } + }, ], verificationMethods: [ { algorithm : 'Ed25519', id : 'sig', - purposes : ['assertionMethod', 'authentication'] + purposes : ['assertionMethod', 'authentication'], }, { algorithm : 'secp256k1', id : 'enc', - purposes : ['keyAgreement'] - } - ] - } + purposes : ['keyAgreement'], + }, + ], + }, }); - } else { // If multiple identities are found, use the first one. // TODO: Implement selecting a connectedDid from multiple identities @@ -356,7 +371,9 @@ export class Web5 { // If the stored identity has a connected DID, use it as the connected DID, otherwise use the identity's DID. connectedDid = identity.metadata.connectedDid ?? identity.did.uri; // If the stored identity has a connected DID, use the identity DID as the delegated DID, otherwise it is undefined. - delegateDid = identity.metadata.connectedDid ? identity.did.uri : undefined; + delegateDid = identity.metadata.connectedDid + ? identity.did.uri + : undefined; if (registration !== undefined) { // If a registration object is passed, we attempt to register the AgentDID and the ConnectedDID with the DWN endpoints provided try { @@ -377,7 +394,7 @@ export class Web5 { // If no failures occurred, call the onSuccess callback registration.onSuccess(); - } catch(error) { + } catch (error) { // for any failure, call the onFailure callback with the error registration.onFailure(error); } @@ -393,8 +410,8 @@ export class Web5 { did : connectedDid, options : { delegateDid, - protocols: connectedProtocols - } + protocols: connectedProtocols, + }, }); if (walletConnectOptions !== undefined) { @@ -405,10 +422,9 @@ export class Web5 { // Enable sync using the specified interval or default. sync ??= '2m'; - userAgent.sync.startSync({ interval: sync }) - .catch((error: any) => { - console.error(`Sync failed: ${error}`); - }); + userAgent.sync.startSync({ interval: sync }).catch((error: any) => { + console.error(`Sync failed: ${error}`); + }); } } @@ -421,9 +437,12 @@ export class Web5 { * Cleans up the DID, Keys and Identity. Primarily used by a failed WalletConnect import. * Does not throw on error, but logs to console. */ - private static async cleanUpIdentity({ identity, userAgent }:{ - identity: BearerIdentity, - userAgent: Web5UserAgent + private static async cleanUpIdentity({ + identity, + userAgent, + }: { + identity: BearerIdentity; + userAgent: Web5UserAgent; }): Promise { try { // Delete the DID and the Associated Keys @@ -432,15 +451,19 @@ export class Web5 { tenant : identity.metadata.tenant, deleteKey : true, }); - } catch(error: any) { - console.error(`Failed to delete DID ${identity.did.uri}: ${error.message}`); + } catch (error: any) { + console.error( + `Failed to delete DID ${identity.did.uri}: ${error.message}` + ); } try { // Delete the Identity await userAgent.identity.delete({ didUri: identity.did.uri }); - } catch(error: any) { - console.error(`Failed to delete Identity ${identity.metadata.name}: ${error.message}`); + } catch (error: any) { + console.error( + `Failed to delete Identity ${identity.metadata.name}: ${error.message}` + ); } } @@ -448,22 +471,34 @@ export class Web5 { * Processes connected grants for a delegate DID. * Stores the grants as the DWN owner to be used later when impersonating the connected DID. */ - static async processConnectedGrants({ grants, agent, delegateDid }: { - grants: DwnDataEncodedRecordsWriteMessage[], - agent: Web5Agent, - delegateDid: string, + static async processConnectedGrants({ + grants, + agent, + delegateDid, + }: { + grants: DwnDataEncodedRecordsWriteMessage[]; + agent: Web5Agent; + delegateDid: string; }): Promise { const connectedProtocols = new Set(); for (const grantMessage of grants) { // use the delegateDid as the connectedDid of the grant as they do not yet support impersonation/delegation - const grant = await PermissionGrant.parse({ connectedDid: delegateDid, agent, message: grantMessage }); + const grant = await PermissionGrant.parse({ + connectedDid : delegateDid, + agent, + message : grantMessage, + }); // store the grant as the owner of the DWN, this will allow the delegateDid to use the grant when impersonating the connectedDid const { status } = await grant.store(true); if (status.code !== 202) { - throw new Error(`AgentDwnApi: Failed to process connected grant: ${status.detail}`); + throw new Error( + `AgentDwnApi: Failed to process connected grant: ${status.detail}` + ); } - const protocol = (grant.scope as DwnMessagesPermissionScope | DwnRecordsPermissionScope).protocol; + const protocol = ( + grant.scope as DwnMessagesPermissionScope | DwnRecordsPermissionScope + ).protocol; if (protocol) { connectedProtocols.add(protocol); } From 4734da26a541cd594a1882193b2feaff8e878186 Mon Sep 17 00:00:00 2001 From: Tim Shamilov Date: Thu, 24 Oct 2024 12:28:04 -0400 Subject: [PATCH 08/11] add changeset --- .changeset/chilled-paws-stare.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .changeset/chilled-paws-stare.md diff --git a/.changeset/chilled-paws-stare.md b/.changeset/chilled-paws-stare.md new file mode 100644 index 000000000..413d35615 --- /dev/null +++ b/.changeset/chilled-paws-stare.md @@ -0,0 +1,29 @@ +--- +"@web5/agent": patch +--- + +security enhancements: use separate DIDs for signing, encryption, verification of web5 connect +feature enhancements: prepare code for the option of exporting the DWA's DID to a wallet + +breaking changes for wallet authors. + +web5 connect's `getAuthRequest()` now returns an object which include both the authRequest and a DID: + +```ts +{ + authRequest: Web5ConnectAuthRequest; + clientEcdhDid: DidResolutionResult; +} +``` + +web5 connect's `submitAuthResponse()` now requires that the did received from `getAuthRequest()` is passed in to the method at position 4: + +```ts +async function submitAuthResponse( + selectedDid: string, + authRequest: Web5ConnectAuthRequest, + randomPin: string, + clientEcdhDid: DidResolutionResult, + agent: Web5Agent +) { ... } +``` From cdea994ced273505e772331babb5b4e8589d37c1 Mon Sep 17 00:00:00 2001 From: Tim Shamilov Date: Thu, 24 Oct 2024 12:28:52 -0400 Subject: [PATCH 09/11] Create weak-shirts-draw.md --- .changeset/weak-shirts-draw.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/weak-shirts-draw.md diff --git a/.changeset/weak-shirts-draw.md b/.changeset/weak-shirts-draw.md new file mode 100644 index 000000000..1853d1bae --- /dev/null +++ b/.changeset/weak-shirts-draw.md @@ -0,0 +1,5 @@ +--- +"@web5/api": patch +--- + +added enhancements and optionality in preparation for export connect From e83dcc8654e469d90d3310acee3b20a5d86cfd6f Mon Sep 17 00:00:00 2001 From: Tim Shamilov Date: Thu, 24 Oct 2024 12:58:45 -0400 Subject: [PATCH 10/11] fix typo --- packages/agent/src/oidc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/agent/src/oidc.ts b/packages/agent/src/oidc.ts index 147602742..7a5af96fa 100644 --- a/packages/agent/src/oidc.ts +++ b/packages/agent/src/oidc.ts @@ -511,7 +511,7 @@ async function decryptWithPin(clientDid: BearerDid, jwe: string, pin: string) { if (!jweProviderEcdhDidKid.didDocument) { throw new Error( - 'Could not resolve provider\'s didd document for shared key derivation' + 'Could not resolve provider\'s DID document for shared key derivation' ); } From 1b396bd23bea6a90827a07936e2c7d918328f228 Mon Sep 17 00:00:00 2001 From: Tim Shamilov Date: Thu, 24 Oct 2024 13:01:34 -0400 Subject: [PATCH 11/11] merge --- packages/agent/src/oidc.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/agent/src/oidc.ts b/packages/agent/src/oidc.ts index 7a5af96fa..36c392d94 100644 --- a/packages/agent/src/oidc.ts +++ b/packages/agent/src/oidc.ts @@ -673,6 +673,7 @@ async function createPermissionGrants( 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 + logger.log(`Creating permission grants for ${scopes.length} scopes given...`); const permissionGrants = await Promise.all( scopes.map((scope) => { // check if the scope is a records permission scope, or a protocol configure scope, if so it should use a delegated permission. @@ -864,7 +865,7 @@ async function submitAuthResponse( delegatePortableDid, }); - // Sign using the signing key + logger.log('Signing auth response object...'); const responseObjectJwt = await Oidc.signJwt({ did : providerSigningDid, data : responseObject, @@ -881,6 +882,7 @@ async function submitAuthResponse( clientEcdhDid?.didDocument ); + logger.log('Encrypting auth response object...'); const encryptedResponse = Oidc.encryptWithPin({ jwt : responseObjectJwt, encryptionKey : sharedKey, @@ -893,6 +895,9 @@ async function submitAuthResponse( state : authRequest.state, }).toString(); + logger.log( + `Sending auth response object to Web5 Connect server: ${authRequest.redirect_uri}` + ); await fetch(authRequest.redirect_uri, { body : formEncodedRequest, method : 'POST',