mirror of
https://github.com/nais/wonderwall.git
synced 2026-02-14 17:49:54 +00:00
feat(session): add feature toggle for automatic refreshing
This commit is contained in:
@@ -38,7 +38,10 @@ The following flags are available:
|
||||
| `session.inactivity` | boolean | Automatically expire user sessions if they have not refreshed their tokens within a given duration. | |
|
||||
| `session.inactivity-timeout` | duration | Inactivity timeout for user sessions. | `30m` |
|
||||
| `session.max-lifetime` | duration | Max lifetime for user sessions. | `1h` |
|
||||
| `session.refresh` | boolean | Enable refresh tokens. In standalone mode, will automatically refresh tokens if they are expired as long as the session is valid (i.e. not exceeding `session.max-lifetime` or `session.inactivity-timeout`). | |
|
||||
| `session.refresh` | boolean | Enable refresh tokens. | |
|
||||
| `session.refresh-auto` | boolean | Enable automatic refresh of tokens. Only available in standalone mode. Will automatically refresh tokens if they are expired as long as the session is valid (i.e. not exceeding `session.max-lifetime` or `session.inactivity-timeout`). | |
|
||||
| `shutdown-graceful-period` | duration | Graceful shutdown period when receiving a shutdown signal after which the server is forcibly exited. | |
|
||||
| `shutdown-wait-before-period` | duration | Wait period when receiving a shutdown signal before actually starting a graceful shutdown. Useful for allowing propagation of Endpoint updates in Kubernetes. | |
|
||||
| `sso.domain` | string | The domain that the session cookies should be set for, usually the second-level domain name (e.g. `example.com`). | |
|
||||
| `sso.enabled` | boolean | Enable single sign-on mode; one server acting as the OIDC Relying Party, and N proxies. The proxies delegate most endpoint operations to the server, and only implements a reverse proxy that reads the user's session data from the shared store. | |
|
||||
| `sso.mode` | string | The SSO mode for this instance. Must be one of `server` or `proxy`. | `server` |
|
||||
|
||||
@@ -29,12 +29,13 @@ The ability to refresh tokens requires the `session.refresh` flag to be enabled.
|
||||
|
||||
The behaviour for refreshing depends on the [runtime mode](configuration.md#modes) for Wonderwall.
|
||||
|
||||
In standalone mode, tokens will at the _earliest_ automatically be renewed 5 minutes before they expire.
|
||||
In standalone mode, tokens can automatically be refreshed by enabling the `session.refresh-auto` flag.
|
||||
If enabled, token will at the _earliest_ automatically be renewed 5 minutes before they expire.
|
||||
If the token already _has_ expired, a refresh attempt is still automatically triggered as long as the session itself not has ended or is marked as inactive.
|
||||
|
||||
Automatic refreshes happens whenever the end-user visits or requests any path that is proxied to the upstream application.
|
||||
|
||||
In SSO mode, tokens are not automatically refreshed, and must be manually refreshed by performing a request to [the `/oauth2/session/refresh` endpoint](endpoints.md#oauth2sessionrefresh).
|
||||
In SSO mode, tokens can not be automatically refreshed. They must be refreshed by performing a request to [the `/oauth2/session/refresh` endpoint](endpoints.md#oauth2sessionrefresh).
|
||||
|
||||
## Session Inactivity
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ type Session struct {
|
||||
InactivityTimeout time.Duration `json:"inactivity-timeout"`
|
||||
MaxLifetime time.Duration `json:"max-lifetime"`
|
||||
Refresh bool `json:"refresh"`
|
||||
RefreshAuto bool `json:"refresh-auto"`
|
||||
}
|
||||
|
||||
type SSO struct {
|
||||
@@ -86,6 +87,7 @@ const (
|
||||
SessionInactivityTimeout = "session.inactivity-timeout"
|
||||
SessionMaxLifetime = "session.max-lifetime"
|
||||
SessionRefresh = "session.refresh"
|
||||
SessionRefreshAuto = "session.refresh-auto"
|
||||
SSOEnabled = "sso.enabled"
|
||||
SSODomain = "sso.domain"
|
||||
SSOModeFlag = "sso.mode"
|
||||
@@ -116,7 +118,8 @@ func Initialize() (*Config, error) {
|
||||
flag.Bool(SessionInactivity, false, "Automatically expire user sessions if they have not refreshed their tokens within a given duration.")
|
||||
flag.Duration(SessionInactivityTimeout, 30*time.Minute, "Inactivity timeout for user sessions.")
|
||||
flag.Duration(SessionMaxLifetime, time.Hour, "Max lifetime for user sessions.")
|
||||
flag.Bool(SessionRefresh, false, "Enable refresh tokens. In standalone mode, will automatically refresh tokens if they are expired as long as the session is valid (i.e. not exceeding 'session.max-lifetime' or 'session.inactivity-timeout').")
|
||||
flag.Bool(SessionRefresh, false, "Enable refresh tokens.")
|
||||
flag.Bool(SessionRefreshAuto, false, "Enable automatic refresh of tokens. Only available in standalone mode. Will automatically refresh tokens if they are expired as long as the session is valid (i.e. not exceeding 'session.max-lifetime' or 'session.inactivity-timeout').")
|
||||
|
||||
flag.Bool(SSOEnabled, false, "Enable single sign-on mode; one server acting as the OIDC Relying Party, and N proxies. The proxies delegate most endpoint operations to the server, and only implements a reverse proxy that reads the user's session data from the shared store.")
|
||||
flag.String(SSODomain, "", "The domain that the session cookies should be set for, usually the second-level domain name (e.g. example.com).")
|
||||
@@ -186,6 +189,10 @@ func (c *Config) Validate() error {
|
||||
return fmt.Errorf("%q cannot be enabled without %q", SessionInactivity, SessionRefresh)
|
||||
}
|
||||
|
||||
if c.Session.RefreshAuto && !c.Session.Refresh {
|
||||
return fmt.Errorf("%q cannot be enabled without %q", SessionRefreshAuto, SessionRefresh)
|
||||
}
|
||||
|
||||
if c.SSO.Enabled {
|
||||
if len(c.Redis.Address) == 0 {
|
||||
return fmt.Errorf("%q must not be empty when %s is set", RedisAddress, SSOEnabled)
|
||||
@@ -195,6 +202,10 @@ func (c *Config) Validate() error {
|
||||
return fmt.Errorf("%q must not be empty when %s is set", SSOSessionCookieName, SSOEnabled)
|
||||
}
|
||||
|
||||
if c.Session.RefreshAuto {
|
||||
return fmt.Errorf("%q cannot be enabled when %q is enabled", SessionRefreshAuto, SSOEnabled)
|
||||
}
|
||||
|
||||
switch c.SSO.Mode {
|
||||
case SSOModeProxy:
|
||||
_, err := url.ParseRequestURI(c.SSO.ServerURL)
|
||||
|
||||
@@ -388,7 +388,7 @@ func (s *Standalone) sessionWriteMetadataResponse(w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
metadata := sess.MetadataVerboseRefresh()
|
||||
if s.Config.SSO.Enabled {
|
||||
if !s.Config.Session.RefreshAuto {
|
||||
metadata.Tokens.NextAutoRefreshInSeconds = int64(-1)
|
||||
}
|
||||
|
||||
|
||||
@@ -210,9 +210,9 @@ func TestSessionRefresh(t *testing.T) {
|
||||
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, refreshedData.Tokens.NextAutoRefreshInSeconds, refreshedData.Tokens.ExpireInSeconds)
|
||||
assert.Greater(t, refreshedData.Tokens.NextAutoRefreshInSeconds, int64(1))
|
||||
// auto refresh is not enabled
|
||||
assert.Less(t, refreshedData.Tokens.NextAutoRefreshInSeconds, refreshedData.Tokens.ExpireInSeconds)
|
||||
assert.Equal(t, refreshedData.Tokens.NextAutoRefreshInSeconds, int64(-1))
|
||||
|
||||
assert.True(t, refreshedData.Tokens.RefreshCooldown)
|
||||
// 1 second < refresh cooldown <= minimum refresh interval
|
||||
@@ -296,10 +296,43 @@ func TestSessionRefresh_WithInactivity(t *testing.T) {
|
||||
assert.WithinDuration(t, expectedTimeoutAt, time.Now().Add(refreshedTimeoutDuration), maxDelta)
|
||||
}
|
||||
|
||||
func TestSession(t *testing.T) {
|
||||
func TestSessionRefresh_WithRefreshAuto(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.Session.Refresh = true
|
||||
cfg.Session.RefreshAuto = true
|
||||
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
idp.ProviderHandler.TokenDuration = 5 * time.Second
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
login(t, rpClient, idp)
|
||||
|
||||
// get initial session info
|
||||
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)
|
||||
|
||||
// wait until refresh cooldown has reached zero before refresh
|
||||
waitForRefreshCooldownTimer(t, idp, rpClient)
|
||||
|
||||
resp = sessionRefresh(t, idp, rpClient)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var refreshedData session.MetadataVerboseWithRefresh
|
||||
err = json.Unmarshal([]byte(resp.Body), &refreshedData)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 1 second < next token refresh <= seconds until token expires
|
||||
assert.LessOrEqual(t, refreshedData.Tokens.NextAutoRefreshInSeconds, refreshedData.Tokens.ExpireInSeconds)
|
||||
assert.Greater(t, refreshedData.Tokens.NextAutoRefreshInSeconds, int64(1))
|
||||
}
|
||||
|
||||
func TestSession(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
idp.ProviderHandler.TokenDuration = 5 * time.Minute
|
||||
defer idp.Close()
|
||||
@@ -400,9 +433,9 @@ func TestSession_WithRefresh(t *testing.T) {
|
||||
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)
|
||||
assert.Greater(t, data.Tokens.NextAutoRefreshInSeconds, int64(1))
|
||||
// auto refresh is not enabled
|
||||
assert.Less(t, data.Tokens.NextAutoRefreshInSeconds, data.Tokens.ExpireInSeconds)
|
||||
assert.Equal(t, data.Tokens.NextAutoRefreshInSeconds, int64(-1))
|
||||
|
||||
assert.True(t, data.Tokens.RefreshCooldown)
|
||||
// 1 second < refresh cooldown <= minimum refresh interval
|
||||
@@ -414,6 +447,30 @@ func TestSession_WithRefresh(t *testing.T) {
|
||||
assert.Equal(t, int64(-1), data.Session.TimeoutInSeconds)
|
||||
}
|
||||
|
||||
func TestSession_WithRefreshAuto(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.Session.Refresh = true
|
||||
cfg.Session.RefreshAuto = 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)
|
||||
|
||||
// 1 second < next token refresh <= seconds until token expires
|
||||
assert.LessOrEqual(t, data.Tokens.NextAutoRefreshInSeconds, data.Tokens.ExpireInSeconds)
|
||||
assert.Greater(t, data.Tokens.NextAutoRefreshInSeconds, int64(1))
|
||||
}
|
||||
|
||||
func TestPing(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
|
||||
@@ -108,7 +108,7 @@ func (in *manager) GetOrRefresh(r *http.Request) (*Session, error) {
|
||||
return nil, fmt.Errorf("getting session: %w", err)
|
||||
}
|
||||
|
||||
if !sess.shouldRefresh() {
|
||||
if !in.cfg.Session.RefreshAuto || !sess.shouldRefresh() {
|
||||
return sess, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user