Skip to content

Commit

Permalink
add start of authorization request flow for user
Browse files Browse the repository at this point in the history
additional tests

additional test

PR feedback

PR feedback

test fix

remove failing test (#2618)

added e2e test for OpenID4VP s2s flow (#2617)

PEX: Provide ParseEnvelope to correctly parse PEX VP envelopes (#2620)

Bump schneider.vip/problem from 1.8.1 to 1.9.0 (#2622)

Bumps [schneider.vip/problem](https://github.com/mschneider82/problem) from 1.8.1 to 1.9.0.
- [Commits](mschneider82/problem@v1.8.1...v1.9.0)

---
updated-dependencies:
- dependency-name: schneider.vip/problem
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

Bump golang.org/x/crypto from 0.15.0 to 0.16.0 (#2628)

Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.15.0 to 0.16.0.
- [Commits](golang/crypto@v0.15.0...v0.16.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

Bump golang.org/x/time from 0.4.0 to 0.5.0 (#2627)

Bumps [golang.org/x/time](https://github.com/golang/time) from 0.4.0 to 0.5.0.
- [Commits](golang/time@v0.4.0...v0.5.0)

---
updated-dependencies:
- dependency-name: golang.org/x/time
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

Missing rfc021 e2e tests changes (#2623)

Discovery: SQLite-based server implementation (#2589)

Bump github.com/nuts-foundation/go-leia/v4 from 4.0.0 to 4.0.1 (#2632)

Bumps [github.com/nuts-foundation/go-leia/v4](https://github.com/nuts-foundation/go-leia) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/nuts-foundation/go-leia/releases)
- [Commits](nuts-foundation/go-leia@v4.0.0...v4.0.1)

---
updated-dependencies:
- dependency-name: github.com/nuts-foundation/go-leia/v4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

Root URL server config property to replace auth.publicurl (#2633)

change idToDID to use did:web in iam API (#2635)

Bump github.com/lestrrat-go/jwx/v2 from 2.0.17 to 2.0.18 (#2645)

Bumps [github.com/lestrrat-go/jwx/v2](https://github.com/lestrrat-go/jwx) from 2.0.17 to 2.0.18.
- [Release notes](https://github.com/lestrrat-go/jwx/releases)
- [Changelog](https://github.com/lestrrat-go/jwx/blob/develop/v2/Changes)
- [Commits](lestrrat-go/jwx@v2.0.17...v2.0.18)

---
updated-dependencies:
- dependency-name: github.com/lestrrat-go/jwx/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

Bump github.com/nats-io/nats-server/v2 from 2.10.5 to 2.10.6 (#2644)

Bumps [github.com/nats-io/nats-server/v2](https://github.com/nats-io/nats-server) from 2.10.5 to 2.10.6.
- [Release notes](https://github.com/nats-io/nats-server/releases)
- [Changelog](https://github.com/nats-io/nats-server/blob/main/.goreleaser.yml)
- [Commits](nats-io/nats-server@v2.10.5...v2.10.6)

---
updated-dependencies:
- dependency-name: github.com/nats-io/nats-server/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

Bump alpine from 3.18.4 to 3.18.5 (#2636)

Bumps alpine from 3.18.4 to 3.18.5.

---
updated-dependencies:
- dependency-name: alpine
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

more NO_CONTENT

timeout godoc
  • Loading branch information
woutslakhorst committed Dec 12, 2023
1 parent 10ec652 commit 5a5f4e7
Show file tree
Hide file tree
Showing 22 changed files with 750 additions and 100 deletions.
30 changes: 28 additions & 2 deletions auth/api/iam/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ func (r *Wrapper) Routes(router core.EchoRouter) {
// - POST handles the form submission, initiating the flow.
router.GET("/iam/:did/openid4vp_demo", r.handleOpenID4VPDemoLanding, auditMiddleware)
router.POST("/iam/:did/openid4vp_demo", r.handleOpenID4VPDemoSendRequest, auditMiddleware)
// The following handlers are used for the user facing OAuth2 flows.
router.GET("/iam/:did/user", r.handleUserLanding, auditMiddleware)
}

func (r Wrapper) middleware(ctx echo.Context, request interface{}, operationID string, f StrictHandlerFunc) (interface{}, error) {
Expand Down Expand Up @@ -375,8 +377,32 @@ func (r Wrapper) idToOwnedDID(ctx context.Context, id string) (*did.DID, error)
return &ownDID, nil
}

func createSession(params map[string]string, ownDID did.DID) *Session {
session := &Session{
func (r Wrapper) RequestAccessToken(ctx context.Context, request RequestAccessTokenRequestObject) (RequestAccessTokenResponseObject, error) {
if request.Body == nil {
// why did oapi-codegen generate a pointer for the body??
return nil, core.InvalidInputError("missing request body")
}
// resolve wallet
requestHolder, err := did.ParseDID(request.Did)
if err != nil {
return nil, core.NotFoundError("did not found: %w", err)
}
isWallet, err := r.vdr.IsOwner(ctx, *requestHolder)
if err != nil {
return nil, err
}
if !isWallet {
return nil, core.InvalidInputError("did not owned by this node: %w", err)
}
if request.Body.UserID != nil && len(*request.Body.UserID) > 0 {
// forward to user flow
return r.requestUserAccessToken(ctx, *requestHolder, request)
}
return r.requestServiceAccessToken(ctx, *requestHolder, request)
}

func createSession(params map[string]string, ownDID did.DID) *OAuthSession {
session := &OAuthSession{
// TODO: Validate client ID
ClientID: params[clientIDParam],
// TODO: Validate scope
Expand Down
55 changes: 54 additions & 1 deletion auth/api/iam/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"net/http/httptest"
"net/url"
"testing"
"time"

"github.com/labstack/echo/v4"
"github.com/nuts-foundation/go-did/did"
Expand All @@ -45,7 +46,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"time"
)

var webDID = did.MustParseDID("did:web:example.com:iam:123")
Expand Down Expand Up @@ -350,6 +350,7 @@ func statusCodeFrom(err error) int {
}

type testCtx struct {
ctrl *gomock.Controller
client *Wrapper
authnServices *auth.MockAuthenticationServices
vdr *vdr.MockVDR
Expand Down Expand Up @@ -382,6 +383,7 @@ func newTestClient(t testing.TB) *testCtx {
vdr.EXPECT().Resolver().Return(resolver).AnyTimes()

return &testCtx{
ctrl: ctrl,
authnServices: authnServices,
relyingParty: relyingPary,
resolver: resolver,
Expand Down Expand Up @@ -488,6 +490,57 @@ func TestWrapper_idToOwnedDID(t *testing.T) {
})
}

func TestWrapper_RequestAccessToken(t *testing.T) {
walletDID := did.MustParseDID("did:web:test.test:iam:123")
verifierDID := did.MustParseDID("did:web:test.test:iam:456")
body := &RequestAccessTokenJSONRequestBody{Verifier: verifierDID.String(), Scope: "first second"}

t.Run("ok - service flow", func(t *testing.T) {
ctx := newTestClient(t)
ctx.vdr.EXPECT().IsOwner(nil, walletDID).Return(true, nil)
ctx.resolver.EXPECT().Resolve(verifierDID, nil).Return(&did.Document{}, &resolver.DocumentMetadata{}, nil)
ctx.relyingParty.EXPECT().RequestRFC021AccessToken(nil, walletDID, verifierDID, "first second").Return(&oauth.TokenResponse{}, nil)

_, err := ctx.client.RequestAccessToken(nil, RequestAccessTokenRequestObject{Did: walletDID.String(), Body: body})

require.NoError(t, err)
})
t.Run("ok - user flow", func(t *testing.T) {
userID := "test"
body := &RequestAccessTokenJSONRequestBody{Verifier: verifierDID.String(), Scope: "first second", UserID: &userID}
ctx := newTestClient(t)
ctx.vdr.EXPECT().IsOwner(nil, walletDID).Return(true, nil)

_, err := ctx.client.RequestAccessToken(nil, RequestAccessTokenRequestObject{Did: walletDID.String(), Body: body})

require.NoError(t, err)
})
t.Run("error - DID not owned", func(t *testing.T) {
ctx := newTestClient(t)
ctx.vdr.EXPECT().IsOwner(nil, walletDID).Return(false, nil)

_, err := ctx.client.RequestAccessToken(nil, RequestAccessTokenRequestObject{Did: walletDID.String(), Body: body})

require.Error(t, err)
assert.ErrorContains(t, err, "not owned by this node")
})
t.Run("error - invalid DID", func(t *testing.T) {
ctx := newTestClient(t)

_, err := ctx.client.RequestAccessToken(nil, RequestAccessTokenRequestObject{Did: "invalid", Body: body})

require.EqualError(t, err, "did not found: invalid DID")
})
t.Run("error - missing request body", func(t *testing.T) {
ctx := newTestClient(t)

_, err := ctx.client.RequestAccessToken(nil, RequestAccessTokenRequestObject{Did: walletDID.String()})

require.Error(t, err)
assert.EqualError(t, err, "missing request body")
})
}

type strictServerCallCapturer bool

func (s *strictServerCallCapturer) handle(ctx echo.Context, request interface{}) (response interface{}, err error) {
Expand Down
25 changes: 23 additions & 2 deletions auth/api/iam/generated.go

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

16 changes: 9 additions & 7 deletions auth/api/iam/openid4vp.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,20 @@ import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"

"github.com/google/uuid"
"github.com/labstack/echo/v4"
ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/go-did/vc"
"github.com/nuts-foundation/nuts-node/auth/oauth"
httpNuts "github.com/nuts-foundation/nuts-node/http"
"github.com/nuts-foundation/nuts-node/vcr/credential"
"github.com/nuts-foundation/nuts-node/vcr/holder"
"net/http"
"net/url"
"strings"
"time"
)

const sessionExpiry = 5 * time.Minute
Expand All @@ -55,15 +57,15 @@ func (r *Wrapper) sendPresentationRequest(ctx context.Context, response http.Res
params[responseTypeParam] = responseTypeVPIDToken
// TODO: Depending on parameter size, we either use redirect with query parameters or a form post.
// For simplicity, we now just query parameters.
result := AddQueryParams(*authzEndpoint, params)
result := httpNuts.AddQueryParams(*authzEndpoint, params)
response.Header().Add("Location", result.String())
response.WriteHeader(http.StatusFound)
return nil
}

// handlePresentationRequest handles an Authorization Request as specified by OpenID4VP: https://openid.net/specs/openid-4-verifiable-presentations-1_0.html.
// It is handled by a wallet, called by a verifier who wants the wallet to present one or more verifiable credentials.
func (r *Wrapper) handlePresentationRequest(params map[string]string, session *Session) (HandleAuthorizeRequestResponseObject, error) {
func (r *Wrapper) handlePresentationRequest(params map[string]string, session *OAuthSession) (HandleAuthorizeRequestResponseObject, error) {
ctx := context.TODO()
// Presentation definition is always derived from the scope.
// Later on, we might support presentation_definition and/or presentation_definition_uri parameters instead of scope as well.
Expand Down Expand Up @@ -179,7 +181,7 @@ func (r *Wrapper) handlePresentationRequestAccept(c echo.Context) error {
return errors.New("missing sessionID parameter")
}

var session Session
var session OAuthSession
sessionStore := r.storageEngine.GetSessionDatabase().GetStore(sessionExpiry, "openid", session.OwnDID.String(), "session")
err := sessionStore.Get(sessionID, &session)
if err != nil {
Expand Down
39 changes: 21 additions & 18 deletions auth/api/iam/s2s_vptoken.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ const s2sMaxPresentationValidity = 5 * time.Second
// The value is specified by Nuts RFC021.
const s2sMaxClockSkew = 5 * time.Second

// sessionValidity defines how long user sessions are valid.
// TODO: Might want to make this configurable at some point
const sessionValidity = 15 * time.Minute

// serviceToService adds support for service-to-service OAuth2 flows,
// which uses a custom vp_token grant to authenticate calls to the token endpoint.
// Clients first call the presentation definition endpoint to get a presentation definition for the desired scope,
// then create a presentation submission given the definition which is posted to the token endpoint as vp_token.
// The AS then returns an access token with the requested scope.
// Requires:
// - GET /presentation_definition?scope=... (returns a presentation definition)
// - POST /token (with vp_token grant)
type serviceToService struct {
}

// handleS2SAccessTokenRequest handles the /token request with vp_token bearer grant type, intended for service-to-service exchanges.
// It performs cheap checks first (parameter presence and validity, matching VCs to the presentation definition), then the more expensive ones (checking signatures).
func (r *Wrapper) handleS2SAccessTokenRequest(issuer did.DID, scope string, submissionJSON string, assertionJSON string) (HandleTokenRequestResponseObject, error) {
Expand Down Expand Up @@ -106,24 +121,12 @@ func (r *Wrapper) handleS2SAccessTokenRequest(issuer did.DID, scope string, subm
return HandleTokenRequest200JSONResponse(*response), nil
}

func (r *Wrapper) RequestAccessToken(ctx context.Context, request RequestAccessTokenRequestObject) (RequestAccessTokenResponseObject, error) {
if request.Body == nil {
// why did oapi-codegen generate a pointer for the body??
return nil, core.InvalidInputError("missing request body")
}
// resolve wallet
requestHolder, err := did.ParseDID(request.Did)
if err != nil {
return nil, core.NotFoundError("did not found: %w", err)
}
isWallet, err := r.vdr.IsOwner(ctx, *requestHolder)
if err != nil {
return nil, err
}
if !isWallet {
return nil, core.InvalidInputError("did not owned by this node: %w", err)
}
func (s serviceToService) handleAuthzRequest(_ map[string]string, _ *OAuthSession) (*authzResponse, error) {
// Protocol does not support authorization code flow
return nil, nil
}

func (r Wrapper) requestServiceAccessToken(ctx context.Context, requestHolder did.DID, request RequestAccessTokenRequestObject) (RequestAccessTokenResponseObject, error) {
// resolve verifier metadata
requestVerifier, err := did.ParseDID(request.Body.Verifier)
if err != nil {
Expand All @@ -137,7 +140,7 @@ func (r *Wrapper) RequestAccessToken(ctx context.Context, request RequestAccessT
return nil, err
}

tokenResult, err := r.auth.RelyingParty().RequestRFC021AccessToken(ctx, *requestHolder, *requestVerifier, request.Body.Scope)
tokenResult, err := r.auth.RelyingParty().RequestRFC021AccessToken(ctx, requestHolder, *requestVerifier, request.Body.Scope)
if err != nil {
// this can be an internal server error, a 400 oauth error or a 412 precondition failed if the wallet does not contain the required credentials
return nil, err
Expand Down
58 changes: 10 additions & 48 deletions auth/api/iam/s2s_vptoken_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,98 +19,60 @@
package iam

import (
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/nuts-foundation/nuts-node/vcr/signature/proof"
"net/http"
"testing"
"time"

"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"errors"
"net/http"
"testing"
"time"

"github.com/lestrrat-go/jwx/v2/jwt"
ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/go-did/vc"
"github.com/nuts-foundation/nuts-node/auth/oauth"
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/jsonld"
"github.com/nuts-foundation/nuts-node/vcr/credential"
"github.com/nuts-foundation/nuts-node/vcr/pe"
"github.com/nuts-foundation/nuts-node/vcr/signature/proof"
"github.com/nuts-foundation/nuts-node/vcr/test"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)

func TestWrapper_RequestAccessToken(t *testing.T) {
func TestWrapper_requestServiceAccessToken(t *testing.T) {
walletDID := did.MustParseDID("did:test:123")
verifierDID := did.MustParseDID("did:test:456")
body := &RequestAccessTokenJSONRequestBody{Verifier: verifierDID.String(), Scope: "first second"}

t.Run("ok", func(t *testing.T) {
ctx := newTestClient(t)
ctx.vdr.EXPECT().IsOwner(nil, walletDID).Return(true, nil)
ctx.resolver.EXPECT().Resolve(verifierDID, nil).Return(&did.Document{}, &resolver.DocumentMetadata{}, nil)
ctx.relyingParty.EXPECT().RequestRFC021AccessToken(nil, walletDID, verifierDID, "first second").Return(&oauth.TokenResponse{}, nil)

_, err := ctx.client.RequestAccessToken(nil, RequestAccessTokenRequestObject{Did: walletDID.String(), Body: body})

require.NoError(t, err)
})
t.Run("error - DID not owned", func(t *testing.T) {
ctx := newTestClient(t)
ctx.vdr.EXPECT().IsOwner(nil, walletDID).Return(false, nil)

_, err := ctx.client.RequestAccessToken(nil, RequestAccessTokenRequestObject{Did: walletDID.String(), Body: body})

require.Error(t, err)
assert.ErrorContains(t, err, "not owned by this node")
})
t.Run("error - invalid DID", func(t *testing.T) {
ctx := newTestClient(t)

_, err := ctx.client.RequestAccessToken(nil, RequestAccessTokenRequestObject{Did: "invalid", Body: body})

require.EqualError(t, err, "did not found: invalid DID")
})
t.Run("error - missing request body", func(t *testing.T) {
ctx := newTestClient(t)

_, err := ctx.client.RequestAccessToken(nil, RequestAccessTokenRequestObject{Did: walletDID.String()})

require.Error(t, err)
assert.EqualError(t, err, "missing request body")
})
t.Run("error - invalid verifier did", func(t *testing.T) {
ctx := newTestClient(t)
body := &RequestAccessTokenJSONRequestBody{Verifier: "invalid"}
ctx.vdr.EXPECT().IsOwner(nil, walletDID).Return(true, nil)

_, err := ctx.client.RequestAccessToken(nil, RequestAccessTokenRequestObject{Did: walletDID.String(), Body: body})
_, err := ctx.client.requestServiceAccessToken(nil, walletDID, RequestAccessTokenRequestObject{Did: walletDID.String(), Body: body})

require.Error(t, err)
assert.EqualError(t, err, "invalid verifier: invalid DID")
})
t.Run("error - verifier not found", func(t *testing.T) {
ctx := newTestClient(t)
ctx.vdr.EXPECT().IsOwner(nil, walletDID).Return(true, nil)
ctx.resolver.EXPECT().Resolve(verifierDID, nil).Return(nil, nil, resolver.ErrNotFound)

_, err := ctx.client.RequestAccessToken(nil, RequestAccessTokenRequestObject{Did: walletDID.String(), Body: body})
_, err := ctx.client.requestServiceAccessToken(nil, walletDID, RequestAccessTokenRequestObject{Did: walletDID.String(), Body: body})

require.Error(t, err)
assert.EqualError(t, err, "verifier not found: unable to find the DID document")
})
t.Run("error - verifier error", func(t *testing.T) {
ctx := newTestClient(t)
ctx.vdr.EXPECT().IsOwner(nil, walletDID).Return(true, nil)
ctx.resolver.EXPECT().Resolve(verifierDID, nil).Return(&did.Document{}, &resolver.DocumentMetadata{}, nil)
ctx.relyingParty.EXPECT().RequestRFC021AccessToken(nil, walletDID, verifierDID, "first second").Return(nil, core.Error(http.StatusPreconditionFailed, "no matching credentials"))

_, err := ctx.client.RequestAccessToken(nil, RequestAccessTokenRequestObject{Did: walletDID.String(), Body: body})
_, err := ctx.client.requestServiceAccessToken(nil, walletDID, RequestAccessTokenRequestObject{Did: walletDID.String(), Body: body})

require.Error(t, err)
assert.EqualError(t, err, "no matching credentials")
Expand Down
Loading

0 comments on commit 5a5f4e7

Please sign in to comment.