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
  • Loading branch information
woutslakhorst committed Sep 19, 2023
1 parent a4efc56 commit 96340c3
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 5 deletions.
18 changes: 18 additions & 0 deletions auth/api/iam/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,24 @@ func (r Wrapper) validateAsNutsFingerprint(ctx context.Context, fingerprint stri

return 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 @@ -214,6 +215,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
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
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
2 changes: 1 addition & 1 deletion auth/api/iam/test/presentation_definition_mapping.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"eOverdracht-overdrachtsbericht":{}}
{"test":{}}
7 changes: 7 additions & 0 deletions auth/api/iam/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package iam

import (
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/nuts-node/vcr/pe"
"github.com/nuts-foundation/nuts-node/vdr/types"
)

Expand All @@ -29,6 +30,9 @@ type DIDDocument = did.Document
// DIDDocumentMetadata is an alias
type DIDDocumentMetadata = types.DocumentMetadata

// PresentationDefinition is an alias
type PresentationDefinition = pe.PresentationDefinition

const (
// responseTypeParam is the name of the response_type parameter.
// Specified by https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.1
Expand Down Expand Up @@ -200,6 +204,9 @@ type OAuthAuthorizationServerMetadata struct {
// See https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-oauth-20-authorization-serv
PreAuthorizedGrantAnonymousAccessSupported bool `json:"pre-authorized_grant_anonymous_access_supported,omitempty"`

// PresentationDefinitionEndpoint defines the URL of the authorization server's presentation definition endpoint.
PresentationDefinitionEndpoint string `json:"presentation_definition_endpoint,omitempty"`

// PresentationDefinitionUriSupported specifies whether the Wallet supports the transfer of presentation_definition by reference, with true indicating support.
// If omitted, the default value is true. (hence pointer, or add custom unmarshalling)
PresentationDefinitionUriSupported *bool `json:"presentation_definition_uri_supported,omitempty"`
Expand Down
1 change: 1 addition & 0 deletions codegen/configs/auth_iam.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ output-options:
- DIDDocument
- OAuthAuthorizationServerMetadata
- OAuthClientMetadata
- PresentationDefinition
43 changes: 43 additions & 0 deletions docs/_static/auth/iam.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,45 @@ paths:
schema:
type: string
format: uri
"/iam/{did}/presentation_definition":
get:
summary: Used by relying parties to obtain a presentation definition for desired scopes.
description: |
Specified by https://identity.foundation/presentation-exchange/spec/v2.0.0/
The presentation definition is a JSON object that describes the desired verifiable credentials and presentation formats.
A presentation definition is matched against a wallet. If verifiable credentials matching the definition are found,
a presentation can created and submitted together with a presentation submission.
The API returns an array of definitions, one per scope/backend combination if applicable.
operationId: presentationDefinition
tags:
- oauth2
parameters:
- name: did
in: path
required: true
schema:
type: string
example: did:nuts:123
- name: scope
in: query
required: true
schema:
type: array
description: The scope for which a presentation definition is requested. Multiple scopes can be specified by separating them with a space.
items:
type: string
example: patient:x:read
responses:
"200":
description: Authorization request accepted, user is asked for consent.
content:
application/json:
schema:
type: array
items:
"$ref": "#/components/schemas/PresentationDefinition"
"404":
description: Unknown DID
# TODO: What format to use? (codegenerator breaks on aliases)
# See issue https://github.com/nuts-foundation/nuts-node/issues/2365
# create aliases for the specced path
Expand Down Expand Up @@ -333,6 +372,10 @@ components:
OAuth2 Client Metadata
Contain properties from several specifications and may grow over time
type: object
PresentationDefinition:
description: |
A presentation definition is a JSON object that describes the desired verifiable credentials and presentation formats.
type: object
ErrorResponse:
type: object
required:
Expand Down

0 comments on commit 96340c3

Please sign in to comment.