From b10cd4495f16cbfb5378ffa1f89f92ee36bec31f Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Fri, 24 May 2024 11:37:00 +0200 Subject: [PATCH] fix tests --- auth/api/iam/api.go | 13 +++- auth/api/iam/api_test.go | 14 ++-- auth/api/iam/openid4vp.go | 3 +- auth/api/iam/openid4vp_test.go | 25 +------ auth/api/iam/session_test.go | 19 ------ auth/api/iam/user.go | 9 +-- auth/api/iam/user_test.go | 66 +++++-------------- auth/api/iam/usersession/data.go | 7 +- auth/api/iam/usersession/data_test.go | 27 ++++++++ auth/api/iam/usersession/test.go | 14 +++- auth/api/iam/usersession/user_session.go | 31 ++++----- auth/api/iam/usersession/user_session_test.go | 23 +++++-- docs/_static/auth/iam.partial.yaml | 4 +- 13 files changed, 115 insertions(+), 140 deletions(-) create mode 100644 auth/api/iam/usersession/data_test.go diff --git a/auth/api/iam/api.go b/auth/api/iam/api.go index 7cee894e07..24b8a239dd 100644 --- a/auth/api/iam/api.go +++ b/auth/api/iam/api.go @@ -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) } diff --git a/auth/api/iam/api_test.go b/auth/api/iam/api_test.go index 2d98230787..e1771c5de6 100644 --- a/auth/api/iam/api_test.go +++ b/auth/api/iam/api_test.go @@ -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" @@ -394,12 +395,7 @@ 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" @@ -407,10 +403,7 @@ func TestWrapper_HandleAuthorizeRequest(t *testing.T) { 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(), }) @@ -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) } diff --git a/auth/api/iam/openid4vp.go b/auth/api/iam/openid4vp.go index b7916ab05c..dc8eff3b29 100644 --- a/auth/api/iam/openid4vp.go +++ b/auth/api/iam/openid4vp.go @@ -23,6 +23,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/nuts-foundation/nuts-node/auth/api/iam/usersession" "net/http" "net/url" "slices" @@ -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"} } diff --git a/auth/api/iam/openid4vp_test.go b/auth/api/iam/openid4vp_test.go index 607f950520..c1ab00688f 100644 --- a/auth/api/iam/openid4vp_test.go +++ b/auth/api/iam/openid4vp_test.go @@ -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" @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) diff --git a/auth/api/iam/session_test.go b/auth/api/iam/session_test.go index 6cdbea08c1..0490ccebf3 100644 --- a/auth/api/iam/session_test.go +++ b/auth/api/iam/session_test.go @@ -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", diff --git a/auth/api/iam/user.go b/auth/api/iam/user.go index 4ea87f9f74..24078a4906 100644 --- a/auth/api/iam/user.go +++ b/auth/api/iam/user.go @@ -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) } @@ -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 } diff --git a/auth/api/iam/user_test.go b/auth/api/iam/user_test.go index 9c8dc1a79f..eef78f3e7e 100644 --- a/auth/api/iam/user_test.go +++ b/auth/api/iam/user_test.go @@ -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) { @@ -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) @@ -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) @@ -243,9 +204,12 @@ 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) @@ -253,6 +217,6 @@ func TestWrapper_handleUserLanding(t *testing.T) { err = ctx.client.handleUserLanding(echoCtx) - assert.Error(t, err) + assert.ErrorIs(t, err, assert.AnError) }) } diff --git a/auth/api/iam/usersession/data.go b/auth/api/iam/usersession/data.go index fdacbe2f03..2b98b0b46a 100644 --- a/auth/api/iam/usersession/data.go +++ b/auth/api/iam/usersession/data.go @@ -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. @@ -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), diff --git a/auth/api/iam/usersession/data_test.go b/auth/api/iam/usersession/data_test.go new file mode 100644 index 0000000000..1999c13ced --- /dev/null +++ b/auth/api/iam/usersession/data_test.go @@ -0,0 +1,27 @@ +package usersession + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/json" + "github.com/lestrrat-go/jwx/v2/jwk" + "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) + }) +} diff --git a/auth/api/iam/usersession/test.go b/auth/api/iam/usersession/test.go index d8ecff0649..e62495e8ff 100644 --- a/auth/api/iam/usersession/test.go +++ b/auth/api/iam/usersession/test.go @@ -1,7 +1,15 @@ package usersession -import "github.com/nuts-foundation/nuts-node/storage" - -func CreateTestSession(sessionStore storage.SessionStore, data *Data) { +import ( + "context" + "github.com/nuts-foundation/go-did/did" + "time" +) +func CreateTestSession(ctx context.Context, tenantDID did.DID) (context.Context, *Data) { + session, _ := createUserSession(tenantDID, time.Hour) + session.Save = func() error { + return nil + } + return context.WithValue(ctx, userSessionContextKey, session), session } diff --git a/auth/api/iam/usersession/user_session.go b/auth/api/iam/usersession/user_session.go index b53abe5091..4fb568066e 100644 --- a/auth/api/iam/usersession/user_session.go +++ b/auth/api/iam/usersession/user_session.go @@ -59,8 +59,11 @@ func (u Middleware) Handle(next echo.HandlerFunc) echo.HandlerFunc { log.Logger().WithError(err).Info("Invalid user session, a new session will be created") } if session == nil { - var sessionID string - sessionID, session, err = u.createUserSession(*tenantDID) + session, err = createUserSession(*tenantDID, u.TimeOut) + sessionID := crypto.GenerateNonce() + if err := u.sessionStore.Put(sessionID, session); err != nil { + return err + } if err != nil { return fmt.Errorf("create user session: %w", err) } @@ -96,36 +99,30 @@ func (u Middleware) loadUserSession(cookies CookieReader, tenantDID did.DID) (*D if !session.TenantDID.Equals(tenantDID) && !session.Wallet.DID.Equals(tenantDID) { return nil, fmt.Errorf("session belongs to another tenant (%s)", session.TenantDID) } - session.saveFunc = func() error { + session.Save = func() error { return u.sessionStore.Put(cookie.Value, session) } return session, nil } -func (u Middleware) createUserSession(tenantDID did.DID) (string, *Data, error) { +func createUserSession(tenantDID did.DID, timeOut time.Duration) (*Data, error) { userJWK, userDID, err := generateUserSessionJWK() if err != nil { - return "", nil, err + return nil, err } userJWKBytes, err := json.Marshal(userJWK) if err != nil { - return "", nil, err + return nil, err } // create user session wallet - data := Data{ + return &Data{ TenantDID: tenantDID, Wallet: UserWallet{ JWK: userJWKBytes, DID: *userDID, }, - ExpiresAt: time.Now().Add(u.TimeOut), - } - - sessionID := crypto.GenerateNonce() - if err := u.sessionStore.Put(sessionID, data); err != nil { - return "", nil, err - } - return sessionID, &data, nil + ExpiresAt: time.Now().Add(timeOut), + }, nil } func userSessionCookiePath(requestURL *url.URL) string { @@ -154,12 +151,12 @@ func (u Middleware) createUserSessionCookie(sessionID string, path string) *http } } -func Get(ctx context.Context, tenantDID string) (*Data, error) { +func Get(ctx context.Context, expectedTenantDID did.DID) (*Data, error) { result, ok := ctx.Value(userSessionContextKey).(*Data) if !ok { return nil, errors.New("no user session found") } - if result.TenantDID.String() != tenantDID { + if result.TenantDID.String() != expectedTenantDID.String() { return nil, errors.New("user session belongs to another tenant") } return result, nil diff --git a/auth/api/iam/usersession/user_session_test.go b/auth/api/iam/usersession/user_session_test.go index 306f539184..d6485a7746 100644 --- a/auth/api/iam/usersession/user_session_test.go +++ b/auth/api/iam/usersession/user_session_test.go @@ -47,7 +47,7 @@ func TestMiddleware_Handle(t *testing.T) { var capturedSession *Data err := instance.Handle(func(c echo.Context) error { var err error - capturedSession, err = Get(c.Request().Context(), tenantDID.String()) + capturedSession, err = Get(c.Request().Context(), tenantDID) return err })(echoContext) @@ -79,7 +79,7 @@ func TestMiddleware_Handle(t *testing.T) { var capturedSession *Data err := instance.Handle(func(c echo.Context) error { var err error - capturedSession, err = Get(c.Request().Context(), tenantDID.String()) + capturedSession, err = Get(c.Request().Context(), tenantDID) return err })(echoContext) @@ -148,7 +148,7 @@ func TestMiddleware_Handle(t *testing.T) { var capturedSession *Data err := instance.Handle(func(c echo.Context) error { var err error - capturedSession, err = Get(c.Request().Context(), tenantDID.String()) + capturedSession, err = Get(c.Request().Context(), tenantDID) return err })(echoContext) @@ -173,7 +173,7 @@ func TestMiddleware_loadUserSession(t *testing.T) { actual, err := instance.loadUserSession((*testCookieReader)(&sessionCookie), tenantDID) assert.NoError(t, err) - actual.saveFunc = nil // otherwise, comparison fails + actual.Save = nil // otherwise, comparison fails assert.Equal(t, expected, *actual) assert.False(t, actual.Wallet.DID.Empty()) }) @@ -235,3 +235,18 @@ func createInstance(t *testing.T) (Middleware, storage.SessionStore) { sessionStore: store, }, store } + +func TestMiddleware_createUserSessionCookie(t *testing.T) { + cookie := Middleware{ + TimeOut: 30 * time.Minute, + }.createUserSessionCookie("sessionID", "/iam/did:web:example.com:iam:123") + + assert.Equal(t, "/iam/did:web:example.com:iam:123", cookie.Path) + assert.Equal(t, "__Host-SID", cookie.Name) + assert.Empty(t, cookie.Domain) + assert.Empty(t, cookie.Expires) + assert.Equal(t, 30*time.Minute, time.Duration(cookie.MaxAge)*time.Second) + assert.Equal(t, http.SameSiteStrictMode, cookie.SameSite) + assert.True(t, cookie.Secure) + assert.True(t, cookie.HttpOnly) +} diff --git a/docs/_static/auth/iam.partial.yaml b/docs/_static/auth/iam.partial.yaml index 92c6b36646..12a4a14b12 100644 --- a/docs/_static/auth/iam.partial.yaml +++ b/docs/_static/auth/iam.partial.yaml @@ -44,7 +44,7 @@ paths: description: DID does not exist. /oauth2/{did}/token: post: - summary: Used by to request access- or refresh tokens. + summary: Used by the OAuth2 client (backend, not the browser) to request access- or refresh tokens. description: | Specified by https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-token-endpoint. Requires the use of PKCE as specified by https://datatracker.ietf.org/doc/html/rfc7636 and optionally DPoP as specified by https://datatracker.ietf.org/doc/html/rfc9449. @@ -99,7 +99,7 @@ paths: $ref: "#/components/schemas/ErrorResponse" /oauth2/{did}/authorize: get: - summary: Used by resource owners to initiate the authorization code flow. + summary: Used by resource owners (the browser) to initiate the authorization code flow. description: Specified by https://datatracker.ietf.org/doc/html/rfc6749#section-3.1 operationId: handleAuthorizeRequest tags: