Skip to content

Commit

Permalink
fix tests
Browse files Browse the repository at this point in the history
  • Loading branch information
reinkrul committed May 24, 2024
1 parent c53e9c5 commit b10cd44
Show file tree
Hide file tree
Showing 13 changed files with 115 additions and 140 deletions.
13 changes: 12 additions & 1 deletion auth/api/iam/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,18 @@ func (r Wrapper) Routes(router core.EchoRouter) {
}, audit.Middleware(apiModuleName))
router.Use(usersession.Middleware{
Skipper: func(c echo.Context) bool {
return strings.HasSuffix(c.Path(), "/user") // user landing page
// The following URLs require a user session:
paths := []string{
"/oauth2/:did/user",
"/oauth2/:did/authorize",
"/oauth2/:did/callback",
}
for _, path := range paths {
if c.Path() == path {
return false
}
}
return true
},
}.Handle)
}
Expand Down
14 changes: 4 additions & 10 deletions auth/api/iam/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/nuts-foundation/nuts-node/auth/api/iam/usersession"
"net/http"
"net/http/httptest"
"net/url"
Expand Down Expand Up @@ -394,23 +395,15 @@ func TestWrapper_HandleAuthorizeRequest(t *testing.T) {
RedirectURI: "https://example.com/iam/holder/cb",
ResponseType: "code",
})
_ = ctx.client.userSessionStore().Put("session-id", UserSession{
TenantDID: holderDID,
Wallet: UserWallet{
DID: holderDID,
},
})
callCtx, _ := usersession.CreateTestSession(requestContext(nil), holderDID)
clientMetadata := oauth.OAuthClientMetadata{VPFormats: oauth.DefaultOpenIDSupportedFormats()}
ctx.iamClient.EXPECT().ClientMetadata(gomock.Any(), "https://example.com/.well-known/authorization-server/iam/verifier").Return(&clientMetadata, nil)
pdEndpoint := "https://example.com/oauth2/did:web:example.com:iam:verifier/presentation_definition?scope=test"
ctx.iamClient.EXPECT().PresentationDefinition(gomock.Any(), pdEndpoint).Return(&pe.PresentationDefinition{}, nil)
ctx.wallet.EXPECT().BuildSubmission(gomock.Any(), holderDID, pe.PresentationDefinition{}, clientMetadata.VPFormats, gomock.Any()).Return(&vc.VerifiablePresentation{}, &pe.PresentationSubmission{}, nil)
ctx.iamClient.EXPECT().PostAuthorizationResponse(gomock.Any(), vc.VerifiablePresentation{}, pe.PresentationSubmission{}, "https://example.com/oauth2/did:web:example.com:iam:verifier/response", "state").Return("https://example.com/iam/holder/redirect", nil)

res, err := ctx.client.HandleAuthorizeRequest(requestContext(map[string]interface{}{}, func(request *http.Request) {
request.Header = make(http.Header)
request.AddCookie(createUserSessionCookie("session-id", "/"))
}), HandleAuthorizeRequestRequestObject{
res, err := ctx.client.HandleAuthorizeRequest(callCtx, HandleAuthorizeRequestRequestObject{
Did: holderDID.String(),
})

Expand Down Expand Up @@ -706,6 +699,7 @@ func TestWrapper_Routes(t *testing.T) {

router.EXPECT().GET(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
router.EXPECT().POST(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
router.EXPECT().Use(gomock.AssignableToTypeOf(usersession.Middleware{}.Handle))

(&Wrapper{}).Routes(router)
}
Expand Down
3 changes: 2 additions & 1 deletion auth/api/iam/openid4vp.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/nuts-foundation/nuts-node/auth/api/iam/usersession"
"net/http"
"net/url"
"slices"
Expand Down Expand Up @@ -287,7 +288,7 @@ func (r Wrapper) handleAuthorizeRequestFromVerifier(ctx context.Context, tenantD

// TODO: Create session if it does not exist (use client state to get original Authorization Code request)?
// Although it would be quite weird (maybe it expired).
userSession, err := getUserSession(ctx.Value(httpRequestContextKey{}).(*http.Request), tenantDID, nil)
userSession, err := usersession.Get(ctx, tenantDID)
if userSession == nil {
return nil, oauth.OAuth2Error{Code: oauth.InvalidRequest, InternalError: err, Description: "no user session found"}
}
Expand Down
25 changes: 2 additions & 23 deletions auth/api/iam/openid4vp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package iam
import (
"context"
"encoding/json"
"github.com/nuts-foundation/nuts-node/auth/api/iam/usersession"
"net/http"
"net/url"
"strings"
Expand Down Expand Up @@ -188,25 +189,12 @@ func TestWrapper_handleAuthorizeRequestFromVerifier(t *testing.T) {
RedirectURI: "https://example.com/iam/holder/cb",
VerifierDID: &verifierDID,
}
userSession := UserSession{
TenantDID: holderDID,
Wallet: UserWallet{
DID: did.MustParseDID("did:jwk:123"),
},
}

httpRequest := &http.Request{
Header: http.Header{},
}
const userSessionID = "session_id"
httpRequest.AddCookie(createUserSessionCookie(userSessionID, "/"))
httpRequestCtx := context.WithValue(context.Background(), httpRequestContextKey{}, httpRequest)
httpRequestCtx, _ := usersession.CreateTestSession(context.Background(), holderDID)
t.Run("invalid client_id", func(t *testing.T) {
ctx := newTestClient(t)
params := defaultParams()
params[oauth.ClientIDParam] = "did:nuts:1"
expectPostError(t, ctx, oauth.InvalidRequest, "invalid client_id parameter (only did:web is supported)", responseURI, "state")
_ = ctx.client.userSessionStore().Put(userSessionID, userSession)

_, err := ctx.client.handleAuthorizeRequestFromVerifier(httpRequestCtx, holderDID, params, pe.WalletOwnerOrganization)

Expand All @@ -227,7 +215,6 @@ func TestWrapper_handleAuthorizeRequestFromVerifier(t *testing.T) {
params := defaultParams()
ctx.iamClient.EXPECT().ClientMetadata(gomock.Any(), "https://example.com/.well-known/authorization-server/iam/verifier").Return(nil, assert.AnError)
expectPostError(t, ctx, oauth.ServerError, "failed to get client metadata (verifier)", responseURI, "state")
_ = ctx.client.userSessionStore().Put(userSessionID, userSession)

_, err := ctx.client.handleAuthorizeRequestFromVerifier(httpRequestCtx, holderDID, params, pe.WalletOwnerOrganization)

Expand All @@ -247,7 +234,6 @@ func TestWrapper_handleAuthorizeRequestFromVerifier(t *testing.T) {
ctx := newTestClient(t)
params := defaultParams()
params[oauth.ClientMetadataParam] = "not empty"
_ = ctx.client.userSessionStore().Put(userSessionID, userSession)
expectPostError(t, ctx, oauth.InvalidRequest, "client_metadata and client_metadata_uri are mutually exclusive", responseURI, "state")

_, err := ctx.client.handleAuthorizeRequestFromVerifier(httpRequestCtx, holderDID, params, pe.WalletOwnerOrganization)
Expand All @@ -259,7 +245,6 @@ func TestWrapper_handleAuthorizeRequestFromVerifier(t *testing.T) {
params := defaultParams()
delete(params, oauth.ClientMetadataURIParam)
params[oauth.ClientMetadataParam] = "{invalid"
_ = ctx.client.userSessionStore().Put(userSessionID, userSession)
expectPostError(t, ctx, oauth.InvalidRequest, "invalid client_metadata", responseURI, "state")

_, err := ctx.client.handleAuthorizeRequestFromVerifier(httpRequestCtx, holderDID, params, pe.WalletOwnerOrganization)
Expand All @@ -271,7 +256,6 @@ func TestWrapper_handleAuthorizeRequestFromVerifier(t *testing.T) {
params := defaultParams()
ctx.iamClient.EXPECT().ClientMetadata(gomock.Any(), "https://example.com/.well-known/authorization-server/iam/verifier").Return(nil, assert.AnError)
expectPostError(t, ctx, oauth.ServerError, "failed to get client metadata (verifier)", responseURI, "state")
_ = ctx.client.userSessionStore().Put(userSessionID, userSession)

_, err := ctx.client.handleAuthorizeRequestFromVerifier(httpRequestCtx, holderDID, params, pe.WalletOwnerOrganization)

Expand Down Expand Up @@ -309,7 +293,6 @@ func TestWrapper_handleAuthorizeRequestFromVerifier(t *testing.T) {
ctx.iamClient.EXPECT().ClientMetadata(gomock.Any(), "https://example.com/.well-known/authorization-server/iam/verifier").Return(&clientMetadata, nil)
params := defaultParams()
params[oauth.PresentationDefParam] = "not empty"
_ = ctx.client.userSessionStore().Put(userSessionID, userSession)
expectPostError(t, ctx, oauth.InvalidRequest, "presentation_definition and presentation_definition_uri are mutually exclusive", responseURI, "state")

_, err := ctx.client.handleAuthorizeRequestFromVerifier(httpRequestCtx, holderDID, params, pe.WalletOwnerOrganization)
Expand All @@ -322,7 +305,6 @@ func TestWrapper_handleAuthorizeRequestFromVerifier(t *testing.T) {
delete(params, oauth.PresentationDefUriParam)
params[oauth.PresentationDefParam] = "{invalid"
ctx.iamClient.EXPECT().ClientMetadata(gomock.Any(), "https://example.com/.well-known/authorization-server/iam/verifier").Return(&clientMetadata, nil)
_ = ctx.client.userSessionStore().Put(userSessionID, userSession)
expectPostError(t, ctx, oauth.InvalidRequest, "invalid presentation_definition", responseURI, "state")

_, err := ctx.client.handleAuthorizeRequestFromVerifier(httpRequestCtx, holderDID, params, pe.WalletOwnerOrganization)
Expand All @@ -336,7 +318,6 @@ func TestWrapper_handleAuthorizeRequestFromVerifier(t *testing.T) {
ctx.iamClient.EXPECT().ClientMetadata(gomock.Any(), "https://example.com/.well-known/authorization-server/iam/verifier").Return(&clientMetadata, nil)
ctx.iamClient.EXPECT().PresentationDefinition(gomock.Any(), pdEndpoint).Return(nil, assert.AnError)
expectPostError(t, ctx, oauth.InvalidPresentationDefinitionURI, "failed to retrieve presentation definition on https://example.com/iam/verifier/presentation_definition?scope=test", responseURI, "state")
_ = ctx.client.userSessionStore().Put(userSessionID, userSession)

_, err := ctx.client.handleAuthorizeRequestFromVerifier(httpRequestCtx, holderDID, params, pe.WalletOwnerOrganization)

Expand All @@ -350,7 +331,6 @@ func TestWrapper_handleAuthorizeRequestFromVerifier(t *testing.T) {
ctx.iamClient.EXPECT().PresentationDefinition(gomock.Any(), pdEndpoint).Return(&pe.PresentationDefinition{}, nil)
ctx.wallet.EXPECT().BuildSubmission(gomock.Any(), holderDID, pe.PresentationDefinition{}, clientMetadata.VPFormats, gomock.Any()).Return(nil, nil, assert.AnError)
expectPostError(t, ctx, oauth.ServerError, assert.AnError.Error(), responseURI, "state")
_ = ctx.client.userSessionStore().Put(userSessionID, userSession)

_, err := ctx.client.handleAuthorizeRequestFromVerifier(httpRequestCtx, holderDID, params, pe.WalletOwnerOrganization)

Expand All @@ -364,7 +344,6 @@ func TestWrapper_handleAuthorizeRequestFromVerifier(t *testing.T) {
ctx.iamClient.EXPECT().PresentationDefinition(gomock.Any(), pdEndpoint).Return(&pe.PresentationDefinition{}, nil)
ctx.wallet.EXPECT().BuildSubmission(gomock.Any(), holderDID, pe.PresentationDefinition{}, clientMetadata.VPFormats, gomock.Any()).Return(nil, nil, holder.ErrNoCredentials)
expectPostError(t, ctx, oauth.InvalidRequest, "no credentials available (PD ID: , wallet: did:web:example.com:iam:holder)", responseURI, "state")
_ = ctx.client.userSessionStore().Put(userSessionID, userSession)

_, err := ctx.client.handleAuthorizeRequestFromVerifier(httpRequestCtx, holderDID, params, pe.WalletOwnerOrganization)

Expand Down
19 changes: 0 additions & 19 deletions auth/api/iam/session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,32 +19,13 @@
package iam

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/nuts-foundation/nuts-node/vcr/pe"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)

func TestUserWallet_Key(t *testing.T) {
t.Run("ok", func(t *testing.T) {
pk, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
keyAsJWK, err := jwk.FromRaw(pk)
require.NoError(t, err)
jwkAsJSON, _ := json.Marshal(keyAsJWK)
wallet := UserWallet{
JWK: jwkAsJSON,
}
key, err := wallet.Key()
require.NoError(t, err)
assert.Equal(t, keyAsJWK, key)
})
}

func TestOpenID4VPVerifier_next(t *testing.T) {
userPresentationDefinition := PresentationDefinition{
Id: "user",
Expand Down
9 changes: 5 additions & 4 deletions auth/api/iam/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,12 @@ func (r Wrapper) handleUserLanding(echoCtx echo.Context) error {
}

// Make sure there's a user session, loaded with EmployeeCredential
userSession, err := usersession.Get(echoCtx.Request().Context(), accessTokenRequest.Did)
tenantDID, _ := did.ParseDID(accessTokenRequest.Did) // can't fail, since the request was created earlier (thus, validated)
userSession, err := usersession.Get(echoCtx.Request().Context(), *tenantDID)
if err != nil {
return err
}
if err := r.provisionUserSession(echoCtx.Request().Context(), *userSession, *redirectSession.AccessTokenRequest.Body.PreauthorizedUser); err != nil {
if err := r.provisionUserSession(echoCtx.Request().Context(), userSession, *redirectSession.AccessTokenRequest.Body.PreauthorizedUser); err != nil {
return fmt.Errorf("couldn't provision user session: %w", err)
}

Expand Down Expand Up @@ -153,12 +154,12 @@ func (r Wrapper) oauthCodeStore() storage.SessionStore {
return r.storageEngine.GetSessionDatabase().GetStore(oAuthFlowTimeout, oauthCodeKey...)
}

func (r Wrapper) provisionUserSession(ctx context.Context, session usersession.Data, preAuthorizedUser UserDetails) error {
func (r Wrapper) provisionUserSession(ctx context.Context, session *usersession.Data, preAuthorizedUser UserDetails) error {
if len(session.Wallet.Credentials) > 0 {
// already provisioned
return nil
}
employeeCredential, err := r.issueEmployeeCredential(ctx, session, preAuthorizedUser)
employeeCredential, err := r.issueEmployeeCredential(ctx, *session, preAuthorizedUser)
if err != nil {
return err
}
Expand Down
66 changes: 15 additions & 51 deletions auth/api/iam/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,21 +88,22 @@ func TestWrapper_handleUserLanding(t *testing.T) {
RequireSignedRequestObject: true,
}

t.Run("new session", func(t *testing.T) {
t.Run("ok", func(t *testing.T) {
ctx := newTestClient(t)
expectedURL := "https://example.com/authorize?client_id=did%3Aweb%3Aexample.com%3Aiam%3A123&request_uri=https://example.com/oauth2/" + webDID.String() + "/request.jwt/&request_uri_method=get"
echoCtx := mock.NewMockContext(ctx.ctrl)
echoCtx.EXPECT().QueryParam("token").Return("token")
echoCtx.EXPECT().Request().MinTimes(1).Return(&http.Request{Host: "example.com"})
httpRequest := &http.Request{
Host: "example.com",
}
requestCtx, userSession := usersession.CreateTestSession(context.Background(), walletDID)
httpRequest = httpRequest.WithContext(requestCtx)
echoCtx.EXPECT().Request().MinTimes(1).Return(httpRequest)
echoCtx.EXPECT().Redirect(http.StatusFound, gomock.Any()).DoAndReturn(func(_ int, arg1 string) error {
testAuthzReqRedirectURI(t, expectedURL, arg1)
return nil
})
var capturedCookie *http.Cookie
echoCtx.EXPECT().Cookie(gomock.Any()).Return(nil, http.ErrNoCookie)
echoCtx.EXPECT().SetCookie(gomock.Any()).DoAndReturn(func(cookie *http.Cookie) {
capturedCookie = cookie
})

var employeeCredentialTemplate vc.VerifiableCredential
var employeeCredentialOptions issuer.CredentialOptions
ctx.vcIssuer.EXPECT().Issue(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, t vc.VerifiableCredential, o issuer.CredentialOptions) (*vc.VerifiableCredential, error) {
Expand All @@ -126,22 +127,8 @@ func TestWrapper_handleUserLanding(t *testing.T) {
require.NoError(t, err)

err = ctx.client.handleUserLanding(echoCtx)

require.NoError(t, err)
// check security settings of session cookie
assert.Equal(t, "/", capturedCookie.Path)
assert.Equal(t, "__Host-SID", capturedCookie.Name)
assert.Empty(t, capturedCookie.Domain)
assert.Empty(t, capturedCookie.Expires)
assert.NotEmpty(t, capturedCookie.MaxAge)
assert.Equal(t, http.SameSiteStrictMode, capturedCookie.SameSite)
assert.True(t, capturedCookie.Secure)
assert.True(t, capturedCookie.HttpOnly)
// check for issued EmployeeCredential in session wallet

userSession := new(usersession.Data)
err := ctx.client.storageEngine.GetSessionDatabase().GetStore(time.Second, "user", "session").
Get(capturedCookie.Value, &userSession)
require.NoError(t, err)
assert.Equal(t, walletDID, userSession.TenantDID)
require.Len(t, userSession.Wallet.Credentials, 1)
Expand All @@ -167,32 +154,6 @@ func TestWrapper_handleUserLanding(t *testing.T) {
err = store.Get("token", &RedirectSession{})
assert.Error(t, err)
})
t.Run("existing session", func(t *testing.T) {
ctx := newTestClient(t)
expectedURL := "https://example.com/authorize?client_id=did%3Aweb%3Aexample.com%3Aiam%3A123&request_uri=https://example.com/oauth2/" + webDID.String() + "/request.jwt/&request_uri_method="
echoCtx := mock.NewMockContext(ctx.ctrl)
echoCtx.EXPECT().QueryParam("token").Return("token")
echoCtx.EXPECT().Request().MinTimes(1).Return(&http.Request{Host: "example.com"})
echoCtx.EXPECT().Redirect(http.StatusFound, gomock.Any()).DoAndReturn(func(_ int, arg1 string) error {
testAuthzReqRedirectURI(t, expectedURL, arg1)
return nil
})
echoCtx.EXPECT().Cookie(gomock.Any()).Return(&sessionCookie, nil)
ctx.iamClient.EXPECT().AuthorizationServerMetadata(gomock.Any(), verifierDID).Return(&serverMetadata, nil)
ctx.jar.EXPECT().Create(webDID, &verifierDID, gomock.Any())
require.NoError(t, ctx.client.userRedirectStore().Put("token", redirectSession))
session := usersession.Data{
TenantDID: walletDID,
Wallet: usersession.UserWallet{
DID: userDID,
},
}
require.NoError(t, ctx.client.userSessionStore().Put(sessionCookie.Value, session))

err := ctx.client.handleUserLanding(echoCtx)

require.NoError(t, err)
})
t.Run("error - no token", func(t *testing.T) {
ctx := newTestClient(t)
echoCtx := mock.NewMockContext(ctx.ctrl)
Expand Down Expand Up @@ -243,16 +204,19 @@ func TestWrapper_handleUserLanding(t *testing.T) {
})
echoCtx := mock.NewMockContext(ctx.ctrl)
echoCtx.EXPECT().QueryParam("token").Return("token")
echoCtx.EXPECT().Request().MinTimes(1).Return(&http.Request{Host: "example.com"})
echoCtx.EXPECT().Cookie(gomock.Any()).Return(nil, http.ErrNoCookie)
echoCtx.EXPECT().SetCookie(gomock.Any())
httpRequest := &http.Request{
Host: "example.com",
}
requestCtx, _ := usersession.CreateTestSession(context.Background(), walletDID)
httpRequest = httpRequest.WithContext(requestCtx)
echoCtx.EXPECT().Request().MinTimes(1).Return(httpRequest)
store := ctx.client.storageEngine.GetSessionDatabase().GetStore(time.Second*5, "user", "redirect")
err := store.Put("token", redirectSession)
require.NoError(t, err)
ctx.iamClient.EXPECT().AuthorizationServerMetadata(gomock.Any(), verifierDID).Return(nil, assert.AnError)

err = ctx.client.handleUserLanding(echoCtx)

assert.Error(t, err)
assert.ErrorIs(t, err, assert.AnError)
})
}
7 changes: 2 additions & 5 deletions auth/api/iam/usersession/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import (

// Data is a session-bound Verifiable Credential wallet.
type Data struct {
saveFunc func() error
// Save is a function that persists the session.
Save func() error `json:"-"`
// TenantDID is the requesting DID when the user session was created, typically the employer's (of the user) DID.
// A session needs to be scoped to the tenant DID, since the session gives access to the tenant's wallet,
// and the user session might contain session-bound credentials (e.g. EmployeeCredential) that were issued by the tenant.
Expand All @@ -20,10 +21,6 @@ type Data struct {
ExpiresAt time.Time `json:"expiresAt"`
}

func (u Data) Save() error {
return u.saveFunc()
}

// UserWallet is a session-bound Verifiable Credential wallet.
// It's an in-memory wallet which contains the user's private key in plain text.
// This is OK, since the associated credentials are intended for protocol compatibility (OpenID4VP with a low-assurance EmployeeCredential),
Expand Down
Loading

0 comments on commit b10cd44

Please sign in to comment.