diff --git a/cmd/wallet-sdk-gomobile/docs/usage.md b/cmd/wallet-sdk-gomobile/docs/usage.md index cdb1396d..f1b33d2c 100644 --- a/cmd/wallet-sdk-gomobile/docs/usage.md +++ b/cmd/wallet-sdk-gomobile/docs/usage.md @@ -1465,6 +1465,18 @@ val preferredVC = savedCredentials.atIndex(0) interaction.presentCredentialUnsafe(preferredVC) ``` +###### Read scope and add custom scope claims + +```kotlin + +val scope = interaction.scope() + +interaction.presentCredentialWithOpts(selectedCredentials, PresentCredentialOpts() + .addScopeClaim("registration", """{"email", "test@example.com"}""")) + +``` + + #### Swift (iOS) ```swift @@ -1514,6 +1526,18 @@ let preferredVC = savedCredentials.atIndex(0) interaction.presentCredentialUnsafe(preferredVC) ``` +###### Read scope and add custom scope claims + +```swift + +val scope = interaction.scope() + +let opts = Openid4vpNewPresentCredentialOpts()?.addScopeClaim("registration", #"{"email", "test@example.com"}"#) + +try interaction.presentCredentialWithOpts(selectedCredentials, opts) + +``` + ### Error Codes & Troubleshooting Tips | Error | Possible Reasons | diff --git a/cmd/wallet-sdk-gomobile/go.mod b/cmd/wallet-sdk-gomobile/go.mod index f249b062..e5e62b07 100644 --- a/cmd/wallet-sdk-gomobile/go.mod +++ b/cmd/wallet-sdk-gomobile/go.mod @@ -64,11 +64,11 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - golang.org/x/crypto v0.13.0 // indirect + golang.org/x/crypto v0.14.0 // indirect golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect - golang.org/x/net v0.15.0 // indirect + golang.org/x/net v0.16.0 // indirect golang.org/x/oauth2 v0.7.0 // indirect - golang.org/x/sys v0.12.0 // indirect + golang.org/x/sys v0.13.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/cmd/wallet-sdk-gomobile/go.sum b/cmd/wallet-sdk-gomobile/go.sum index 38f517c0..112650dc 100644 --- a/cmd/wallet-sdk-gomobile/go.sum +++ b/cmd/wallet-sdk-gomobile/go.sum @@ -162,15 +162,15 @@ golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= +golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -178,8 +178,8 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/cmd/wallet-sdk-gomobile/openid4vp/interaction.go b/cmd/wallet-sdk-gomobile/openid4vp/interaction.go index 2cd383fa..3b2ab213 100644 --- a/cmd/wallet-sdk-gomobile/openid4vp/interaction.go +++ b/cmd/wallet-sdk-gomobile/openid4vp/interaction.go @@ -30,8 +30,9 @@ import ( type goAPIOpenID4VP interface { GetQuery() *presexch.PresentationDefinition - PresentCredential(credentials []*afgoverifiable.Credential) error - PresentCredentialUnsafe(credential *afgoverifiable.Credential) error + Scope() []string + PresentCredential(credentials []*afgoverifiable.Credential, customClaims openid4vp.CustomClaims) error + PresentCredentialUnsafe(credential *afgoverifiable.Credential, customClaims openid4vp.CustomClaims) error VerifierDisplayData() *openid4vp.VerifierDisplayData } @@ -134,6 +135,11 @@ func (o *Interaction) GetQuery() ([]byte, error) { return pdBytes, nil } +// Scope returns vp integration scope. +func (o *Interaction) Scope() *Scope { + return NewScope(o.goAPIOpenID4VP.Scope()) +} + // VerifierDisplayData returns display information about verifier. func (o *Interaction) VerifierDisplayData() *VerifierDisplayData { displayData := o.goAPIOpenID4VP.VerifierDisplayData() @@ -148,7 +154,25 @@ func (o *Interaction) PresentCredential(credentials *verifiable.CredentialsArray return wrapper.ToMobileErrorWithTrace(err, o.oTel) } - return wrapper.ToMobileErrorWithTrace(o.goAPIOpenID4VP.PresentCredential(vcs), o.oTel) + return wrapper.ToMobileErrorWithTrace(o.goAPIOpenID4VP.PresentCredential(vcs, openid4vp.CustomClaims{}), o.oTel) +} + +// PresentCredentialWithOpts presents credentials to redirect uri from request object. +func (o *Interaction) PresentCredentialWithOpts( + credentials *verifiable.CredentialsArray, + opts *PresentCredentialOpts, +) error { + vcs, err := unwrapVCs(credentials) + if err != nil { + return wrapper.ToMobileErrorWithTrace(err, o.oTel) + } + + claims, err := getCustomClaims(opts) + if err != nil { + return err + } + + return wrapper.ToMobileErrorWithTrace(o.goAPIOpenID4VP.PresentCredential(vcs, claims), o.oTel) } // PresentCredentialUnsafe presents a single credential to redirect uri from @@ -159,7 +183,8 @@ func (o *Interaction) PresentCredential(credentials *verifiable.CredentialsArray // provided credential, at least in terms of issuer fields, and subject data // fields. func (o *Interaction) PresentCredentialUnsafe(credential *verifiable.Credential) error { - return wrapper.ToMobileErrorWithTrace(o.goAPIOpenID4VP.PresentCredentialUnsafe(credential.VC), o.oTel) + return wrapper.ToMobileErrorWithTrace(o.goAPIOpenID4VP.PresentCredentialUnsafe(credential.VC, + openid4vp.CustomClaims{}), o.oTel) } // OTelTraceID returns open telemetry trace id. @@ -222,3 +247,26 @@ func unwrapVCs(vcs *verifiable.CredentialsArray) ([]*afgoverifiable.Credential, return credentials, nil } + +func getCustomClaims(opts *PresentCredentialOpts) (openid4vp.CustomClaims, error) { + if opts == nil { + return openid4vp.CustomClaims{}, nil + } + + claims := openid4vp.CustomClaims{ + ScopeClaims: map[string]interface{}{}, + } + + for key, value := range opts.scopeClaims { + var jsonValue interface{} + + err := json.Unmarshal([]byte(value), &jsonValue) + if err != nil { + return openid4vp.CustomClaims{}, fmt.Errorf("fail to parse %q claim json: %w", key, err) + } + + claims.ScopeClaims[key] = jsonValue + } + + return claims, nil +} diff --git a/cmd/wallet-sdk-gomobile/openid4vp/interaction_test.go b/cmd/wallet-sdk-gomobile/openid4vp/interaction_test.go index d3e1dc60..14119b9f 100644 --- a/cmd/wallet-sdk-gomobile/openid4vp/interaction_test.go +++ b/cmd/wallet-sdk-gomobile/openid4vp/interaction_test.go @@ -76,6 +76,8 @@ func TestNewInteraction(t *testing.T) { instance, err := NewInteraction(requiredArgs, opts) require.NoError(t, err) require.NotNil(t, instance) + require.Equal(t, 1, instance.Scope().Length()) + require.Equal(t, "openid", instance.Scope().AtIndex(0)) }) t.Run("All other options invoked", func(t *testing.T) { resolver, err := gomobdid.NewResolver(gomobdid.NewResolverOpts()) @@ -182,6 +184,21 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { require.NoError(t, err) }) + t.Run("Success With Opts", func(t *testing.T) { + instance := makeInteraction() + + err := instance.PresentCredentialWithOpts(credentials, NewPresentCredentialOpts(). + AddScopeClaim("claim1", `{"key" : "val"}`)) + require.NoError(t, err) + }) + + t.Run("Success With nil Opts", func(t *testing.T) { + instance := makeInteraction() + + err := instance.PresentCredentialWithOpts(credentials, nil) + require.NoError(t, err) + }) + t.Run("Success Unsafe", func(t *testing.T) { instance := makeInteraction() @@ -200,6 +217,26 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { require.Contains(t, err.Error(), "present credentials failed") }) + t.Run("Present credentials with opts failed", func(t *testing.T) { + instance := makeInteraction() + + instance.goAPIOpenID4VP = &mockGoAPIInteraction{ + PresentCredentialErr: errors.New("present credentials failed"), + } + + err := instance.PresentCredentialWithOpts(credentials, NewPresentCredentialOpts(). + AddScopeClaim("claim1", `{"key" : "val"}`)) + require.Contains(t, err.Error(), "present credentials failed") + }) + + t.Run("Present credentials with invalid scope value", func(t *testing.T) { + instance := makeInteraction() + + err := instance.PresentCredentialWithOpts(credentials, NewPresentCredentialOpts(). + AddScopeClaim("claim1", `"key" : "val"`)) + require.ErrorContains(t, err, `fail to parse "claim1" claim json`) + }) + t.Run("Present credentials unsafe failed", func(t *testing.T) { instance := makeInteraction() @@ -229,6 +266,20 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { }) } +func TestGetCustomClaims(t *testing.T) { + t.Run("Success", func(t *testing.T) { + claims, err := getCustomClaims(NewPresentCredentialOpts(). + AddScopeClaim("claim1", `{"key" : "val"}`)) + require.NoError(t, err) + require.Equal(t, map[string]interface{}{ + "claim1": map[string]interface{}{ + "key": "val", + }, + }, + claims.ScopeClaims) + }) +} + func TestInteraction_VerifierDisplayData(t *testing.T) { t.Run("Success", func(t *testing.T) { instance := &Interaction{ @@ -289,6 +340,7 @@ func (c *mockCrypto) Verify([]byte, []byte, string) error { type mockGoAPIInteraction struct { GetQueryResult *presexch.PresentationDefinition + ScopeResult []string PresentCredentialErr error PresentCredentialUnsafeErr error VerifierDisplayDataRes *openid4vp.VerifierDisplayData @@ -298,11 +350,15 @@ func (o *mockGoAPIInteraction) GetQuery() *presexch.PresentationDefinition { return o.GetQueryResult } -func (o *mockGoAPIInteraction) PresentCredential([]*afgoverifiable.Credential) error { +func (o *mockGoAPIInteraction) Scope() []string { + return o.ScopeResult +} + +func (o *mockGoAPIInteraction) PresentCredential([]*afgoverifiable.Credential, openid4vp.CustomClaims) error { return o.PresentCredentialErr } -func (o *mockGoAPIInteraction) PresentCredentialUnsafe(*afgoverifiable.Credential) error { +func (o *mockGoAPIInteraction) PresentCredentialUnsafe(*afgoverifiable.Credential, openid4vp.CustomClaims) error { return o.PresentCredentialUnsafeErr } diff --git a/cmd/wallet-sdk-gomobile/openid4vp/opts.go b/cmd/wallet-sdk-gomobile/openid4vp/opts.go index b91f1895..b39ebabb 100644 --- a/cmd/wallet-sdk-gomobile/openid4vp/opts.go +++ b/cmd/wallet-sdk-gomobile/openid4vp/opts.go @@ -100,3 +100,24 @@ func (o *Opts) EnableAddingDIProofs(kms *localkms.KMS) *Opts { return o } + +// NewPresentCredentialOpts returns a new PresentCredentialOpts object. +func NewPresentCredentialOpts() *PresentCredentialOpts { + return &PresentCredentialOpts{} +} + +// PresentCredentialOpts contains options for present credential operation. +type PresentCredentialOpts struct { + scopeClaims map[string]string +} + +// AddScopeClaim adds scope claim with given name. +func (o *PresentCredentialOpts) AddScopeClaim(claimName, claimJSON string) *PresentCredentialOpts { + if o.scopeClaims == nil { + o.scopeClaims = map[string]string{} + } + + o.scopeClaims[claimName] = claimJSON + + return o +} diff --git a/cmd/wallet-sdk-gomobile/openid4vp/scope.go b/cmd/wallet-sdk-gomobile/openid4vp/scope.go new file mode 100644 index 00000000..87f5471c --- /dev/null +++ b/cmd/wallet-sdk-gomobile/openid4vp/scope.go @@ -0,0 +1,28 @@ +/* +Copyright Gen Digital Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package openid4vp + +// Scope represents an array of scope strings. +// Since arrays and slices are not compatible with gomobile, this type acts as a wrapper around a Go array of strings. +type Scope struct { + scope []string +} + +// NewScope creates Scope object from array of scopes. +func NewScope(scope []string) *Scope { + return &Scope{scope: scope} +} + +// Length returns the number scopes. +func (s *Scope) Length() int { + return len(s.scope) +} + +// AtIndex returns scope by index. +func (s *Scope) AtIndex(index int) string { + return s.scope[index] +} diff --git a/cmd/wallet-sdk-gomobile/openid4vp/scope_test.go b/cmd/wallet-sdk-gomobile/openid4vp/scope_test.go new file mode 100644 index 00000000..7b031c15 --- /dev/null +++ b/cmd/wallet-sdk-gomobile/openid4vp/scope_test.go @@ -0,0 +1,23 @@ +/* +Copyright Gen Digital Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package openid4vp_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/trustbloc/wallet-sdk/cmd/wallet-sdk-gomobile/openid4vp" +) + +func TestNewScope(t *testing.T) { + scope := openid4vp.NewScope([]string{"scope1", "scope2"}) + + require.Equal(t, 2, scope.Length()) + require.Equal(t, "scope1", scope.AtIndex(0)) + require.Equal(t, "scope2", scope.AtIndex(1)) +} diff --git a/demo/app/android/app/src/main/kotlin/walletsdk/openid4vp/OpenID4VP.kt b/demo/app/android/app/src/main/kotlin/walletsdk/openid4vp/OpenID4VP.kt index d3070bf2..9c7cbe98 100644 --- a/demo/app/android/app/src/main/kotlin/walletsdk/openid4vp/OpenID4VP.kt +++ b/demo/app/android/app/src/main/kotlin/walletsdk/openid4vp/OpenID4VP.kt @@ -7,11 +7,8 @@ SPDX-License-Identifier: Apache-2.0 package walletsdk.openid4vp import dev.trustbloc.wallet.sdk.api.* -import dev.trustbloc.wallet.sdk.openid4vp.Interaction import dev.trustbloc.wallet.sdk.credential.* -import dev.trustbloc.wallet.sdk.openid4vp.Opts -import dev.trustbloc.wallet.sdk.openid4vp.Args -import dev.trustbloc.wallet.sdk.openid4vp.VerifierDisplayData +import dev.trustbloc.wallet.sdk.openid4vp.* import dev.trustbloc.wallet.sdk.otel.Otel import dev.trustbloc.wallet.sdk.verifiable.CredentialsArray import dev.trustbloc.wallet.sdk.stderr.MetricsLogger diff --git a/demo/app/ios/Runner/OpenID4VP.swift b/demo/app/ios/Runner/OpenID4VP.swift index 118bc4b1..69823c8f 100644 --- a/demo/app/ios/Runner/OpenID4VP.swift +++ b/demo/app/ios/Runner/OpenID4VP.swift @@ -41,7 +41,7 @@ public class OpenID4VP { opts!.add(trace!.traceHeader()) let interaction = Openid4vpNewInteraction(args, opts, nil) - + vpQueryContent = try interaction!.getQuery() initiatedInteraction = interaction } @@ -67,7 +67,7 @@ public class OpenID4VP { } // let verifiablePresentation = try CredentialNewInquirer(documentLoader)!.query(vpQueryContent, credentials: selectedCredentials) - + try initiatedInteraction.presentCredential(selectedCredentials) } diff --git a/pkg/openid4vp/openid4vp.go b/pkg/openid4vp/openid4vp.go index a2061d97..e9bc4879 100644 --- a/pkg/openid4vp/openid4vp.go +++ b/pkg/openid4vp/openid4vp.go @@ -131,6 +131,11 @@ func (o *Interaction) GetQuery() *presexch.PresentationDefinition { return o.requestObject.Claims.VPToken.PresentationDefinition } +// Scope returns vp integration scope. +func (o *Interaction) Scope() []string { + return strings.Split(o.requestObject.Scope, "+") +} + // VerifierDisplayData returns display information about verifier. func (o *Interaction) VerifierDisplayData() *VerifierDisplayData { return &VerifierDisplayData{ @@ -147,18 +152,20 @@ type presentOpts struct { } // PresentCredential presents credentials to redirect uri from request object. -func (o *Interaction) PresentCredential(credentials []*verifiable.Credential) error { +func (o *Interaction) PresentCredential(credentials []*verifiable.Credential, customClaims CustomClaims) error { return o.presentCredentials( credentials, + customClaims, &presentOpts{signer: o.signer}, ) } // PresentCredentialUnsafe presents a single credential to redirect uri from request object. // This skips presentation definition constraint validation. -func (o *Interaction) PresentCredentialUnsafe(credential *verifiable.Credential) error { +func (o *Interaction) PresentCredentialUnsafe(credential *verifiable.Credential, customClaims CustomClaims) error { return o.presentCredentials( []*verifiable.Credential{credential}, + customClaims, &presentOpts{ ignoreConstraints: true, }, @@ -166,12 +173,17 @@ func (o *Interaction) PresentCredentialUnsafe(credential *verifiable.Credential) } // PresentCredential presents credentials to redirect uri from request object. -func (o *Interaction) presentCredentials(credentials []*verifiable.Credential, opts *presentOpts) error { +func (o *Interaction) presentCredentials( + credentials []*verifiable.Credential, + customClaims CustomClaims, + opts *presentOpts, +) error { timeStartPresentCredential := time.Now() response, err := createAuthorizedResponse( credentials, o.requestObject, + customClaims, o.didResolver, o.crypto, o.documentLoader, @@ -293,6 +305,7 @@ func verifyTokenSignature(rawJwt string, claims interface{}, proofChecker jwt.Pr func createAuthorizedResponse( credentials []*verifiable.Credential, requestObject *requestObject, + customClaims CustomClaims, didResolver api.DIDResolver, crypto api.Crypto, documentLoader ld.DocumentLoader, @@ -302,9 +315,11 @@ func createAuthorizedResponse( case 0: return nil, fmt.Errorf("expected at least one credential to present to verifier") case 1: - return createAuthorizedResponseOneCred(credentials[0], requestObject, didResolver, crypto, documentLoader, opts) + return createAuthorizedResponseOneCred(credentials[0], requestObject, customClaims, + didResolver, crypto, documentLoader, opts) default: - return createAuthorizedResponseMultiCred(credentials, requestObject, didResolver, crypto, documentLoader, + return createAuthorizedResponseMultiCred(credentials, requestObject, customClaims, + didResolver, crypto, documentLoader, opts.signer) } } @@ -312,6 +327,7 @@ func createAuthorizedResponse( func createAuthorizedResponseOneCred( //nolint:funlen,gocyclo // Unable to decompose without a major reworking credential *verifiable.Credential, requestObject *requestObject, + customClaims CustomClaims, didResolver api.DIDResolver, crypto api.Crypto, documentLoader ld.DocumentLoader, @@ -380,7 +396,7 @@ func createAuthorizedResponseOneCred( //nolint:funlen,gocyclo // Unable to decom presentation.CustomFields["presentation_submission"] = nil - idTokenJWS, err := createIDToken(requestObject, presentationSubmission, did, jwtSigner) + idTokenJWS, err := createIDToken(requestObject, presentationSubmission, did, customClaims, jwtSigner) if err != nil { return nil, err } @@ -407,6 +423,7 @@ func createAuthorizedResponseOneCred( //nolint:funlen,gocyclo // Unable to decom func createAuthorizedResponseMultiCred( //nolint:funlen,gocyclo // Unable to decompose without a major reworking credentials []*verifiable.Credential, requestObject *requestObject, + customClaims CustomClaims, didResolver api.DIDResolver, crypto api.Crypto, documentLoader ld.DocumentLoader, @@ -497,7 +514,8 @@ func createAuthorizedResponseMultiCred( //nolint:funlen,gocyclo // Unable to dec return nil, err } - idTokenJWS, err := createIDToken(requestObject, submission, idTokenSigningDID, signers[idTokenSigningDID]) + idTokenJWS, err := createIDToken(requestObject, submission, idTokenSigningDID, + customClaims, signers[idTokenSigningDID]) if err != nil { return nil, err } @@ -540,12 +558,14 @@ func createIDToken( req *requestObject, submission interface{}, signingDID string, + customClaims CustomClaims, signer api.JWTSigner, ) (string, error) { idToken := &idTokenClaims{ VPToken: idTokenVPToken{ PresentationSubmission: submission, }, + Scope: customClaims.ScopeClaims, Nonce: req.Nonce, Exp: time.Now().Unix() + tokenLiveTimeSec, Iss: "https://self-issued.me/v2/openid-vc", diff --git a/pkg/openid4vp/openid4vp_test.go b/pkg/openid4vp/openid4vp_test.go index d584520e..c59dd51e 100644 --- a/pkg/openid4vp/openid4vp_test.go +++ b/pkg/openid4vp/openid4vp_test.go @@ -260,7 +260,8 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { require.Equal(t, "", displayData.Purpose) require.Equal(t, "", displayData.LogoURI) - err = interaction.PresentCredential(credentials) + err = interaction.PresentCredential(credentials, CustomClaims{}) + require.NoError(t, err) expectedState := "636df28459a07d50cc4b657e" @@ -343,14 +344,36 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { query := interaction.GetQuery() require.NotNil(t, query) - err = interaction.PresentCredentialUnsafe(singleCred[0]) + err = interaction.PresentCredentialUnsafe(singleCred[0], CustomClaims{}) require.NoError(t, err) }) + t.Run("Check custom claims", func(t *testing.T) { + response, err := createAuthorizedResponse( + singleCred, + mockRequestObject, + CustomClaims{ScopeClaims: map[string]interface{}{ + "customClaimName": "customClaimValue", + }}, + &didResolverMock{ResolveValue: mockDoc}, + &cryptoMock{SignVal: []byte(testSignature)}, + lddl, + nil, + ) + require.NoError(t, err) + + body, err := base64.RawURLEncoding.DecodeString(strings.Split(response.IDTokenJWS, ".")[1]) + require.NoError(t, err) + + require.Contains(t, string(body), "customClaimName") + require.Contains(t, string(body), "customClaimValue") + }) + t.Run("Check nonce", func(t *testing.T) { response, err := createAuthorizedResponse( singleCred, mockRequestObject, + CustomClaims{}, &didResolverMock{ResolveValue: mockDoc}, &cryptoMock{SignVal: []byte(testSignature)}, lddl, @@ -407,6 +430,7 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { response, err := createAuthorizedResponse( singleCred, req, + CustomClaims{}, &didResolverMock{ResolveValue: mockDoc}, &cryptoMock{SignVal: []byte(testSignature)}, lddl, @@ -420,6 +444,7 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { response, err = createAuthorizedResponse( singleCred, req, + CustomClaims{}, &didResolverMock{ResolveValue: mockDoc}, &cryptoMock{SignVal: []byte(testSignature)}, lddl, @@ -448,7 +473,7 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { query := interaction.GetQuery() require.NotNil(t, query) - err = interaction.PresentCredential(nil) + err = interaction.PresentCredential(nil, CustomClaims{}) require.Error(t, err) require.Contains(t, err.Error(), "expected at least one credential") }) @@ -494,6 +519,7 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { _, err := createAuthorizedResponse( vcs, mockRequestObject, + CustomClaims{}, &didResolverMock{ResolveValue: mockDoc}, &cryptoMock{}, lddl, @@ -509,15 +535,16 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { t.Run("fail to resolve signing DID", func(t *testing.T) { expectErr := errors.New("resolve failed") - _, err := createAuthorizedResponse(singleCred, mockRequestObject, + _, err := createAuthorizedResponse(singleCred, mockRequestObject, CustomClaims{}, &didResolverMock{ResolveErr: expectErr}, &cryptoMock{}, lddl, &presentOpts{}) require.ErrorIs(t, err, expectErr) }) t.Run("signing DID has no signing key", func(t *testing.T) { - _, err := createAuthorizedResponse(singleCred, mockRequestObject, &didResolverMock{ResolveValue: &did.DocResolution{ - DIDDocument: &did.Doc{}, - }}, &cryptoMock{}, lddl, nil) + _, err := createAuthorizedResponse(singleCred, mockRequestObject, CustomClaims{}, + &didResolverMock{ResolveValue: &did.DocResolution{ + DIDDocument: &did.Doc{}, + }}, &cryptoMock{}, lddl, nil) require.Error(t, err) require.Contains(t, err.Error(), "no assertion method for signing") @@ -529,6 +556,7 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { _, err := createAuthorizedResponse( singleCred, mockRequestObject, + CustomClaims{}, &didResolverMock{ResolveValue: mockDoc}, &cryptoMock{SignErr: expectErr}, lddl, @@ -551,6 +579,7 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { _, err = createAuthorizedResponse( singleCred, mockRequestObject, + CustomClaims{}, &didResolverMock{ResolveValue: mockDoc}, &cryptoMock{}, lddl, @@ -571,6 +600,7 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { _, err = createAuthorizedResponse( credentials, mockRequestObject, + CustomClaims{}, &didResolverMock{ResolveValue: mockDoc}, &cryptoMock{}, lddl, @@ -607,7 +637,7 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { httpClient.Response = string(errResponseBytes) - err = interaction.PresentCredential(credentials) + err = interaction.PresentCredential(credentials, CustomClaims{}) testutil.RequireErrorContains(t, err, InvalidScopeError) }) @@ -619,7 +649,7 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { httpClient.Response = string(errResponseBytes) - err = interaction.PresentCredential(credentials) + err = interaction.PresentCredential(credentials, CustomClaims{}) testutil.RequireErrorContains(t, err, InvalidRequestError) }) @@ -631,7 +661,7 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { httpClient.Response = string(errResponseBytes) - err = interaction.PresentCredential(credentials) + err = interaction.PresentCredential(credentials, CustomClaims{}) testutil.RequireErrorContains(t, err, InvalidClientError) }) @@ -643,7 +673,7 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { httpClient.Response = string(errResponseBytes) - err = interaction.PresentCredential(credentials) + err = interaction.PresentCredential(credentials, CustomClaims{}) testutil.RequireErrorContains(t, err, VPFormatsNotSupportedError) }) @@ -655,7 +685,7 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { httpClient.Response = string(errResponseBytes) - err = interaction.PresentCredential(credentials) + err = interaction.PresentCredential(credentials, CustomClaims{}) testutil.RequireErrorContains(t, err, InvalidPresentationDefinitionURIError) }) @@ -667,7 +697,7 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { httpClient.Response = string(errResponseBytes) - err = interaction.PresentCredential(credentials) + err = interaction.PresentCredential(credentials, CustomClaims{}) testutil.RequireErrorContains(t, err, InvalidPresentationDefinitionReferenceError) }) @@ -679,7 +709,7 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { httpClient.Response = string(errResponseBytes) - err = interaction.PresentCredential(credentials) + err = interaction.PresentCredential(credentials, CustomClaims{}) testutil.RequireErrorContains(t, err, OtherAuthorizationResponseError) }) @@ -695,7 +725,7 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { httpClient.Response = string(errResponseBytes) - err = interaction.PresentCredential(credentials) + err = interaction.PresentCredential(credentials, CustomClaims{}) require.Error(t, err) var walletError *walleterror.Error @@ -711,7 +741,7 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { httpClient.Response = string(errResponseBytes) - err = interaction.PresentCredential(credentials) + err = interaction.PresentCredential(credentials, CustomClaims{}) testutil.RequireErrorContains(t, err, MSEntraNotFoundError) }) t.Run("Token error", func(t *testing.T) { @@ -722,7 +752,7 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { httpClient.Response = string(errResponseBytes) - err = interaction.PresentCredential(credentials) + err = interaction.PresentCredential(credentials, CustomClaims{}) testutil.RequireErrorContains(t, err, MSEntraTokenError) }) t.Run("Transient error", func(t *testing.T) { @@ -733,7 +763,7 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { httpClient.Response = string(errResponseBytes) - err = interaction.PresentCredential(credentials) + err = interaction.PresentCredential(credentials, CustomClaims{}) testutil.RequireErrorContains(t, err, MSEntraTransientError) }) t.Run("Unknown/other error type in msEntraErrorResponse object", func(t *testing.T) { @@ -744,7 +774,7 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { httpClient.Response = string(errResponseBytes) - err = interaction.PresentCredential(credentials) + err = interaction.PresentCredential(credentials, CustomClaims{}) testutil.RequireErrorContains(t, err, OtherAuthorizationResponseError) }) }) @@ -752,12 +782,24 @@ func TestOpenID4VP_PresentCredential(t *testing.T) { t.Run("Response body is neither an errorResponse nor an msEntraErrorResponse object", func(t *testing.T) { httpClient.Response = "" - err = interaction.PresentCredential(credentials) + err = interaction.PresentCredential(credentials, CustomClaims{}) testutil.RequireErrorContains(t, err, OtherAuthorizationResponseError) }) }) } +func TestInteraction_Scope(t *testing.T) { + t.Run("Success", func(t *testing.T) { + interaction := &Interaction{requestObject: &requestObject{ + Scope: "openid+msregistration", + }} + + require.Len(t, interaction.Scope(), 2) + require.Contains(t, interaction.Scope(), "openid") + require.Contains(t, interaction.Scope(), "msregistration") + }) +} + func TestResolverAdapter(t *testing.T) { mockDoc := mockResolution(t, mockDID) adapter := wrapResolver(&didResolverMock{ResolveValue: mockDoc}) diff --git a/pkg/openid4vp/tokens.go b/pkg/openid4vp/tokens.go index b42a3220..251357b0 100644 --- a/pkg/openid4vp/tokens.go +++ b/pkg/openid4vp/tokens.go @@ -14,16 +14,22 @@ type idTokenVPToken struct { PresentationSubmission interface{} `json:"presentation_submission"` //nolint: tagliatelle } +// CustomClaims represents custom claims to be added to id_token. +type CustomClaims struct { + ScopeClaims map[string]interface{} +} + type idTokenClaims struct { - VPToken idTokenVPToken `json:"_vp_token"` //nolint: tagliatelle - Nonce string `json:"nonce"` - Exp int64 `json:"exp"` - Iss string `json:"iss"` - Sub string `json:"sub"` - Aud string `json:"aud"` - Nbf int64 `json:"nbf"` - Iat int64 `json:"iat"` - Jti string `json:"jti"` + VPToken idTokenVPToken `json:"_vp_token"` //nolint: tagliatelle + Scope map[string]interface{} `json:"_scope,omitempty"` //nolint: tagliatelle + Nonce string `json:"nonce"` + Exp int64 `json:"exp"` + Iss string `json:"iss"` + Sub string `json:"sub"` + Aud string `json:"aud"` + Nbf int64 `json:"nbf"` + Iat int64 `json:"iat"` + Jti string `json:"jti"` } type vpTokenClaims struct { diff --git a/test/integration/cli/go.mod b/test/integration/cli/go.mod index de3d4cf3..58d8b1f8 100644 --- a/test/integration/cli/go.mod +++ b/test/integration/cli/go.mod @@ -20,7 +20,7 @@ require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.23.0 // indirect - golang.org/x/net v0.15.0 // indirect + golang.org/x/net v0.16.0 // indirect golang.org/x/oauth2 v0.7.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.1 // indirect diff --git a/test/integration/cli/go.sum b/test/integration/cli/go.sum index 52248dc0..987a9398 100644 --- a/test/integration/cli/go.sum +++ b/test/integration/cli/go.sum @@ -45,13 +45,13 @@ go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= +golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/test/integration/go.mod b/test/integration/go.mod index cf59b8aa..318d3609 100644 --- a/test/integration/go.mod +++ b/test/integration/go.mod @@ -75,10 +75,10 @@ require ( go.opentelemetry.io/otel/trace v1.14.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.6.0 // indirect - golang.org/x/crypto v0.13.0 // indirect + golang.org/x/crypto v0.14.0 // indirect golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect - golang.org/x/net v0.15.0 // indirect - golang.org/x/sys v0.12.0 // indirect + golang.org/x/net v0.16.0 // indirect + golang.org/x/sys v0.13.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/test/integration/go.sum b/test/integration/go.sum index b58c6ade..4ba35178 100644 --- a/test/integration/go.sum +++ b/test/integration/go.sum @@ -196,15 +196,15 @@ golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= +golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -212,8 +212,8 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=