Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions admin/auth_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions admin/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions admin/database/postgres/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 7 additions & 2 deletions admin/server/auth/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
19 changes: 16 additions & 3 deletions admin/server/auth/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -91,13 +92,25 @@ 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)
if authToken, ok := sess.Values[cookieFieldAccessToken].(string); ok && authToken != "" {
// Re-save the cookie to refresh its expiration
validatedToken, err := a.admin.ValidateAuthToken(r.Context(), authToken)
if err != nil {
// Token validation failures can be due to expiry or transient context issues.
// Clear the cookie and continue down the chain rather than failing the request.
delete(sess.Values, cookieFieldAccessToken)
a.logger.Warn("failed to validate auth token", zap.Error(err), observability.ZapCtx(r.Context()))
} else {
if err := a.admin.ExtendBrowserSessionAuthToken(r.Context(), validatedToken, browserSessionTTL, browserSessionTTLRefreshThreshold); err != nil {
a.logger.Warn("failed to extend browser session auth token TTL", zap.Error(err), observability.ZapCtx(r.Context()))
}
}

// Re-save the cookie to refresh its expiration after successful validation.
if err := sess.Save(r, w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
Expand Down
Loading