From 2456f0550a31c15c58d96756d42f722dc9d0c44a Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 2 Feb 2023 13:33:52 +0000 Subject: [PATCH 1/6] feat: add session expiry control flags Adds --session-duration which lets admins customize the default session expiration for browser sessions. Adds --disable-session-expiry-refresh which allows admins to prevent session expiry from being automatically bumped upon the API key being used. --- cli/deployment/config.go | 14 ++++++++++++- coderd/apikey.go | 9 ++++++-- coderd/apikey_test.go | 33 ++++++++++++++++++++++++++++++ coderd/coderd.go | 31 ++++++++++++++++------------ coderd/database/dbgen/generator.go | 6 ++++-- coderd/httpmw/apikey.go | 17 ++++++++------- coderd/httpmw/apikey_test.go | 32 +++++++++++++++++++++++++++++ coderd/workspaceapps.go | 17 ++++++--------- codersdk/deployment.go | 2 ++ 9 files changed, 125 insertions(+), 36 deletions(-) diff --git a/cli/deployment/config.go b/cli/deployment/config.go index df739d02decea..5b04e2cd1fd84 100644 --- a/cli/deployment/config.go +++ b/cli/deployment/config.go @@ -479,7 +479,7 @@ func newConfig() *codersdk.DeploymentConfig { }, MaxTokenLifetime: &codersdk.DeploymentConfigField[time.Duration]{ Name: "Max Token Lifetime", - Usage: "The maximum lifetime duration for any user creating a token.", + Usage: "The maximum lifetime duration users can specify when creating an API token.", Flag: "max-token-lifetime", Default: 24 * 30 * time.Hour, }, @@ -531,6 +531,18 @@ func newConfig() *codersdk.DeploymentConfig { Flag: "disable-path-apps", Default: false, }, + SessionDuration: &codersdk.DeploymentConfigField[time.Duration]{ + Name: "Session Duration", + Usage: "The token expiry duration for browser sessions. Sessions may last longer if they are actively making requests, but this functionality can be disabled via --disable-session-expiry-refresh.", + Flag: "session-duration", + Default: 24 * time.Hour, + }, + DisableSessionExpiryRefresh: &codersdk.DeploymentConfigField[bool]{ + Name: "Disable Session Expiry Refresh", + Usage: "Disable automatic session expiry bumping due to activity. This forces all sessions to become invalid after the session expiry duration has been reached.", + Flag: "disable-session-expiry-refresh", + Default: false, + }, } } diff --git a/coderd/apikey.go b/coderd/apikey.go index 5a8d7882eb7ad..49fe08fc9baaf 100644 --- a/coderd/apikey.go +++ b/coderd/apikey.go @@ -288,14 +288,19 @@ func (api *API) createAPIKey(ctx context.Context, params createAPIKeyParams) (*h } hashed := sha256.Sum256([]byte(keySecret)) - // Default expires at to now+lifetime, or just 24hrs if not set + // Default expires at to now+lifetime, or use the configured value if not + // set. if params.ExpiresAt.IsZero() { if params.LifetimeSeconds != 0 { params.ExpiresAt = database.Now().Add(time.Duration(params.LifetimeSeconds) * time.Second) } else { - params.ExpiresAt = database.Now().Add(24 * time.Hour) + params.ExpiresAt = database.Now().Add(api.DeploymentConfig.SessionDuration.Value) + params.LifetimeSeconds = int64(api.DeploymentConfig.SessionDuration.Value.Seconds()) } } + if params.LifetimeSeconds == 0 { + params.LifetimeSeconds = int64(time.Until(params.ExpiresAt).Seconds()) + } ip := net.ParseIP(params.RemoteAddr) if ip == nil { diff --git a/coderd/apikey_test.go b/coderd/apikey_test.go index 6d4b710984b4e..bbefd31ec7785 100644 --- a/coderd/apikey_test.go +++ b/coderd/apikey_test.go @@ -2,9 +2,11 @@ package coderd_test import ( "context" + "net/http" "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/coder/coder/coderd/coderdtest" @@ -109,6 +111,37 @@ func TestTokenMaxLifetime(t *testing.T) { require.ErrorContains(t, err, "lifetime must be less") } +func TestSessionExpiry(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + dc := coderdtest.DeploymentConfig(t) + adminClient := coderdtest.New(t, &coderdtest.Options{ + DeploymentConfig: dc, + }) + adminUser := coderdtest.CreateFirstUser(t, adminClient) + + // This is a hack, but we need the admin account to have a long expiry + // otherwise the test will flake, so we only update the expiry config after + // the admin account has been created. + // + // We don't support updating the deployment config after startup, but for + // this test it works because we don't copy the value (and we use pointers). + dc.SessionDuration.Value = time.Second + + userClient := coderdtest.CreateAnotherUser(t, adminClient, adminUser.OrganizationID) + time.Sleep(dc.SessionDuration.Value + time.Second) + + _, err := userClient.User(ctx, codersdk.Me) + require.Error(t, err) + var sdkErr *codersdk.Error + if assert.ErrorAs(t, err, &sdkErr) { + require.Equal(t, http.StatusUnauthorized, sdkErr.StatusCode()) + require.Contains(t, sdkErr.Message, "session has expired") + } +} + func TestAPIKey(t *testing.T) { t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) diff --git a/coderd/coderd.go b/coderd/coderd.go index 179d10498b478..b10497518b714 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -248,17 +248,19 @@ func New(options *Options) *API { } apiKeyMiddleware := httpmw.ExtractAPIKey(httpmw.ExtractAPIKeyConfig{ - DB: options.Database, - OAuth2Configs: oauthConfigs, - RedirectToLogin: false, - Optional: false, + DB: options.Database, + OAuth2Configs: oauthConfigs, + RedirectToLogin: false, + DisableSessionExpiryRefresh: options.DeploymentConfig.DisableSessionExpiryRefresh.Value, + Optional: false, }) // Same as above but it redirects to the login page. apiKeyMiddlewareRedirect := httpmw.ExtractAPIKey(httpmw.ExtractAPIKeyConfig{ - DB: options.Database, - OAuth2Configs: oauthConfigs, - RedirectToLogin: true, - Optional: false, + DB: options.Database, + OAuth2Configs: oauthConfigs, + RedirectToLogin: true, + DisableSessionExpiryRefresh: options.DeploymentConfig.DisableSessionExpiryRefresh.Value, + Optional: false, }) // API rate limit middleware. The counter is local and not shared between @@ -283,8 +285,9 @@ func New(options *Options) *API { OAuth2Configs: oauthConfigs, // The code handles the the case where the user is not // authenticated automatically. - RedirectToLogin: false, - Optional: true, + RedirectToLogin: false, + DisableSessionExpiryRefresh: options.DeploymentConfig.DisableSessionExpiryRefresh.Value, + Optional: true, }), httpmw.ExtractUserParam(api.Database, false), httpmw.ExtractWorkspaceAndAgentParam(api.Database), @@ -310,8 +313,9 @@ func New(options *Options) *API { // Optional is true to allow for public apps. If an // authorization check fails and the user is not authenticated, // they will be redirected to the login page by the app handler. - RedirectToLogin: false, - Optional: true, + RedirectToLogin: false, + DisableSessionExpiryRefresh: options.DeploymentConfig.DisableSessionExpiryRefresh.Value, + Optional: true, }), // Redirect to the login page if the user tries to open an app with // "me" as the username and they are not logged in. @@ -669,7 +673,8 @@ type API struct { WorkspaceClientCoordinateOverride atomic.Pointer[func(rw http.ResponseWriter) bool] TailnetCoordinator atomic.Pointer[tailnet.Coordinator] QuotaCommitter atomic.Pointer[proto.QuotaCommitter] - HTTPAuth *HTTPAuthorizer + + HTTPAuth *HTTPAuthorizer // APIHandler serves "/api/v2" APIHandler chi.Router diff --git a/coderd/database/dbgen/generator.go b/coderd/database/dbgen/generator.go index 2d3c420fc6784..acf67e802577b 100644 --- a/coderd/database/dbgen/generator.go +++ b/coderd/database/dbgen/generator.go @@ -9,13 +9,15 @@ import ( "testing" "time" - "github.com/coder/coder/cryptorand" "github.com/tabbed/pqtype" - "github.com/coder/coder/coderd/database" + "github.com/coder/coder/cryptorand" + "github.com/google/uuid" "github.com/moby/moby/pkg/namesgenerator" "github.com/stretchr/testify/require" + + "github.com/coder/coder/coderd/database" ) // All methods take in a 'seed' object. Any provided fields in the seed will be diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go index 5899cc6e074ea..a498399f48967 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -88,9 +88,10 @@ const ( ) type ExtractAPIKeyConfig struct { - DB database.Store - OAuth2Configs *OAuth2Configs - RedirectToLogin bool + DB database.Store + OAuth2Configs *OAuth2Configs + RedirectToLogin bool + DisableSessionExpiryRefresh bool // Optional governs whether the API key is optional. Use this if you want to // allow unauthenticated requests. @@ -266,10 +267,12 @@ func ExtractAPIKey(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler { } // Only update the ExpiresAt once an hour to prevent database spam. // We extend the ExpiresAt to reduce re-authentication. - apiKeyLifetime := time.Duration(key.LifetimeSeconds) * time.Second - if key.ExpiresAt.Sub(now) <= apiKeyLifetime-time.Hour { - key.ExpiresAt = now.Add(apiKeyLifetime) - changed = true + if !cfg.DisableSessionExpiryRefresh { + apiKeyLifetime := time.Duration(key.LifetimeSeconds) * time.Second + if key.ExpiresAt.Sub(now) <= apiKeyLifetime-time.Hour { + key.ExpiresAt = now.Add(apiKeyLifetime) + changed = true + } } if changed { err := cfg.DB.UpdateAPIKeyByID(r.Context(), database.UpdateAPIKeyByIDParams{ diff --git a/coderd/httpmw/apikey_test.go b/coderd/httpmw/apikey_test.go index 425999eb9f6f9..7514bcfce7f58 100644 --- a/coderd/httpmw/apikey_test.go +++ b/coderd/httpmw/apikey_test.go @@ -363,6 +363,38 @@ func TestAPIKey(t *testing.T) { require.NotEqual(t, sentAPIKey.ExpiresAt, gotAPIKey.ExpiresAt) }) + t.Run("NoRefresh", func(t *testing.T) { + t.Parallel() + var ( + db = databasefake.New() + user = dbgen.User(t, db, database.User{}) + sentAPIKey, token = dbgen.APIKey(t, db, database.APIKey{ + UserID: user.ID, + LastUsed: database.Now().AddDate(0, 0, -1), + ExpiresAt: database.Now().AddDate(0, 0, 1), + }) + + r = httptest.NewRequest("GET", "/", nil) + rw = httptest.NewRecorder() + ) + r.Header.Set(codersdk.SessionTokenHeader, token) + + httpmw.ExtractAPIKey(httpmw.ExtractAPIKeyConfig{ + DB: db, + RedirectToLogin: false, + DisableSessionExpiryRefresh: true, + })(successHandler).ServeHTTP(rw, r) + res := rw.Result() + defer res.Body.Close() + require.Equal(t, http.StatusOK, res.StatusCode) + + gotAPIKey, err := db.GetAPIKeyByID(r.Context(), sentAPIKey.ID) + require.NoError(t, err) + + require.NotEqual(t, sentAPIKey.LastUsed, gotAPIKey.LastUsed) + require.Equal(t, sentAPIKey.ExpiresAt, gotAPIKey.ExpiresAt) + }) + t.Run("OAuthNotExpired", func(t *testing.T) { t.Parallel() var ( diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index 168345bc2d27e..70ac372cb42df 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -733,23 +733,18 @@ func (api *API) workspaceApplicationAuth(rw http.ResponseWriter, r *http.Request } // Create the application_connect-scoped API key with the same lifetime as - // the current session (defaulting to 1 day, capped to 1 week). + // the current session. exp := apiKey.ExpiresAt - if exp.IsZero() { - exp = database.Now().Add(time.Hour * 24) - } - if time.Until(exp) > time.Hour*24*7 { - exp = database.Now().Add(time.Hour * 24 * 7) - } - lifetime := apiKey.LifetimeSeconds - if lifetime > int64((time.Hour * 24 * 7).Seconds()) { - lifetime = int64((time.Hour * 24 * 7).Seconds()) + lifetimeSeconds := apiKey.LifetimeSeconds + if exp.IsZero() || time.Until(exp) > api.DeploymentConfig.SessionDuration.Value { + exp = database.Now().Add(api.DeploymentConfig.SessionDuration.Value) + lifetimeSeconds = int64(api.DeploymentConfig.SessionDuration.Value.Seconds()) } cookie, err := api.createAPIKey(ctx, createAPIKeyParams{ UserID: apiKey.UserID, LoginType: database.LoginTypePassword, ExpiresAt: exp, - LifetimeSeconds: lifetime, + LifetimeSeconds: lifetimeSeconds, Scope: database.APIKeyScopeApplicationConnect, }) if err != nil { diff --git a/codersdk/deployment.go b/codersdk/deployment.go index b9778aba2aba6..770627a4430c5 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -141,6 +141,8 @@ type DeploymentConfig struct { Logging *LoggingConfig `json:"logging" typescript:",notnull"` Dangerous *DangerousConfig `json:"dangerous" typescript:",notnull"` DisablePathApps *DeploymentConfigField[bool] `json:"disable_path_apps" typescript:",notnull"` + SessionDuration *DeploymentConfigField[time.Duration] `json:"max_session_expiry" typescript:",notnull"` + DisableSessionExpiryRefresh *DeploymentConfigField[bool] `json:"disable_session_expiry_refresh" typescript:",notnull"` // DEPRECATED: Use HTTPAddress or TLS.Address instead. Address *DeploymentConfigField[string] `json:"address" typescript:",notnull"` From 8df63c02276fa2f4dadadd8e4ab03923aaffd876 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 2 Feb 2023 13:41:45 +0000 Subject: [PATCH 2/6] fixup! feat: add session expiry control flags --- cli/testdata/coder_server_--help.golden | 16 ++++++++++++++-- coderd/apidoc/docs.go | 6 ++++++ coderd/apidoc/swagger.json | 6 ++++++ docs/api/general.md | 22 ++++++++++++++++++++++ docs/api/schemas.md | 24 ++++++++++++++++++++++++ docs/cli/coder_server.md | 6 +++++- site/src/api/typesGenerated.ts | 2 ++ 7 files changed, 79 insertions(+), 3 deletions(-) diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 92c01cc953958..5b64f30b21a12 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -91,6 +91,11 @@ Flags: recommended for security purposes if a --wildcard-access-url is configured. Consumes $CODER_DISABLE_PATH_APPS + --disable-session-expiry-refresh Disable automatic session expiry bumping + due to activity. This forces all sessions + to become invalid after the session + expiry duration has been reached. + Consumes $CODER_DISABLE_SESSION_EXPIRY_REFRESH --experiments strings Enable one or more experiments. These are not ready for production. Separate multiple experiments with commas, or @@ -111,8 +116,8 @@ Flags: --log-stackdriver string Output Stackdriver compatible logs to a given file. Consumes $CODER_LOGGING_STACKDRIVER - --max-token-lifetime duration The maximum lifetime duration for any - user creating a token. + --max-token-lifetime duration The maximum lifetime duration users can + specify when creating an API token. Consumes $CODER_MAX_TOKEN_LIFETIME (default 720h0m0s) --oauth2-github-allow-everyone Allow all logins, setting this option @@ -219,6 +224,13 @@ Flags: --secure-auth-cookie Controls if the 'Secure' property is set on browser session cookies. Consumes $CODER_SECURE_AUTH_COOKIE + --session-duration duration The token expiry duration for browser + sessions. Sessions may last longer if + they are actively making requests, but + this functionality can be disabled via + --disable-session-expiry-refresh. + Consumes $CODER_MAX_SESSION_EXPIRY + (default 24h0m0s) --ssh-keygen-algorithm string The algorithm to use for generating ssh keys. Accepted values are "ed25519", "ecdsa", or "rsa4096". diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 4025b8338820a..b5e807b90594a 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -6001,6 +6001,9 @@ const docTemplate = `{ "disable_path_apps": { "$ref": "#/definitions/codersdk.DeploymentConfigField-bool" }, + "disable_session_expiry_refresh": { + "$ref": "#/definitions/codersdk.DeploymentConfigField-bool" + }, "experimental": { "description": "DEPRECATED: Use Experiments instead.", "allOf": [ @@ -6024,6 +6027,9 @@ const docTemplate = `{ "logging": { "$ref": "#/definitions/codersdk.LoggingConfig" }, + "max_session_expiry": { + "$ref": "#/definitions/codersdk.DeploymentConfigField-time_Duration" + }, "max_token_lifetime": { "$ref": "#/definitions/codersdk.DeploymentConfigField-time_Duration" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index cfa10bc868962..18cfaead54ce9 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -5332,6 +5332,9 @@ "disable_path_apps": { "$ref": "#/definitions/codersdk.DeploymentConfigField-bool" }, + "disable_session_expiry_refresh": { + "$ref": "#/definitions/codersdk.DeploymentConfigField-bool" + }, "experimental": { "description": "DEPRECATED: Use Experiments instead.", "allOf": [ @@ -5355,6 +5358,9 @@ "logging": { "$ref": "#/definitions/codersdk.LoggingConfig" }, + "max_session_expiry": { + "$ref": "#/definitions/codersdk.DeploymentConfigField-time_Duration" + }, "max_token_lifetime": { "$ref": "#/definitions/codersdk.DeploymentConfigField-time_Duration" }, diff --git a/docs/api/general.md b/docs/api/general.md index eab1ffbd4c6e4..1fc703b9a2354 100644 --- a/docs/api/general.md +++ b/docs/api/general.md @@ -300,6 +300,17 @@ curl -X GET http://coder-server:8080/api/v2/config/deployment \ "usage": "string", "value": true }, + "disable_session_expiry_refresh": { + "default": true, + "enterprise": true, + "flag": "string", + "hidden": true, + "name": "string", + "secret": true, + "shorthand": "string", + "usage": "string", + "value": true + }, "experimental": { "default": true, "enterprise": true, @@ -414,6 +425,17 @@ curl -X GET http://coder-server:8080/api/v2/config/deployment \ "value": "string" } }, + "max_session_expiry": { + "default": 0, + "enterprise": true, + "flag": "string", + "hidden": true, + "name": "string", + "secret": true, + "shorthand": "string", + "usage": "string", + "value": 0 + }, "max_token_lifetime": { "default": 0, "enterprise": true, diff --git a/docs/api/schemas.md b/docs/api/schemas.md index b40144925b84f..f6bb00f3651cc 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1658,6 +1658,17 @@ CreateParameterRequest is a structure used to create a new parameter value for a "usage": "string", "value": true }, + "disable_session_expiry_refresh": { + "default": true, + "enterprise": true, + "flag": "string", + "hidden": true, + "name": "string", + "secret": true, + "shorthand": "string", + "usage": "string", + "value": true + }, "experimental": { "default": true, "enterprise": true, @@ -1772,6 +1783,17 @@ CreateParameterRequest is a structure used to create a new parameter value for a "value": "string" } }, + "max_session_expiry": { + "default": 0, + "enterprise": true, + "flag": "string", + "hidden": true, + "name": "string", + "secret": true, + "shorthand": "string", + "usage": "string", + "value": 0 + }, "max_token_lifetime": { "default": 0, "enterprise": true, @@ -2406,12 +2428,14 @@ CreateParameterRequest is a structure used to create a new parameter value for a | `dangerous` | [codersdk.DangerousConfig](#codersdkdangerousconfig) | false | | | | `derp` | [codersdk.DERP](#codersdkderp) | false | | | | `disable_path_apps` | [codersdk.DeploymentConfigField-bool](#codersdkdeploymentconfigfield-bool) | false | | | +| `disable_session_expiry_refresh` | [codersdk.DeploymentConfigField-bool](#codersdkdeploymentconfigfield-bool) | false | | | | `experimental` | [codersdk.DeploymentConfigField-bool](#codersdkdeploymentconfigfield-bool) | false | | Experimental Use Experiments instead. | | `experiments` | [codersdk.DeploymentConfigField-array_string](#codersdkdeploymentconfigfield-array_string) | false | | | | `gitauth` | [codersdk.DeploymentConfigField-array_codersdk_GitAuthConfig](#codersdkdeploymentconfigfield-array_codersdk_gitauthconfig) | false | | | | `http_address` | [codersdk.DeploymentConfigField-string](#codersdkdeploymentconfigfield-string) | false | | | | `in_memory_database` | [codersdk.DeploymentConfigField-bool](#codersdkdeploymentconfigfield-bool) | false | | | | `logging` | [codersdk.LoggingConfig](#codersdkloggingconfig) | false | | | +| `max_session_expiry` | [codersdk.DeploymentConfigField-time_Duration](#codersdkdeploymentconfigfield-time_duration) | false | | | | `max_token_lifetime` | [codersdk.DeploymentConfigField-time_Duration](#codersdkdeploymentconfigfield-time_duration) | false | | | | `metrics_cache_refresh_interval` | [codersdk.DeploymentConfigField-time_Duration](#codersdkdeploymentconfigfield-time_duration) | false | | | | `oauth2` | [codersdk.OAuth2Config](#codersdkoauth2config) | false | | | diff --git a/docs/cli/coder_server.md b/docs/cli/coder_server.md index d25a72df239a2..eaf94328f8630 100644 --- a/docs/cli/coder_server.md +++ b/docs/cli/coder_server.md @@ -37,6 +37,8 @@ coder server [flags] Consumes $CODER_DERP_SERVER_STUN_ADDRESSES (default [stun.l.google.com:19302]) --disable-path-apps Disable workspace apps that are not served from subdomains. Path-based apps can make requests to the Coder API and pose a security risk when the workspace serves malicious JavaScript. This is recommended for security purposes if a --wildcard-access-url is configured. Consumes $CODER_DISABLE_PATH_APPS + --disable-session-expiry-refresh Disable automatic session expiry bumping due to activity. This forces all sessions to become invalid after the session expiry duration has been reached. + Consumes $CODER_DISABLE_SESSION_EXPIRY_REFRESH --experiments strings Enable one or more experiments. These are not ready for production. Separate multiple experiments with commas, or enter '*' to opt-in to all available experiments. Consumes $CODER_EXPERIMENTS -h, --help help for server @@ -48,7 +50,7 @@ coder server [flags] Consumes $CODER_LOGGING_JSON --log-stackdriver string Output Stackdriver compatible logs to a given file. Consumes $CODER_LOGGING_STACKDRIVER - --max-token-lifetime duration The maximum lifetime duration for any user creating a token. + --max-token-lifetime duration The maximum lifetime duration users can specify when creating an API token. Consumes $CODER_MAX_TOKEN_LIFETIME (default 720h0m0s) --oauth2-github-allow-everyone Allow all logins, setting this option means allowed orgs and teams must be empty. Consumes $CODER_OAUTH2_GITHUB_ALLOW_EVERYONE @@ -108,6 +110,8 @@ coder server [flags] Consumes $CODER_PROXY_TRUSTED_ORIGINS --secure-auth-cookie Controls if the 'Secure' property is set on browser session cookies. Consumes $CODER_SECURE_AUTH_COOKIE + --session-duration duration The token expiry duration for browser sessions. Sessions may last longer if they are actively making requests, but this functionality can be disabled via --disable-session-expiry-refresh. + Consumes $CODER_MAX_SESSION_EXPIRY (default 24h0m0s) --ssh-keygen-algorithm string The algorithm to use for generating ssh keys. Accepted values are "ed25519", "ecdsa", or "rsa4096". Consumes $CODER_SSH_KEYGEN_ALGORITHM (default "ed25519") --swagger-enable Expose the swagger endpoint via /swagger. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 048c269c0f5b5..2936a41d2650b 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -324,6 +324,8 @@ export interface DeploymentConfig { readonly logging: LoggingConfig readonly dangerous: DangerousConfig readonly disable_path_apps: DeploymentConfigField + readonly max_session_expiry: DeploymentConfigField + readonly disable_session_expiry_refresh: DeploymentConfigField readonly address: DeploymentConfigField readonly experimental: DeploymentConfigField } From adeb9b683ff4e9886c747b4f46d101bfd62fe742 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Fri, 3 Feb 2023 15:59:54 +0000 Subject: [PATCH 3/6] chore: remove sleep from session expiry test --- coderd/apikey_test.go | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/coderd/apikey_test.go b/coderd/apikey_test.go index bbefd31ec7785..3de6d5a893519 100644 --- a/coderd/apikey_test.go +++ b/coderd/apikey_test.go @@ -3,6 +3,7 @@ package coderd_test import ( "context" "net/http" + "strings" "testing" "time" @@ -10,6 +11,8 @@ import ( "github.com/stretchr/testify/require" "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/coderd/database" + "github.com/coder/coder/coderd/database/dbtestutil" "github.com/coder/coder/codersdk" "github.com/coder/coder/testutil" ) @@ -117,8 +120,12 @@ func TestSessionExpiry(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() dc := coderdtest.DeploymentConfig(t) + + db, pubsub := dbtestutil.NewDB(t) adminClient := coderdtest.New(t, &coderdtest.Options{ DeploymentConfig: dc, + Database: db, + Pubsub: pubsub, }) adminUser := coderdtest.CreateFirstUser(t, adminClient) @@ -131,9 +138,24 @@ func TestSessionExpiry(t *testing.T) { dc.SessionDuration.Value = time.Second userClient := coderdtest.CreateAnotherUser(t, adminClient, adminUser.OrganizationID) - time.Sleep(dc.SessionDuration.Value + time.Second) - _, err := userClient.User(ctx, codersdk.Me) + // Find the session cookie, and ensure it has the correct expiry. + token := userClient.SessionToken() + apiKey, err := db.GetAPIKeyByID(ctx, strings.Split(token, "-")[0]) + require.NoError(t, err) + + require.EqualValues(t, dc.SessionDuration.Value.Seconds(), apiKey.LifetimeSeconds) + require.WithinDuration(t, apiKey.CreatedAt.Add(dc.SessionDuration.Value), apiKey.ExpiresAt, 2*time.Second) + + // Update the session token to be expired so we can test that it is + // rejected for extra points. + err = db.UpdateAPIKeyByID(ctx, database.UpdateAPIKeyByIDParams{ + ID: apiKey.ID, + ExpiresAt: database.Now().Add(-time.Hour), + }) + require.NoError(t, err) + + _, err = userClient.User(ctx, codersdk.Me) require.Error(t, err) var sdkErr *codersdk.Error if assert.ErrorAs(t, err, &sdkErr) { From 7a698c9f80526228503beab5938fe176919212dd Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Fri, 3 Feb 2023 16:02:07 +0000 Subject: [PATCH 4/6] fixup! chore: remove sleep from session expiry test --- coderd/workspaceapps_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/coderd/workspaceapps_test.go b/coderd/workspaceapps_test.go index ad47c19231766..cd254748ea5b2 100644 --- a/coderd/workspaceapps_test.go +++ b/coderd/workspaceapps_test.go @@ -505,6 +505,7 @@ func TestWorkspaceApplicationAuth(t *testing.T) { require.Equal(t, user.ID, apiKeyInfo.UserID) require.Equal(t, codersdk.LoginTypePassword, apiKeyInfo.LoginType) require.WithinDuration(t, currentAPIKey.ExpiresAt, apiKeyInfo.ExpiresAt, 5*time.Second) + require.EqualValues(t, currentAPIKey.LifetimeSeconds, apiKeyInfo.LifetimeSeconds) // Verify the API key permissions appClient := codersdk.New(client.URL) From 4f043f3f52a0144f7d0249d0185fe9f637598409 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Fri, 3 Feb 2023 16:46:31 +0000 Subject: [PATCH 5/6] fixup! chore: remove sleep from session expiry test --- coderd/httpmw/apikey_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/httpmw/apikey_test.go b/coderd/httpmw/apikey_test.go index f8b76f999e40e..f3573ab3486d2 100644 --- a/coderd/httpmw/apikey_test.go +++ b/coderd/httpmw/apikey_test.go @@ -366,7 +366,7 @@ func TestAPIKey(t *testing.T) { t.Run("NoRefresh", func(t *testing.T) { t.Parallel() var ( - db = databasefake.New() + db = dbfake.New() user = dbgen.User(t, db, database.User{}) sentAPIKey, token = dbgen.APIKey(t, db, database.APIKey{ UserID: user.ID, From 4239c7da4ae1525e5b91c2642fd50ec59f6a86af Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Fri, 3 Feb 2023 17:22:58 +0000 Subject: [PATCH 6/6] fixup! chore: remove sleep from session expiry test --- coderd/apikey_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/coderd/apikey_test.go b/coderd/apikey_test.go index 3de6d5a893519..9361ced76e7bf 100644 --- a/coderd/apikey_test.go +++ b/coderd/apikey_test.go @@ -151,7 +151,9 @@ func TestSessionExpiry(t *testing.T) { // rejected for extra points. err = db.UpdateAPIKeyByID(ctx, database.UpdateAPIKeyByIDParams{ ID: apiKey.ID, + LastUsed: apiKey.LastUsed, ExpiresAt: database.Now().Add(-time.Hour), + IPAddress: apiKey.IPAddress, }) require.NoError(t, err)