refactor: separate refresh-specific fields from session info; enable endpoint without refresh feature

This commit is contained in:
Trong Huu Nguyen
2022-08-31 15:54:13 +02:00
parent 06b71cf56d
commit 619ae52d45
7 changed files with 98 additions and 32 deletions

View File

@@ -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:

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)
}
})

View File

@@ -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"`

View File

@@ -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)