From 619ae52d4573047cfa98a2f0704171044bf24de2 Mon Sep 17 00:00:00 2001 From: Trong Huu Nguyen Date: Wed, 31 Aug 2022 15:54:13 +0200 Subject: [PATCH] refactor: separate refresh-specific fields from session info; enable endpoint without refresh feature --- README.md | 12 +++--- pkg/handler/handler_session_info.go | 8 +++- pkg/handler/handler_session_refresh.go | 2 +- pkg/handler/handler_test.go | 56 +++++++++++++++++--------- pkg/router/router.go | 2 +- pkg/session/data.go | 30 ++++++++++++-- pkg/session/data_test.go | 20 ++++++++- 7 files changed, 98 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 6bc2b1f..31c05c1 100644 --- a/README.md +++ b/README.md @@ -42,12 +42,12 @@ Wonderwall exposes and owns these endpoints (which means they will never be prox Endpoints that are available for use by applications: -| Path | Description | -|---------------------------|------------------------------------------------------| -| `/oauth2/login` | Initiates the OpenID Connect Authorization Code flow | -| `/oauth2/logout` | Initiates local and global/single-logout | -| `/oauth2/session` | Returns the current user's session metadata | -| `/oauth2/session/refresh` | Refreshes the tokens for the user's session | +| Path | Description | +|---------------------------|------------------------------------------------------------------------------------------------| +| `/oauth2/login` | Initiates the OpenID Connect Authorization Code flow | +| `/oauth2/logout` | Initiates local and global/single-logout | +| `/oauth2/session` | Returns the current user's session metadata | +| `/oauth2/session/refresh` | Refreshes the tokens for the user's session. Requires the `session.refresh` flag to be enabled | Endpoints that should be registered at and only be triggered by identity providers: diff --git a/pkg/handler/handler_session_info.go b/pkg/handler/handler_session_info.go index 757da66..48acbc2 100644 --- a/pkg/handler/handler_session_info.go +++ b/pkg/handler/handler_session_info.go @@ -27,7 +27,13 @@ func (h *Handler) SessionInfo(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/json") - err = json.NewEncoder(w).Encode(data.Metadata.Verbose()) + + if h.Config.Session.Refresh { + err = json.NewEncoder(w).Encode(data.Metadata.VerboseWithRefresh()) + } else { + err = json.NewEncoder(w).Encode(data.Metadata.Verbose()) + } + if err != nil { logger.Warnf("session/info: marshalling metadata: %+v", err) w.WriteHeader(http.StatusInternalServerError) diff --git a/pkg/handler/handler_session_refresh.go b/pkg/handler/handler_session_refresh.go index 0fdb879..264ecf5 100644 --- a/pkg/handler/handler_session_refresh.go +++ b/pkg/handler/handler_session_refresh.go @@ -41,7 +41,7 @@ func (h *Handler) SessionRefresh(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/json") - err = json.NewEncoder(w).Encode(data.Metadata.Verbose()) + err = json.NewEncoder(w).Encode(data.Metadata.VerboseWithRefresh()) if err != nil { logger.Warnf("session/refresh: marshalling metadata: %+v", err) w.WriteHeader(http.StatusInternalServerError) diff --git a/pkg/handler/handler_test.go b/pkg/handler/handler_test.go index ad7627b..385b3be 100644 --- a/pkg/handler/handler_test.go +++ b/pkg/handler/handler_test.go @@ -188,6 +188,41 @@ func TestHandler_SessionInfo(t *testing.T) { // 1 second < time until token expires <= max duration for tokens from IDP assert.LessOrEqual(t, tokenExpiryDuration, idp.ProviderHandler.TokenDuration) assert.Greater(t, tokenExpiryDuration, time.Second) +} + +func TestHandler_SessionInfo_WithRefresh(t *testing.T) { + cfg := mock.Config() + cfg.Session.Refresh = true + + idp := mock.NewIdentityProvider(cfg) + idp.ProviderHandler.TokenDuration = 5 * time.Minute + defer idp.Close() + + rpClient := idp.RelyingPartyClient() + login(t, rpClient, idp) + + resp := sessionInfo(t, idp, rpClient) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var data session.MetadataVerboseWithRefresh + err := json.Unmarshal([]byte(resp.Body), &data) + assert.NoError(t, err) + + allowedSkew := 5 * time.Second + assert.WithinDuration(t, time.Now(), data.Session.CreatedAt, allowedSkew) + assert.WithinDuration(t, time.Now().Add(cfg.Session.MaxLifetime), data.Session.EndsAt, allowedSkew) + assert.WithinDuration(t, time.Now().Add(idp.ProviderHandler.TokenDuration), data.Tokens.ExpireAt, allowedSkew) + assert.WithinDuration(t, time.Now(), data.Tokens.RefreshedAt, allowedSkew) + + sessionEndDuration := time.Duration(data.Session.EndsInSeconds) * time.Second + // 1 second < time until session ends <= configured max session lifetime + assert.LessOrEqual(t, sessionEndDuration, cfg.Session.MaxLifetime) + assert.Greater(t, sessionEndDuration, time.Second) + + tokenExpiryDuration := time.Duration(data.Tokens.ExpireInSeconds) * time.Second + // 1 second < time until token expires <= max duration for tokens from IDP + assert.LessOrEqual(t, tokenExpiryDuration, idp.ProviderHandler.TokenDuration) + assert.Greater(t, tokenExpiryDuration, time.Second) // 1 second < next token refresh <= seconds until token expires assert.LessOrEqual(t, data.Tokens.NextAutoRefreshInSeconds, data.Tokens.ExpireInSeconds) @@ -199,21 +234,6 @@ func TestHandler_SessionInfo(t *testing.T) { assert.Greater(t, data.Tokens.RefreshCooldownSeconds, int64(1)) } -func TestHandler_SessionInfo_Disabled(t *testing.T) { - cfg := mock.Config() - cfg.Session.Refresh = false - - idp := mock.NewIdentityProvider(cfg) - idp.ProviderHandler.TokenDuration = 5 * time.Second - defer idp.Close() - - rpClient := idp.RelyingPartyClient() - login(t, rpClient, idp) - - resp := sessionInfo(t, idp, rpClient) - assert.Equal(t, http.StatusNotFound, resp.StatusCode) -} - func TestHandler_SessionRefresh(t *testing.T) { cfg := mock.Config() cfg.Session.Refresh = true @@ -229,7 +249,7 @@ func TestHandler_SessionRefresh(t *testing.T) { resp := sessionInfo(t, idp, rpClient) assert.Equal(t, http.StatusOK, resp.StatusCode) - var data session.MetadataVerbose + var data session.MetadataVerboseWithRefresh err := json.Unmarshal([]byte(resp.Body), &data) assert.NoError(t, err) @@ -245,7 +265,7 @@ func TestHandler_SessionRefresh(t *testing.T) { resp := sessionInfo(t, idp, rpClient) assert.Equal(t, http.StatusOK, resp.StatusCode) - var temp session.MetadataVerbose + var temp session.MetadataVerboseWithRefresh err = json.Unmarshal([]byte(resp.Body), &temp) assert.NoError(t, err) @@ -259,7 +279,7 @@ func TestHandler_SessionRefresh(t *testing.T) { resp = sessionRefresh(t, idp, rpClient) assert.Equal(t, http.StatusOK, resp.StatusCode) - var refreshedData session.MetadataVerbose + var refreshedData session.MetadataVerboseWithRefresh err = json.Unmarshal([]byte(resp.Body), &refreshedData) assert.NoError(t, err) diff --git a/pkg/router/router.go b/pkg/router/router.go index 1b3036d..bb0224a 100644 --- a/pkg/router/router.go +++ b/pkg/router/router.go @@ -36,9 +36,9 @@ func New(handler *handler.Handler) chi.Router { r.Get(paths.Logout, handler.Logout) r.Get(paths.FrontChannelLogout, handler.FrontChannelLogout) r.Get(paths.LogoutCallback, handler.LogoutCallback) + r.Get(paths.Session, handler.SessionInfo) if handler.Config.Session.Refresh { - r.Get(paths.Session, handler.SessionInfo) r.Get(paths.SessionRefresh, handler.SessionRefresh) } }) diff --git a/pkg/session/data.go b/pkg/session/data.go index 4976b63..37abcda 100644 --- a/pkg/session/data.go +++ b/pkg/session/data.go @@ -187,7 +187,6 @@ func (in *Metadata) Verbose() MetadataVerbose { expireTime := in.Tokens.ExpireAt endTime := in.Session.EndsAt - nextRefreshTime := in.NextRefresh() return MetadataVerbose{ Session: MetadataSessionVerbose{ @@ -195,8 +194,22 @@ func (in *Metadata) Verbose() MetadataVerbose { EndsInSeconds: toSeconds(endTime.Sub(now)), }, Tokens: MetadataTokensVerbose{ - MetadataTokens: in.Tokens, - ExpireInSeconds: toSeconds(expireTime.Sub(now)), + MetadataTokens: in.Tokens, + ExpireInSeconds: toSeconds(expireTime.Sub(now)), + }, + } +} + +func (in *Metadata) VerboseWithRefresh() MetadataVerboseWithRefresh { + now := time.Now() + + verbose := in.Verbose() + nextRefreshTime := in.NextRefresh() + + return MetadataVerboseWithRefresh{ + Session: verbose.Session, + Tokens: MetadataTokensVerboseWithRefresh{ + MetadataTokensVerbose: verbose.Tokens, NextAutoRefreshInSeconds: toSeconds(nextRefreshTime.Sub(now)), RefreshCooldown: in.IsRefreshOnCooldown(), RefreshCooldownSeconds: toSeconds(in.RefreshCooldown().Sub(now)), @@ -209,6 +222,11 @@ type MetadataVerbose struct { Tokens MetadataTokensVerbose `json:"tokens"` } +type MetadataVerboseWithRefresh struct { + Session MetadataSessionVerbose `json:"session"` + Tokens MetadataTokensVerboseWithRefresh `json:"tokens"` +} + type MetadataSessionVerbose struct { MetadataSession EndsInSeconds int64 `json:"ends_in_seconds"` @@ -216,7 +234,11 @@ type MetadataSessionVerbose struct { type MetadataTokensVerbose struct { MetadataTokens - ExpireInSeconds int64 `json:"expire_in_seconds"` + ExpireInSeconds int64 `json:"expire_in_seconds"` +} + +type MetadataTokensVerboseWithRefresh struct { + MetadataTokensVerbose NextAutoRefreshInSeconds int64 `json:"next_auto_refresh_in_seconds"` RefreshCooldown bool `json:"refresh_cooldown"` RefreshCooldownSeconds int64 `json:"refresh_cooldown_seconds"` diff --git a/pkg/session/data_test.go b/pkg/session/data_test.go index 030553d..d4c1123 100644 --- a/pkg/session/data_test.go +++ b/pkg/session/data_test.go @@ -214,6 +214,24 @@ func TestMetadata_Verbose(t *testing.T) { expected = time.Now().Add(tokenLifetime) actual = time.Now().Add(durationSeconds(verbose.Tokens.ExpireInSeconds)) assert.WithinDuration(t, expected, actual, maxDelta) +} + +func TestMetadata_VerboseWithRefresh(t *testing.T) { + tokenLifetime := 30 * time.Minute + sessionLifetime := time.Hour + + metadata := session.NewMetadata(tokenLifetime, sessionLifetime) + + verbose := metadata.VerboseWithRefresh() + maxDelta := time.Second + + expected := time.Now().Add(sessionLifetime) + actual := time.Now().Add(durationSeconds(verbose.Session.EndsInSeconds)) + assert.WithinDuration(t, expected, actual, maxDelta) + + expected = time.Now().Add(tokenLifetime) + actual = time.Now().Add(durationSeconds(verbose.Tokens.ExpireInSeconds)) + assert.WithinDuration(t, expected, actual, maxDelta) expected = time.Now().Add(tokenLifetime).Add(-session.RefreshLeeway) actual = time.Now().Add(durationSeconds(verbose.Tokens.NextAutoRefreshInSeconds)) @@ -230,7 +248,7 @@ func TestMetadata_Verbose(t *testing.T) { t.Run("refresh not on cooldown", func(t *testing.T) { metadata := session.NewMetadata(tokenLifetime, sessionLifetime) metadata.Tokens.RefreshedAt = time.Now().Add(-5 * time.Minute) - verbose := metadata.Verbose() + verbose := metadata.VerboseWithRefresh() assert.False(t, verbose.Tokens.RefreshCooldown) assert.Equal(t, int64(0), verbose.Tokens.RefreshCooldownSeconds)