Skip to content

Commit

Permalink
Add policy backend router (#2647)
Browse files Browse the repository at this point in the history
* generated client code for policy api, implemented presentation_definition call

* added authorized client API and tests
  • Loading branch information
woutslakhorst authored Jan 8, 2024
1 parent 30f34f3 commit 5b3991b
Show file tree
Hide file tree
Showing 41 changed files with 860 additions and 383 deletions.
5 changes: 4 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ The following options can be configured on the server:
http.default.auth.type Whether to enable authentication for the default interface, specify 'token_v2' for bearer token mode or 'token' for legacy bearer token mode.
http.default.cors.origin [] When set, enables CORS from the specified origins on the default HTTP interface.
**JSONLD**
jsonld.contexts.localmapping [https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson,https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson,https://schema.org=assets/contexts/schema-org-v13.ldjson] This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist.
jsonld.contexts.localmapping [https://schema.org=assets/contexts/schema-org-v13.ldjson,https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson,https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson] This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist.
jsonld.contexts.remoteallowlist [https://schema.org,https://www.w3.org/2018/credentials/v1,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json] In strict mode, fetching external JSON-LD contexts is not allowed except for context-URLs listed here.
**Network**
network.bootstrapnodes [] List of bootstrap nodes ('<host>:<port>') which the node initially connect to.
Expand Down Expand Up @@ -260,6 +260,9 @@ The following options can be configured on the server:
vcr.openid4vci.definitionsdir Directory with the additional credential definitions the node could issue (experimental, may change without notice).
vcr.openid4vci.enabled true Enable issuing and receiving credentials over OpenID4VCI.
vcr.openid4vci.timeout 30s Time-out for OpenID4VCI HTTP client operations.
**policy**
policy.address The address of a remote policy server. Mutual exclusive with policy.directory.
policy.directory Directory to read policy files from. Policy files are JSON files that contain a scope to PresentationDefinition mapping. Mutual exclusive with policy.address.
==================================== =============================================================================================================================================================================================================================================================================================================== ================================================================================================================================================================================================================================================================================================================================

This table is automatically generated using the configuration flags in the core and engines. When they're changed
Expand Down
5 changes: 0 additions & 5 deletions auth/api/auth/v1/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import (
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/vcr"
"github.com/nuts-foundation/nuts-node/vcr/credential"
"github.com/nuts-foundation/nuts-node/vcr/pe"
"github.com/nuts-foundation/nuts-node/vdr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -93,10 +92,6 @@ func (m *mockAuthClient) PublicURL() *url.URL {
return nil
}

func (m *mockAuthClient) PresentationDefinitions() *pe.DefinitionResolver {
return &pe.DefinitionResolver{}
}

func createContext(t *testing.T) *TestContext {
t.Helper()
ctrl := gomock.NewController(t)
Expand Down
26 changes: 16 additions & 10 deletions auth/api/iam/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/nuts-foundation/nuts-node/auth/log"
"github.com/nuts-foundation/nuts-node/auth/oauth"
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/policy"
"github.com/nuts-foundation/nuts-node/storage"
"github.com/nuts-foundation/nuts-node/vcr"
"github.com/nuts-foundation/nuts-node/vdr"
Expand Down Expand Up @@ -62,11 +63,12 @@ type Wrapper struct {
vcr vcr.VCR
vdr vdr.VDR
auth auth.AuthenticationServices
policyBackend policy.PDPBackend
templates *template.Template
storageEngine storage.Engine
}

func New(authInstance auth.AuthenticationServices, vcrInstance vcr.VCR, vdrInstance vdr.VDR, storageEngine storage.Engine) *Wrapper {
func New(authInstance auth.AuthenticationServices, vcrInstance vcr.VCR, vdrInstance vdr.VDR, storageEngine storage.Engine, policyBackend policy.PDPBackend) *Wrapper {
templates := template.New("oauth2 templates")
_, err := templates.ParseFS(assets, "assets/*.html")
if err != nil {
Expand All @@ -75,6 +77,7 @@ func New(authInstance auth.AuthenticationServices, vcrInstance vcr.VCR, vdrInsta
return &Wrapper{
storageEngine: storageEngine,
auth: authInstance,
policyBackend: policyBackend,
vcr: vcrInstance,
vdr: vdrInstance,
templates: templates,
Expand Down Expand Up @@ -156,7 +159,7 @@ func (r Wrapper) HandleTokenRequest(ctx context.Context, request HandleTokenRequ
Description: "missing required parameters",
}
}
return r.handleS2SAccessTokenRequest(*ownDID, *request.Body.Scope, *request.Body.PresentationSubmission, *request.Body.Assertion)
return r.handleS2SAccessTokenRequest(ctx, *ownDID, *request.Body.Scope, *request.Body.PresentationSubmission, *request.Body.Assertion)
default:
return nil, oauth.OAuth2Error{
Code: oauth.UnsupportedGrantType,
Expand Down Expand Up @@ -306,7 +309,7 @@ func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAutho
case responseTypeVPIDToken:
// Options:
// - OpenID4VP+SIOP flow, vp_token is sent in Authorization Response
return r.handlePresentationRequest(params, session)
return r.handlePresentationRequest(ctx, params, session)
default:
// TODO: This should be a redirect?
redirectURI, _ := url.Parse(session.RedirectURI)
Expand Down Expand Up @@ -350,18 +353,21 @@ func (r Wrapper) OAuthClientMetadata(ctx context.Context, request OAuthClientMet

return OAuthClientMetadata200JSONResponse(clientMetadata(*r.identityURL(request.Id))), nil
}
func (r Wrapper) PresentationDefinition(_ context.Context, request PresentationDefinitionRequestObject) (PresentationDefinitionResponseObject, error) {
func (r Wrapper) PresentationDefinition(ctx 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
// todo: we only take the first scope as main scope, when backends are introduced we need to use all scopes and send them as one to the backend.
scopes := strings.Split(request.Params.Scope, " ")
presentationDefinition := r.auth.PresentationDefinitions().ByScope(scopes[0])
if presentationDefinition == nil {
authorizer, err := r.idToOwnedDID(ctx, request.Id)
if err != nil {
return nil, err
}

presentationDefinition, err := r.policyBackend.PresentationDefinition(ctx, *authorizer, request.Params.Scope)
if err != nil {
return nil, oauth.OAuth2Error{
Code: oauth.InvalidScope,
Code: oauth.InvalidScope,
Description: err.Error(),
}
}

Expand Down
32 changes: 25 additions & 7 deletions auth/api/iam/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
oauthServices "github.com/nuts-foundation/nuts-node/auth/services/oauth"
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/jsonld"
"github.com/nuts-foundation/nuts-node/policy"
"github.com/nuts-foundation/nuts-node/storage"
"github.com/nuts-foundation/nuts-node/vcr"
"github.com/nuts-foundation/nuts-node/vcr/pe"
Expand Down Expand Up @@ -171,13 +172,14 @@ func TestWrapper_GetOAuthClientMetadata(t *testing.T) {
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")
presentationDefinition := pe.PresentationDefinition{Id: "test"}

t.Run("ok", func(t *testing.T) {
test := newTestClient(t)
test.policy.EXPECT().PresentationDefinition(gomock.Any(), webDID, "eOverdracht-overdrachtsbericht").Return(&presentationDefinition, nil)
test.vdr.EXPECT().IsOwner(gomock.Any(), webDID).Return(true, nil)

response, err := test.client.PresentationDefinition(ctx, PresentationDefinitionRequestObject{Did: webDID.ID, Params: PresentationDefinitionParams{Scope: "eOverdracht-overdrachtsbericht"}})
response, err := test.client.PresentationDefinition(ctx, PresentationDefinitionRequestObject{Id: "123", Params: PresentationDefinitionParams{Scope: "eOverdracht-overdrachtsbericht"}})

require.NoError(t, err)
require.NotNil(t, response)
Expand All @@ -188,7 +190,7 @@ func TestWrapper_PresentationDefinition(t *testing.T) {
t.Run("ok - missing scope", func(t *testing.T) {
test := newTestClient(t)

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

require.NoError(t, err)
require.NotNil(t, response)
Expand All @@ -198,12 +200,25 @@ func TestWrapper_PresentationDefinition(t *testing.T) {

t.Run("error - unknown scope", func(t *testing.T) {
test := newTestClient(t)
test.vdr.EXPECT().IsOwner(gomock.Any(), webDID).Return(true, nil)
test.policy.EXPECT().PresentationDefinition(gomock.Any(), webDID, "unknown").Return(nil, policy.ErrNotFound)

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

require.Error(t, err)
assert.Nil(t, response)
assert.Equal(t, string(oauth.InvalidScope), err.Error())
assert.Equal(t, "invalid_scope - not found", err.Error())
})

t.Run("error - unknown ID", func(t *testing.T) {
test := newTestClient(t)
test.vdr.EXPECT().IsOwner(gomock.Any(), gomock.Any()).Return(false, nil)

response, err := test.client.PresentationDefinition(ctx, PresentationDefinitionRequestObject{Id: "notdid", Params: PresentationDefinitionParams{Scope: "eOverdracht-overdrachtsbericht"}})

require.Error(t, err)
assert.Nil(t, response)
assert.Equal(t, "invalid_request - issuer DID not owned by the server", err.Error())
})
}

Expand Down Expand Up @@ -405,6 +420,7 @@ type testCtx struct {
client *Wrapper
authnServices *auth.MockAuthenticationServices
vdr *vdr.MockVDR
policy *policy.MockBackend
resolver *resolver.MockDIDResolver
relyingParty *oauthServices.MockRelyingParty
vcVerifier *verifier.MockVerifier
Expand All @@ -419,7 +435,7 @@ func newTestClient(t testing.TB) *testCtx {
storageEngine := storage.NewTestStorageEngine(t)
authnServices := auth.NewMockAuthenticationServices(ctrl)
authnServices.EXPECT().PublicURL().Return(publicURL).AnyTimes()
authnServices.EXPECT().PresentationDefinitions().Return(pe.TestDefinitionResolver(t)).AnyTimes()
policyInstance := policy.NewMockBackend(ctrl)
mockResolver := resolver.NewMockDIDResolver(ctrl)
relyingPary := oauthServices.NewMockRelyingParty(ctrl)
vcVerifier := verifier.NewMockVerifier(ctrl)
Expand All @@ -436,6 +452,7 @@ func newTestClient(t testing.TB) *testCtx {
return &testCtx{
ctrl: ctrl,
authnServices: authnServices,
policy: policyInstance,
relyingParty: relyingPary,
vcVerifier: vcVerifier,
resolver: mockResolver,
Expand All @@ -447,6 +464,7 @@ func newTestClient(t testing.TB) *testCtx {
vdr: mockVDR,
vcr: mockVCR,
storageEngine: storageEngine,
policyBackend: policyInstance,
},
}
}
Expand Down
Loading

0 comments on commit 5b3991b

Please sign in to comment.