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
13 changes: 11 additions & 2 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,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)
Expand All @@ -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
}
Comment on lines +106 to +110
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validation might fail due to just expiration or context failure, so using internal error here may not make sense. Instead, consider a) validating the token before writing out the refreshed cookie, b) if validating the token fails, just delete the cookie, then continue down the middleware chain (instead of blocking)

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)
})
Expand Down
Loading