Skip to content

Commit

Permalink
added presentation_definition endpoint to the metadata
Browse files Browse the repository at this point in the history
added implementation of the presentation definition endpoint

test fix

added API client for presentation definition
  • Loading branch information
woutslakhorst committed Sep 21, 2023
1 parent f167c98 commit 3da08a9
Show file tree
Hide file tree
Showing 12 changed files with 322 additions and 11 deletions.
18 changes: 18 additions & 0 deletions auth/api/iam/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,24 @@ func (r Wrapper) OAuthClientMetadata(ctx context.Context, request OAuthClientMet

return OAuthClientMetadata200JSONResponse(clientMetadata(*identity)), nil
}
func (r Wrapper) PresentationDefinition(_ context.Context, request PresentationDefinitionRequestObject) (PresentationDefinitionResponseObject, error) {
if len(request.Params.Scope) == 0 {
return PresentationDefinition200JSONResponse([]PresentationDefinition{}), nil
}

// todo: only const scopes supported, scopes with variable arguments not supported yet
// map all scopes to a presentation definition
presentationDefinitions := make([]PresentationDefinition, 0, len(request.Params.Scope))
for _, scope := range request.Params.Scope {
presentationDefinition := r.auth.PresentationDefinitions().ByScope(scope)
if presentationDefinition == nil {
return nil, core.InvalidInputError("unsupported scope: %s", scope)
}
presentationDefinitions = append(presentationDefinitions, *presentationDefinition)
}

return PresentationDefinition200JSONResponse(presentationDefinitions), nil
}

func createSession(params map[string]string, ownDID did.DID) *Session {
session := &Session{
Expand Down
40 changes: 40 additions & 0 deletions auth/api/iam/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/nuts-foundation/nuts-node/audit"
"github.com/nuts-foundation/nuts-node/auth"
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/vcr/pe"
vdr "github.com/nuts-foundation/nuts-node/vdr/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -154,6 +155,45 @@ func TestWrapper_GetOAuthClientMetadata(t *testing.T) {
assert.Nil(t, res)
})
}
func TestWrapper_PresentationDefinition(t *testing.T) {
webDID := did.MustParseDID("did:web:example.com:iam:123")
ctx := audit.TestContext()
definitionResolver := pe.DefinitionResolver{}
_ = definitionResolver.LoadFromFile("test/presentation_definition_mapping.json")

t.Run("ok", func(t *testing.T) {
test := newTestClient(t)
test.authnServices.EXPECT().PresentationDefinitions().Return(&definitionResolver)

response, err := test.client.PresentationDefinition(ctx, PresentationDefinitionRequestObject{Did: webDID.ID, Params: PresentationDefinitionParams{Scope: []string{"test"}}})

require.NoError(t, err)
require.NotNil(t, response)
definitions := []PresentationDefinition(response.(PresentationDefinition200JSONResponse))
assert.Len(t, definitions, 1)
})

t.Run("ok - missing scope", func(t *testing.T) {
test := newTestClient(t)

response, err := test.client.PresentationDefinition(ctx, PresentationDefinitionRequestObject{Did: webDID.ID, Params: PresentationDefinitionParams{}})

require.NoError(t, err)
require.NotNil(t, response)
definitions := []PresentationDefinition(response.(PresentationDefinition200JSONResponse))
assert.Len(t, definitions, 0)
})

t.Run("error - unknown scope", func(t *testing.T) {
test := newTestClient(t)
test.authnServices.EXPECT().PresentationDefinitions().Return(&definitionResolver)

response, err := test.client.PresentationDefinition(ctx, PresentationDefinitionRequestObject{Did: webDID.ID, Params: PresentationDefinitionParams{Scope: []string{"unknown"}}})

assert.EqualError(t, err, "unsupported scope: unknown")
assert.Nil(t, response)
})
}

// statusCodeFrom returns the statuscode if err is core.HTTPStatusCodeError, or 0 if it isn't
func statusCodeFrom(err error) int {
Expand Down
43 changes: 40 additions & 3 deletions auth/api/iam/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/nuts-foundation/nuts-node/vdr/didweb"
"io"
"net/http"
"net/url"
)

// HTTPClient holds the server address and other basic settings for the http client
Expand Down Expand Up @@ -55,9 +56,9 @@ func (hb HTTPClient) OAuthAuthorizationServerMetadata(ctx context.Context, webDI
return nil, err
}

request := &http.Request{
Method: "GET",
URL: metadataURL,
request, err := http.NewRequest(http.MethodGet, metadataURL.String(), nil)
if err != nil {
return nil, err
}
response, err := hb.httpClient.Do(request.WithContext(ctx))
if err != nil {
Expand All @@ -80,3 +81,39 @@ func (hb HTTPClient) OAuthAuthorizationServerMetadata(ctx context.Context, webDI

return &metadata, nil
}

// PresentationDefinition retrieves the presentation definition for the given web DID and scope(s).
// We pass the endpoint url for the presentation definition endpoint because we already retrieved the metadata in a previous step.
// The scopes are evaluated as raw query params and encoded if needed.
func (hb HTTPClient) PresentationDefinition(ctx context.Context, definitionEndpoint string, scopes []string) ([]PresentationDefinition, error) {
presentationDefinitionURL, err := url.Parse(definitionEndpoint)
if err != nil {
return nil, err
}
presentationDefinitionURL.RawQuery = url.Values{"scope": scopes}.Encode()

// create a GET request with scope query param
request, err := http.NewRequest(http.MethodGet, presentationDefinitionURL.String(), nil)
if err != nil {
return nil, err
}
response, err := hb.httpClient.Do(request.WithContext(ctx))
if err != nil {
return nil, fmt.Errorf("failed to call endpoint: %w", err)
}
if err = core.TestResponseCode(http.StatusOK, response); err != nil {
return nil, err
}

definitions := make([]PresentationDefinition, 0)
var data []byte

if data, err = io.ReadAll(response.Body); err != nil {
return nil, fmt.Errorf("unable to read response: %w", err)
}
if err = json.Unmarshal(data, &definitions); err != nil {
return nil, fmt.Errorf("unable to unmarshal response: %w, %s", err, string(data))
}

return definitions, nil
}
74 changes: 74 additions & 0 deletions auth/api/iam/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,80 @@ func TestHTTPClient_OAuthAuthorizationServerMetadata(t *testing.T) {
})
}

func TestHTTPClient_PresentationDefinition(t *testing.T) {
ctx := context.Background()
definitions := []PresentationDefinition{
{
Id: "123",
},
}

t.Run("ok", func(t *testing.T) {
handler := http2.Handler{StatusCode: http.StatusOK, ResponseData: definitions}
tlsServer, client := testServerAndClient(t, &handler)

response, err := client.PresentationDefinition(ctx, tlsServer.URL, []string{"test"})

require.NoError(t, err)
require.NotNil(t, definitions)
assert.Equal(t, definitions, response)
require.NotNil(t, handler.Request)
})
t.Run("ok - multiple scopes", func(t *testing.T) {
handler := http2.Handler{StatusCode: http.StatusOK, ResponseData: definitions}
tlsServer, client := testServerAndClient(t, &handler)

response, err := client.PresentationDefinition(ctx, tlsServer.URL, []string{"first", "second"})

require.NoError(t, err)
require.NotNil(t, definitions)
assert.Equal(t, definitions, response)
require.NotNil(t, handler.Request)
assert.Equal(t, url.Values{"scope": []string{"first", "second"}}, handler.Request.URL.Query())
})

t.Run("error - not found", func(t *testing.T) {
handler := http2.Handler{StatusCode: http.StatusNotFound}
tlsServer, client := testServerAndClient(t, &handler)

response, err := client.PresentationDefinition(ctx, tlsServer.URL, []string{"test"})

require.Error(t, err)
assert.EqualError(t, err, "server returned HTTP 404 (expected: 200)")
assert.Nil(t, response)
})
t.Run("error - invalid URL", func(t *testing.T) {
handler := http2.Handler{StatusCode: http.StatusNotFound}
_, client := testServerAndClient(t, &handler)

response, err := client.PresentationDefinition(ctx, ":", []string{"test"})

require.Error(t, err)
assert.EqualError(t, err, "parse \":\": missing protocol scheme")
assert.Nil(t, response)
})
t.Run("error - unknown host", func(t *testing.T) {
handler := http2.Handler{StatusCode: http.StatusNotFound}
_, client := testServerAndClient(t, &handler)

response, err := client.PresentationDefinition(ctx, "http://localhost", []string{"test"})

require.Error(t, err)
assert.ErrorContains(t, err, "connection refused")
assert.Nil(t, response)
})
t.Run("error - invalid response", func(t *testing.T) {
handler := http2.Handler{StatusCode: http.StatusOK, ResponseData: "}"}
tlsServer, client := testServerAndClient(t, &handler)

response, err := client.PresentationDefinition(ctx, tlsServer.URL, []string{"test"})

require.Error(t, err)
assert.EqualError(t, err, "unable to unmarshal response")
assert.Nil(t, response)
})
}

func testServerAndClient(t *testing.T, handler http.Handler) (*httptest.Server, *HTTPClient) {
tlsServer := http2.TestTLSServer(t, handler)
return tlsServer, &HTTPClient{
Expand Down
89 changes: 89 additions & 0 deletions auth/api/iam/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions auth/api/iam/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,10 @@ func authorizationServerMetadata(identity url.URL) OAuthAuthorizationServerMetad
TokenEndpoint: identity.JoinPath("token").String(),
GrantTypesSupported: grantTypesSupported,
PreAuthorizedGrantAnonymousAccessSupported: true,
VPFormats: vpFormatsSupported,
VPFormatsSupported: vpFormatsSupported,
ClientIdSchemesSupported: clientIdSchemesSupported,
PresentationDefinitionEndpoint: identity.JoinPath("presentation_definition").String(),
VPFormats: vpFormatsSupported,
VPFormatsSupported: vpFormatsSupported,
ClientIdSchemesSupported: clientIdSchemesSupported,
}
}

Expand Down
7 changes: 4 additions & 3 deletions auth/api/iam/metadata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,10 @@ func Test_authorizationServerMetadata(t *testing.T) {
TokenEndpoint: identity + "/token",
GrantTypesSupported: []string{"authorization_code", "vp_token", "urn:ietf:params:oauth:grant-type:pre-authorized_code"},
PreAuthorizedGrantAnonymousAccessSupported: true,
VPFormats: vpFormatsSupported,
VPFormatsSupported: vpFormatsSupported,
ClientIdSchemesSupported: []string{"did"},
PresentationDefinitionEndpoint: identity + "/presentation_definition",
VPFormats: vpFormatsSupported,
VPFormatsSupported: vpFormatsSupported,
ClientIdSchemesSupported: []string{"did"},
}
assert.Equal(t, expected, authorizationServerMetadata(*identityURL))
}
Expand Down
2 changes: 1 addition & 1 deletion auth/api/iam/openid4vp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func TestWrapper_handlePresentationRequest(t *testing.T) {
mockVDR.EXPECT().IsOwner(gomock.Any(), holderDID).Return(true, nil)

params := map[string]string{
"scope": "eOverdracht-overdrachtsbericht",
"scope": "test",
"response_type": "code",
"response_mode": "direct_post",
"client_metadata_uri": "https://example.com/client_metadata.xml",
Expand Down
Loading

0 comments on commit 3da08a9

Please sign in to comment.