diff --git a/auth/api/iam/api.go b/auth/api/iam/api.go index d96b314c50..ac4d9abe23 100644 --- a/auth/api/iam/api.go +++ b/auth/api/iam/api.go @@ -474,11 +474,16 @@ func (r Wrapper) PresentationDefinition(ctx context.Context, request Presentatio } } - if _, ok := mapping[pe.WalletOwnerOrganization]; !ok { - return nil, oauthError(oauth.ServerError, "no presentation definition found for organization wallet") + walletOwnerType := pe.WalletOwnerOrganization + if request.Params.WalletOwnerType != nil { + walletOwnerType = *request.Params.WalletOwnerType + } + result, exists := mapping[walletOwnerType] + if !exists { + return nil, oauthError(oauth.InvalidRequest, fmt.Sprintf("no presentation definition found for '%s' wallet", walletOwnerType)) } - return PresentationDefinition200JSONResponse(mapping[pe.WalletOwnerOrganization]), nil + return PresentationDefinition200JSONResponse(result), nil } // toOwnedDIDForOAuth2 is like toOwnedDID but wraps the errors in oauth.OAuth2Error to make sure they're returned as specified by the OAuth2 RFC. diff --git a/auth/api/iam/api_test.go b/auth/api/iam/api_test.go index 8fbb64e771..8f31632c37 100644 --- a/auth/api/iam/api_test.go +++ b/auth/api/iam/api_test.go @@ -225,6 +225,7 @@ func TestWrapper_PresentationDefinition(t *testing.T) { webDID := did.MustParseDID("did:web:example.com:iam:123") ctx := audit.TestContext() walletOwnerMapping := pe.WalletOwnerMapping{pe.WalletOwnerOrganization: pe.PresentationDefinition{Id: "test"}} + userWalletType := pe.WalletOwnerUser t.Run("ok", func(t *testing.T) { test := newTestClient(t) @@ -250,6 +251,33 @@ func TestWrapper_PresentationDefinition(t *testing.T) { assert.True(t, ok) }) + t.Run("ok - user wallet", func(t *testing.T) { + walletOwnerMapping := pe.WalletOwnerMapping{pe.WalletOwnerUser: pe.PresentationDefinition{Id: "test"}} + + test := newTestClient(t) + test.policy.EXPECT().PresentationDefinitions(gomock.Any(), webDID, "example-scope").Return(walletOwnerMapping, nil) + test.vdr.EXPECT().IsOwner(gomock.Any(), webDID).Return(true, nil) + + response, err := test.client.PresentationDefinition(ctx, PresentationDefinitionRequestObject{Did: webDID.String(), Params: PresentationDefinitionParams{Scope: "example-scope", WalletOwnerType: &userWalletType}}) + + require.NoError(t, err) + require.NotNil(t, response) + _, ok := response.(PresentationDefinition200JSONResponse) + assert.True(t, ok) + }) + + t.Run("err - unknown wallet type", func(t *testing.T) { + test := newTestClient(t) + test.vdr.EXPECT().IsOwner(gomock.Any(), webDID).Return(true, nil) + test.policy.EXPECT().PresentationDefinitions(gomock.Any(), webDID, "example-scope").Return(walletOwnerMapping, nil) + + response, err := test.client.PresentationDefinition(ctx, PresentationDefinitionRequestObject{Did: webDID.String(), Params: PresentationDefinitionParams{Scope: "example-scope", WalletOwnerType: &userWalletType}}) + + require.Error(t, err) + assert.Nil(t, response) + assert.Equal(t, "invalid_request - no presentation definition found for 'user' wallet", err.Error()) + }) + t.Run("error - unknown scope", func(t *testing.T) { test := newTestClient(t) test.vdr.EXPECT().IsOwner(gomock.Any(), webDID).Return(true, nil) diff --git a/auth/api/iam/generated.go b/auth/api/iam/generated.go index 6ee1fb13d1..3d70cc36d3 100644 --- a/auth/api/iam/generated.go +++ b/auth/api/iam/generated.go @@ -129,7 +129,8 @@ type CallbackParams struct { // PresentationDefinitionParams defines parameters for PresentationDefinition. type PresentationDefinitionParams struct { - Scope string `form:"scope" json:"scope"` + Scope string `form:"scope" json:"scope"` + WalletOwnerType *WalletOwnerType `form:"wallet_owner_type,omitempty" json:"wallet_owner_type,omitempty"` } // HandleAuthorizeResponseFormdataBody defines parameters for HandleAuthorizeResponse. @@ -664,6 +665,13 @@ func (w *ServerInterfaceWrapper) PresentationDefinition(ctx echo.Context) error return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter scope: %s", err)) } + // ------------- Optional query parameter "wallet_owner_type" ------------- + + err = runtime.BindQueryParameter("form", true, false, "wallet_owner_type", ctx.QueryParams(), ¶ms.WalletOwnerType) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter wallet_owner_type: %s", err)) + } + // Invoke the callback with all the unmarshaled arguments err = w.Handler.PresentationDefinition(ctx, did, params) return err diff --git a/auth/api/iam/openid4vp.go b/auth/api/iam/openid4vp.go index b798df8b06..72b85f84be 100644 --- a/auth/api/iam/openid4vp.go +++ b/auth/api/iam/openid4vp.go @@ -537,7 +537,7 @@ func (r Wrapper) handleAccessTokenRequest(ctx context.Context, verifier did.DID, return nil, withCallbackURI(oauthError(oauth.InvalidRequest, fmt.Sprintf("client_id does not match: %s vs %s", oauthSession.ClientID, *clientId)), callbackURI) } - state := oauthSession.ServerState + state := oauthSession.ServerState mapping, err := r.policyBackend.PresentationDefinitions(ctx, verifier, oauthSession.Scope) if err != nil { return nil, withCallbackURI(oauthError(oauth.ServerError, fmt.Sprintf("failed to fetch presentation definition: %s", err.Error())), callbackURI) diff --git a/auth/api/iam/types.go b/auth/api/iam/types.go index 03341cffd7..a20cf5bb17 100644 --- a/auth/api/iam/types.go +++ b/auth/api/iam/types.go @@ -60,6 +60,9 @@ type OAuthAuthorizationServerMetadata = oauth.AuthorizationServerMetadata // OAuthClientMetadata is an alias type OAuthClientMetadata = oauth.OAuthClientMetadata +// WalletOwnerType is an alias +type WalletOwnerType = pe.WalletOwnerType + const ( sessionExpiry = 5 * time.Minute ) diff --git a/codegen/configs/auth_iam.yaml b/codegen/configs/auth_iam.yaml index 2e3b4e176b..401c5754d0 100644 --- a/codegen/configs/auth_iam.yaml +++ b/codegen/configs/auth_iam.yaml @@ -15,3 +15,4 @@ output-options: - TokenResponse - VerifiablePresentation - VerifiableCredential + - WalletOwnerType diff --git a/docs/_static/auth/iam.yaml b/docs/_static/auth/iam.yaml index 2564308c96..a3545185eb 100644 --- a/docs/_static/auth/iam.yaml +++ b/docs/_static/auth/iam.yaml @@ -220,6 +220,10 @@ paths: description: | The scope for which a presentation definition is requested. Multiple scopes can be specified by separating them with a space. example: usecase patient:x:read + - name: wallet_owner_type + in: query + schema: + $ref: '#/components/schemas/WalletOwnerType' responses: "200": description: PresentationDefinition that matches scope is found. @@ -804,6 +808,14 @@ components: items: $ref: '#/components/schemas/VerifiablePresentation' description: The Verifiable Presentations that were used to request the access token using the same encoding as used in the access token request. + WalletOwnerType: + type: string + description: | + Wallet owner type that should fulfill the presentation definition. + Can either be an organization wallet or a user (personal) wallet. + enum: + - organization + - user securitySchemes: jwtBearerAuth: type: http