@@ -13,8 +13,14 @@ import { xchacha20poly1305 } from '@noble/ciphers/chacha';
13
13
import type { ConnectPermissionRequest } from './connect.js' ;
14
14
import { DidDocument , DidJwk , PortableDid , type BearerDid } from '@web5/dids' ;
15
15
import { AgentDwnApi } from './dwn-api.js' ;
16
- import { DwnInterfaceName , DwnMethodName } from '@tbd54566975/dwn-sdk-js' ;
17
- import { DwnInterface } from './types/dwn.js' ;
16
+ import {
17
+ DwnInterfaceName ,
18
+ DwnMethodName ,
19
+ type PermissionScope ,
20
+ type RecordsWriteMessage ,
21
+ } from '@tbd54566975/dwn-sdk-js' ;
22
+ import { DwnInterface , DwnProtocolDefinition } from './types/dwn.js' ;
23
+ import { AgentPermissionsApi } from './permissions-api.js' ;
18
24
19
25
/**
20
26
* Sent to an OIDC server to authorize a client. Allows clients
@@ -157,10 +163,14 @@ export type SIOPv2AuthResponse = {
157
163
158
164
/** An auth response that is compatible with both Web5 Connect and (hopefully, WIP) OIDC SIOPv2 */
159
165
export type Web5ConnectAuthResponse = {
160
- delegateGrants : any [ ] ;
161
- delegateDid : PortableDid ;
166
+ delegateGrants : DelegateGrant [ ] ;
167
+ delegatePortableDid : PortableDid ;
162
168
} & SIOPv2AuthResponse ;
163
169
170
+ export type DelegateGrant = ( RecordsWriteMessage & {
171
+ encodedData : string ;
172
+ } )
173
+
164
174
/** Represents the different OIDC endpoint types.
165
175
* 1. `pushedAuthorizationRequest`: client sends {@link PushedAuthRequest} receives {@link PushedAuthResponse}
166
176
* 2. `authorize`: provider gets the {@link Web5ConnectAuthRequest} JWT that was stored by the PAR
@@ -240,10 +250,7 @@ async function generateCodeChallenge() {
240
250
async function createAuthRequest (
241
251
options : RequireOnly <
242
252
Web5ConnectAuthRequest ,
243
- | 'client_id'
244
- | 'scope'
245
- | 'redirect_uri'
246
- | 'permissionRequests'
253
+ 'client_id' | 'scope' | 'redirect_uri' | 'permissionRequests'
247
254
>
248
255
) {
249
256
// Generate a random state value to associate the authorization request with the response.
@@ -306,7 +313,7 @@ async function encryptAuthRequest({
306
313
async function createResponseObject (
307
314
options : RequireOnly <
308
315
Web5ConnectAuthResponse ,
309
- 'iss' | 'sub' | 'aud' | 'delegateGrants' | 'delegateDid '
316
+ 'iss' | 'sub' | 'aud' | 'delegateGrants' | 'delegatePortableDid '
310
317
>
311
318
) {
312
319
const currentTimeInSeconds = Math . floor ( Date . now ( ) / 1000 ) ;
@@ -476,12 +483,12 @@ async function decryptAuthResponse(
476
483
477
484
// get the delegatedid public key from the header
478
485
const header = Convert . base64Url ( protectedHeaderB64U ) . toObject ( ) as Jwk ;
479
- const delegateDid = await DidJwk . resolve ( header . kid ! . split ( '#' ) [ 0 ] ) ;
486
+ const delegateResolvedDid = await DidJwk . resolve ( header . kid ! . split ( '#' ) [ 0 ] ) ;
480
487
481
488
// derive ECDH shared key using the provider's public key and our clientDid private key
482
489
const sharedKey = await Oidc . deriveSharedKey (
483
490
clientDid ,
484
- delegateDid . didDocument !
491
+ delegateResolvedDid . didDocument !
485
492
) ;
486
493
487
494
// add the pin to the AAD
@@ -606,39 +613,117 @@ function encryptAuthResponse({
606
613
* Creates the permission grants that assign to the selectedDid the level of
607
614
* permissions that the web app requested in the {@link Web5ConnectAuthRequest}
608
615
*/
609
- export async function createPermissionGrants (
616
+ async function createPermissionGrants (
610
617
selectedDid : string ,
611
- delegateDid : BearerDid ,
612
- dwn : AgentDwnApi
618
+ delegateBearerDid : BearerDid ,
619
+ dwn : AgentDwnApi ,
620
+ permissionsApi : AgentPermissionsApi ,
621
+ scopes : PermissionScope [ ] ,
622
+ protocolUri : string
613
623
) {
614
- // TODO: remove mock after adding functionality: https://github.com/TBD54566975/web5-js/issues/827
615
- const permissionRequestData = {
616
- description :
617
- 'The app is asking to Records Write to http://profile-protocol.xyz' ,
618
- scope : {
619
- interface : DwnInterfaceName . Records ,
620
- method : DwnMethodName . Write ,
621
- protocol : 'http://profile-protocol.xyz' ,
622
- } ,
623
- } ;
624
+ const permissionGrants = await Promise . all (
625
+ scopes . map ( ( scope ) =>
626
+ permissionsApi . createGrant ( {
627
+ grantedTo : delegateBearerDid . uri ,
628
+ scope,
629
+ dateExpires : '2040-06-25T16:09:16.693356Z' ,
630
+ author : selectedDid ,
631
+ } )
632
+ )
633
+ ) ;
634
+
635
+ // Grant Messages Query and Messages Read for sync to work
636
+ permissionGrants . push (
637
+ await permissionsApi . createGrant ( {
638
+ grantedTo : delegateBearerDid . uri ,
639
+ scope : {
640
+ interface : DwnInterfaceName . Messages ,
641
+ method : DwnMethodName . Query ,
642
+ protocol : protocolUri ,
643
+ } ,
644
+ dateExpires : '2040-06-25T16:09:16.693356Z' ,
645
+ author : selectedDid ,
646
+ } )
647
+ ) ;
648
+ permissionGrants . push (
649
+ await permissionsApi . createGrant ( {
650
+ grantedTo : delegateBearerDid . uri ,
651
+ scope : {
652
+ interface : DwnInterfaceName . Messages ,
653
+ method : DwnMethodName . Read ,
654
+ protocol : protocolUri ,
655
+ } ,
656
+ dateExpires : '2040-06-25T16:09:16.693356Z' ,
657
+ author : selectedDid ,
658
+ } )
659
+ ) ;
660
+
661
+ const messagePromises = permissionGrants . map ( async ( grant ) => {
662
+ // Quirk: we have to pull out encodedData out of the message the schema validator doesnt want it there
663
+ const { encodedData, ...rawMessage } = grant . message ;
664
+
665
+ const data = Convert . base64Url ( encodedData ) . toUint8Array ( ) ;
666
+ const params = {
667
+ author : selectedDid ,
668
+ target : selectedDid ,
669
+ messageType : DwnInterface . RecordsWrite ,
670
+ dataStream : new Blob ( [ data ] ) ,
671
+ rawMessage,
672
+ } ;
673
+
674
+ const message = await dwn . processRequest ( params ) ;
675
+ const sent = await dwn . sendRequest ( params ) ;
676
+
677
+ // TODO: cleanup all grants if one fails by deleting them from the DWN: https://github.com/TBD54566975/web5-js/issues/849
678
+ if ( message . reply . status . code !== 202 ) {
679
+ throw new Error (
680
+ `Could not process the message. Error details: ${ message . reply . status . detail } `
681
+ ) ;
682
+ }
683
+ if ( sent . reply . status . code !== 202 ) {
684
+ throw new Error (
685
+ `Could not send the message. Error details: ${ message . reply . status . detail } `
686
+ ) ;
687
+ }
688
+
689
+ return grant . message ;
690
+ } ) ;
691
+
692
+ const messages = await Promise . all ( messagePromises ) ;
624
693
625
- // TODO: remove mock after adding functionality: https://github.com/TBD54566975/web5-js/issues/827
626
- const message = await dwn . processRequest ( {
694
+ return messages ;
695
+ }
696
+
697
+ /**
698
+ * Installs the protocols required by the Client on the Provider
699
+ * if they don't already exist.
700
+ */
701
+ async function prepareProtocols (
702
+ selectedDid : string ,
703
+ agentDwnApi : AgentDwnApi ,
704
+ protocolDefinition : DwnProtocolDefinition
705
+ ) {
706
+ const queryMessage = await agentDwnApi . processRequest ( {
627
707
author : selectedDid ,
708
+ messageType : DwnInterface . ProtocolsQuery ,
628
709
target : selectedDid ,
629
- messageType : DwnInterface . RecordsWrite ,
630
- messageParams : {
631
- recipient : delegateDid . uri ,
632
- protocolPath : 'grant' ,
633
- protocol : ' https://tbd.website/dwn/permissions' ,
634
- dataFormat : 'application/json' ,
635
- data : Convert . object ( permissionRequestData ) . toUint8Array ( ) ,
636
- } ,
637
- // todo: is it data or datastream?
638
- // dataStream: await Convert.object(permissionRequestData).toBlobAsync(),
710
+ messageParams : { filter : { protocol : protocolDefinition . protocol } } ,
639
711
} ) ;
640
712
641
- return [ message ] ;
713
+ if ( queryMessage . reply . status . code === 404 ) {
714
+ const configureMessage = await agentDwnApi . processRequest ( {
715
+ author : selectedDid ,
716
+ messageType : DwnInterface . ProtocolsConfigure ,
717
+ target : selectedDid ,
718
+ messageParams : { definition : protocolDefinition } ,
719
+ } ) ;
720
+
721
+ if ( configureMessage . reply . status . code !== 202 ) {
722
+ throw new Error ( `Could not install protocol: ${ configureMessage . reply . status . detail } ` ) ;
723
+ }
724
+ } else if ( queryMessage . reply . status . code !== 200 ) {
725
+ throw new Error ( `Could not fetch protcol: ${ queryMessage . reply . status . detail } ` ) ;
726
+ }
642
727
}
643
728
644
729
/**
@@ -654,46 +739,59 @@ async function submitAuthResponse(
654
739
selectedDid : string ,
655
740
authRequest : Web5ConnectAuthRequest ,
656
741
randomPin : string ,
657
- dwn : AgentDwnApi
742
+ agentDwnApi : AgentDwnApi ,
743
+ agentPermissionsApi : AgentPermissionsApi
658
744
) {
659
- const delegateDid = await DidJwk . create ( ) ;
660
- const delegateDidPortable = await delegateDid . export ( ) ;
745
+ const delegateBearerDid = await DidJwk . create ( ) ;
746
+ const delegatePortableDid = await delegateBearerDid . export ( ) ;
747
+
748
+ const delegateGrantPromises = authRequest . permissionRequests . map ( async ( permissionRequest ) => {
749
+ await prepareProtocols ( selectedDid , agentDwnApi , permissionRequest . protocolDefinition ) ;
750
+
751
+ // TODO: validate to make sure the scopes and definition are assigned to the same protocol
752
+ const permissionGrants = await Oidc . createPermissionGrants (
753
+ selectedDid ,
754
+ delegateBearerDid ,
755
+ agentDwnApi ,
756
+ agentPermissionsApi ,
757
+ permissionRequest . permissionScopes ,
758
+ permissionRequest . protocolDefinition . protocol
759
+ ) ;
661
760
662
- const permissionGrants = await Oidc . createPermissionGrants (
663
- selectedDid ,
664
- delegateDid ,
665
- dwn
666
- ) ;
761
+ return permissionGrants ;
762
+ } ) ;
763
+
764
+ const delegateGrants = ( await Promise . all ( delegateGrantPromises ) ) . flat ( ) ;
667
765
668
766
const responseObject = await Oidc . createResponseObject ( {
669
767
//* the IDP's did that was selected to be connected
670
- iss : selectedDid ,
768
+ iss : selectedDid ,
671
769
//* the client's new identity
672
- sub : delegateDid . uri ,
770
+ sub : delegateBearerDid . uri ,
673
771
//* the client's temporary ephemeral did used for connect
674
- aud : authRequest . client_id ,
772
+ aud : authRequest . client_id ,
675
773
//* the nonce of the original auth request
676
- nonce : authRequest . nonce ,
677
- delegateGrants : permissionGrants ,
678
- delegateDid : delegateDidPortable ,
774
+ nonce : authRequest . nonce ,
775
+ delegateGrants,
776
+ delegatePortableDid ,
679
777
} ) ;
680
778
681
779
// Sign the Response Object using the ephemeral DID's signing key.
682
780
const responseObjectJwt = await Oidc . signJwt ( {
683
- did : delegateDid ,
781
+ did : delegateBearerDid ,
684
782
data : responseObject ,
685
783
} ) ;
686
784
const clientDid = await DidJwk . resolve ( authRequest . client_id ) ;
687
785
688
786
const sharedKey = await Oidc . deriveSharedKey (
689
- delegateDid ,
787
+ delegateBearerDid ,
690
788
clientDid ?. didDocument !
691
789
) ;
692
790
693
791
const encryptedResponse = Oidc . encryptAuthResponse ( {
694
792
jwt : responseObjectJwt ! ,
695
793
encryptionKey : sharedKey ,
696
- delegateDidKeyId : delegateDid . document . verificationMethod ! [ 0 ] . id ,
794
+ delegateDidKeyId : delegateBearerDid . document . verificationMethod ! [ 0 ] . id ,
697
795
randomPin,
698
796
} ) ;
699
797
0 commit comments