diff --git a/admin/auth_token.go b/admin/auth_token.go index e702eb09c5ce..3d89081524e4 100644 --- a/admin/auth_token.go +++ b/admin/auth_token.go @@ -226,6 +226,26 @@ func (s *Service) IssueMagicAuthToken(ctx context.Context, opts *IssueMagicAuthT return &magicAuthToken{model: dat, token: tkn}, nil } +// ExtendBrowserSessionAuthToken extends a Rill web browser session token when its +// remaining lifetime is at or below refreshThreshold. +func (s *Service) ExtendBrowserSessionAuthToken(ctx context.Context, authTok AuthToken, fullTTL, refreshThreshold time.Duration) error { + uat, ok := authTok.TokenModel().(*database.UserAuthToken) + if !ok { + return nil + } + if uat.AuthClientID == nil || *uat.AuthClientID != database.AuthClientIDRillWeb { + return nil + } + if uat.RepresentingUserID != nil || uat.Refresh { + return nil + } + if uat.ExpiresOn != nil && time.Until(*uat.ExpiresOn) > refreshThreshold { + return nil + } + newExpiresOn := time.Now().Add(fullTTL) + return s.DB.UpdateUserAuthTokenExpiresOn(ctx, uat.ID, newExpiresOn) +} + // RevokeAuthToken removes an auth token from persistent storage. func (s *Service) RevokeAuthToken(ctx context.Context, token string) error { parsed, err := authtoken.FromString(token) diff --git a/admin/database/database.go b/admin/database/database.go index 045adf24425c..da1bfaab0d0e 100644 --- a/admin/database/database.go +++ b/admin/database/database.go @@ -164,6 +164,7 @@ type DB interface { FindUserAuthToken(ctx context.Context, id string) (*UserAuthToken, error) InsertUserAuthToken(ctx context.Context, opts *InsertUserAuthTokenOptions) (*UserAuthToken, error) UpdateUserAuthTokenUsedOn(ctx context.Context, ids []string) error + UpdateUserAuthTokenExpiresOn(ctx context.Context, id string, expiresOn time.Time) error DeleteUserAuthToken(ctx context.Context, id string) error DeleteAllUserAuthTokens(ctx context.Context, userID string) (int, error) DeleteUserAuthTokensByUserAndRepresentingUser(ctx context.Context, userID, representingUserID string) error diff --git a/admin/database/postgres/postgres.go b/admin/database/postgres/postgres.go index 8b07fa038cfb..134adf297e01 100644 --- a/admin/database/postgres/postgres.go +++ b/admin/database/postgres/postgres.go @@ -1232,6 +1232,14 @@ func (c *connection) UpdateUserAuthTokenUsedOn(ctx context.Context, ids []string return nil } +func (c *connection) UpdateUserAuthTokenExpiresOn(ctx context.Context, id string, expiresOn time.Time) error { + _, err := c.getDB(ctx).ExecContext(ctx, "UPDATE user_auth_tokens SET expires_on=$2 WHERE id=$1", id, expiresOn) + if err != nil { + return parseErr("auth token", err) + } + return nil +} + func (c *connection) DeleteUserAuthToken(ctx context.Context, id string) error { res, err := c.getDB(ctx).ExecContext(ctx, "DELETE FROM user_auth_tokens WHERE id=$1", id) return checkDeleteRow("auth token", res, err) diff --git a/admin/server/auth/handlers.go b/admin/server/auth/handlers.go index 33d986031dc6..7a30a83ab4a0 100644 --- a/admin/server/auth/handlers.go +++ b/admin/server/auth/handlers.go @@ -31,6 +31,11 @@ const ( cookieFieldAccessToken = "access_token" ) +var ( + browserSessionTTL = 14 * 24 * time.Hour + browserSessionTTLRefreshThreshold = browserSessionTTL - 24*time.Hour +) + // RegisterEndpoints adds HTTP endpoints for auth. // The mux must be served on the ExternalURL of the Authenticator since the logic in these handlers relies on knowing the full external URIs. // Note that these are not gRPC handlers, just regular HTTP endpoints that we mount on the gRPC-gateway mux. @@ -342,7 +347,7 @@ func (a *Authenticator) authLoginCallback(w http.ResponseWriter, r *http.Request } // Issue a new persistent auth token - authToken, err := a.admin.IssueUserAuthToken(r.Context(), user.ID, database.AuthClientIDRillWeb, "Browser session", nil, nil, false) + authToken, err := a.admin.IssueUserAuthToken(r.Context(), user.ID, database.AuthClientIDRillWeb, "Browser session", nil, &browserSessionTTL, false) if err != nil { http.Error(w, fmt.Sprintf("failed to issue API token: %s", err), http.StatusInternalServerError) return @@ -405,7 +410,7 @@ func (a *Authenticator) authLoginCustomDomainCallback(w http.ResponseWriter, r * http.Error(w, err.Error(), http.StatusUnauthorized) return } - newAuthToken, err := a.admin.IssueUserAuthToken(r.Context(), validated.OwnerID(), database.AuthClientIDRillWeb, "Browser session", nil, nil, false) + newAuthToken, err := a.admin.IssueUserAuthToken(r.Context(), validated.OwnerID(), database.AuthClientIDRillWeb, "Browser session", nil, &browserSessionTTL, false) if err != nil { http.Error(w, fmt.Sprintf("failed to issue API token: %s", err), http.StatusInternalServerError) return diff --git a/admin/server/auth/middleware.go b/admin/server/auth/middleware.go index 92ca6296d3a4..3ebf12be6800 100644 --- a/admin/server/auth/middleware.go +++ b/admin/server/auth/middleware.go @@ -11,6 +11,7 @@ import ( "github.com/grpc-ecosystem/go-grpc-middleware/util/metautils" "github.com/rilldata/rill/runtime/pkg/observability" semconv "go.opentelemetry.io/otel/semconv/v1.21.0" + "go.uber.org/zap" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" @@ -91,8 +92,8 @@ func (a *Authenticator) HTTPMiddlewareLenient(next http.Handler) http.Handler { } // CookieRefreshMiddleware is a middleware that refreshes the auth cookie. -// This enables us to do rolling cookie refreshes so we can have a relatively short cookie max age. -// Note that it does not update the auth token encrypted inside the cookie. +// This enables us to do rolling cookie refreshes so we can have a relatively short cookie max age (browserSessionTTL). +// Once per day we refresh the auth token TTL if it's still valid. func (a *Authenticator) CookieRefreshMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { sess := a.cookies.Get(r, cookieName) @@ -102,6 +103,14 @@ func (a *Authenticator) CookieRefreshMiddleware(next http.Handler) http.Handler http.Error(w, err.Error(), http.StatusInternalServerError) return } + validatedToken, err := a.admin.ValidateAuthToken(r.Context(), authToken) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if err := a.admin.ExtendBrowserSessionAuthToken(r.Context(), validatedToken, browserSessionTTL, browserSessionTTLRefreshThreshold); err != nil { + a.logger.Info("failed to extend browser session auth token TTL", zap.Error(err), observability.ZapCtx(r.Context())) + } } next.ServeHTTP(w, r) })