From aefad76342d6be97f33140552f324fc3256e40fa Mon Sep 17 00:00:00 2001 From: Gabriel Hernandez Date: Tue, 10 Mar 2026 17:15:09 +0000 Subject: [PATCH] extend the expiration date for the auth token cookie (#41261) **Related issue:** Resolves #41262 This extends the expiration date for the host auth token cookie. # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. - [x] QA'd all new/changed functionality manually --- changes/issue-41262-update-authtoken-expiry | 1 + frontend/pages/LoginPage/LoginPage.tsx | 7 ++-- frontend/pages/LoginPage/LoginPreviewPage.tsx | 14 +++++--- frontend/pages/MfaPage/MfaPage.tsx | 7 ++-- frontend/services/entities/sessions.ts | 1 + frontend/utilities/auth_token/index.ts | 17 ++++++--- server/fleet/service.go | 2 ++ server/mock/service/service_mock.go | 12 +++++++ server/service/metrics_sessions.go | 4 +++ server/service/sessions.go | 35 +++++++++++++++++-- 10 files changed, 86 insertions(+), 14 deletions(-) create mode 100644 changes/issue-41262-update-authtoken-expiry diff --git a/changes/issue-41262-update-authtoken-expiry b/changes/issue-41262-update-authtoken-expiry new file mode 100644 index 0000000000..aa919c4a8e --- /dev/null +++ b/changes/issue-41262-update-authtoken-expiry @@ -0,0 +1 @@ +- Updated expiration date of the auth token cookie to match the fleet session duration diff --git a/frontend/pages/LoginPage/LoginPage.tsx b/frontend/pages/LoginPage/LoginPage.tsx index 581d02c1a5..6c2e23e67b 100644 --- a/frontend/pages/LoginPage/LoginPage.tsx +++ b/frontend/pages/LoginPage/LoginPage.tsx @@ -120,9 +120,12 @@ const LoginPage = ({ router, location }: ILoginPageProps) => { try { const response = await sessionsAPI.login(formData); - const { user, available_teams, token } = response; + const { user, available_teams, token, token_expires_at } = response; - authToken.save(token); + const expiresAt = token_expires_at + ? new Date(token_expires_at) + : undefined; + authToken.save(token, expiresAt); setCurrentUser(user); setAvailableTeams(user, available_teams); diff --git a/frontend/pages/LoginPage/LoginPreviewPage.tsx b/frontend/pages/LoginPage/LoginPreviewPage.tsx index dae263f201..cd1d529335 100644 --- a/frontend/pages/LoginPage/LoginPreviewPage.tsx +++ b/frontend/pages/LoginPage/LoginPreviewPage.tsx @@ -33,10 +33,16 @@ const LoginPreviewPage = ({ router }: ILoginPreviewPageProps): JSX.Element => { const { DASHBOARD } = paths; try { - const { user, available_teams, token } = await sessionsAPI.login( - formData - ); - authToken.save(token); + const { + user, + available_teams, + token, + token_expires_at, + } = await sessionsAPI.login(formData); + const expiresAt = token_expires_at + ? new Date(token_expires_at) + : undefined; + authToken.save(token, expiresAt); setCurrentUser(user); setAvailableTeams(user, available_teams); diff --git a/frontend/pages/MfaPage/MfaPage.tsx b/frontend/pages/MfaPage/MfaPage.tsx index 5c7b00a0c6..bd8c76b3f1 100644 --- a/frontend/pages/MfaPage/MfaPage.tsx +++ b/frontend/pages/MfaPage/MfaPage.tsx @@ -43,9 +43,12 @@ const MfaPage = ({ router, params }: IMfaPage) => { try { const response = await sessionsAPI.finishMFA({ token: mfaToken }); - const { user, available_teams, token } = response; + const { user, available_teams, token, token_expires_at } = response; - authToken.save(token); + const expiresAt = token_expires_at + ? new Date(token_expires_at) + : undefined; + authToken.save(token, expiresAt); setCurrentUser(user); setAvailableTeams(user, available_teams); diff --git a/frontend/services/entities/sessions.ts b/frontend/services/entities/sessions.ts index bc1787d8e5..db5796db86 100644 --- a/frontend/services/entities/sessions.ts +++ b/frontend/services/entities/sessions.ts @@ -23,6 +23,7 @@ export interface ILoginResponse { available_teams: ITeamSummary[]; available_fleets: ITeamSummary[]; token: string; + token_expires_at?: string; } export default { diff --git a/frontend/utilities/auth_token/index.ts b/frontend/utilities/auth_token/index.ts index 7158fb07d9..f01848a82a 100644 --- a/frontend/utilities/auth_token/index.ts +++ b/frontend/utilities/auth_token/index.ts @@ -4,8 +4,14 @@ */ import Cookie from "js-cookie"; -const save = (token: string): void => { - Cookie.set("__Host-token", token, { secure: true, sameSite: "lax" }); +const DEFAULT_EXPIRATION_DAYS = 5; + +const save = (token: string, expiresAt?: Date): void => { + Cookie.set("__Host-token", token, { + secure: true, + sameSite: "lax", + expires: expiresAt ?? DEFAULT_EXPIRATION_DAYS, + }); }; const get = (): string | null => { @@ -13,9 +19,12 @@ const get = (): string | null => { }; const remove = (): void => { - // NOTE: the entire cookie including the name and values must be provided + // NOTE: the secure and sameSite from the cookie must be provided // to correctly remove. That is why we include the options here as well. - Cookie.remove("__Host-token", { secure: true, sameSite: "lax" }); + Cookie.remove("__Host-token", { + secure: true, + sameSite: "lax", + }); }; export default { diff --git a/server/fleet/service.go b/server/fleet/service.go index eb1d5a3814..f84ad97f25 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -240,6 +240,8 @@ type Service interface { // SSOSettings returns non-sensitive single sign on information used before authentication SSOSettings(ctx context.Context) (*SessionSSOSettings, error) Login(ctx context.Context, email, password string, supportsEmailVerification bool) (user *User, session *Session, err error) + // GetSessionDuration returns the configured session duration + GetSessionDuration(ctx context.Context) time.Duration Logout(ctx context.Context) (err error) CompleteMFA(ctx context.Context, token string) (*Session, *User, error) DestroySession(ctx context.Context) (err error) diff --git a/server/mock/service/service_mock.go b/server/mock/service/service_mock.go index 7c64c34b72..b1a6ba7d2a 100644 --- a/server/mock/service/service_mock.go +++ b/server/mock/service/service_mock.go @@ -111,6 +111,8 @@ type SSOSettingsFunc func(ctx context.Context) (*fleet.SessionSSOSettings, error type LoginFunc func(ctx context.Context, email string, password string, supportsEmailVerification bool) (user *fleet.User, session *fleet.Session, err error) +type GetSessionDurationFunc func(ctx context.Context) time.Duration + type LogoutFunc func(ctx context.Context) (err error) type CompleteMFAFunc func(ctx context.Context, token string) (*fleet.Session, *fleet.User, error) @@ -1026,6 +1028,9 @@ type Service struct { LoginFunc LoginFunc LoginFuncInvoked bool + GetSessionDurationFunc GetSessionDurationFunc + GetSessionDurationFuncInvoked bool + LogoutFunc LogoutFunc LogoutFuncInvoked bool @@ -2515,6 +2520,13 @@ func (s *Service) Login(ctx context.Context, email string, password string, supp return s.LoginFunc(ctx, email, password, supportsEmailVerification) } +func (s *Service) GetSessionDuration(ctx context.Context) time.Duration { + s.mu.Lock() + s.GetSessionDurationFuncInvoked = true + s.mu.Unlock() + return s.GetSessionDurationFunc(ctx) +} + func (s *Service) Logout(ctx context.Context) (err error) { s.mu.Lock() s.LogoutFuncInvoked = true diff --git a/server/service/metrics_sessions.go b/server/service/metrics_sessions.go index 3e57052714..eadf523fb6 100644 --- a/server/service/metrics_sessions.go +++ b/server/service/metrics_sessions.go @@ -23,6 +23,10 @@ func (mw metricsMiddleware) Login(ctx context.Context, email string, password st return user, session, err } +func (mw metricsMiddleware) GetSessionDuration(ctx context.Context) time.Duration { + return mw.Service.GetSessionDuration(ctx) +} + func (mw metricsMiddleware) Logout(ctx context.Context) error { var err error defer func(begin time.Time) { diff --git a/server/service/sessions.go b/server/service/sessions.go index 0c02ec5031..29ed7ec940 100644 --- a/server/service/sessions.go +++ b/server/service/sessions.go @@ -120,6 +120,7 @@ type loginResponse struct { User *fleet.User `json:"user,omitempty"` AvailableTeams []*fleet.TeamSummary `json:"available_teams" renameto:"available_fleets"` Token string `json:"token,omitempty"` + TokenExpiresAt *time.Time `json:"token_expires_at,omitempty"` Err error `json:"error,omitempty"` } @@ -159,7 +160,20 @@ func loginEndpoint(ctx context.Context, request interface{}, svc fleet.Service) return loginResponse{Err: err}, nil } } - return loginResponse{user, availableTeams, session.Key, nil}, nil + + // Calculate token expiration time if session duration is configured + var tokenExpiresAt *time.Time + if sessionDuration := svc.GetSessionDuration(ctx); sessionDuration > 0 { + expiresAt := time.Now().Add(sessionDuration).UTC() + tokenExpiresAt = &expiresAt + } + + return loginResponse{ + User: user, + AvailableTeams: availableTeams, + Token: session.Key, + TokenExpiresAt: tokenExpiresAt, + }, nil } var ( @@ -254,6 +268,10 @@ func (svc *Service) makeSession(ctx context.Context, userID uint) (*fleet.Sessio return svc.ds.NewSession(ctx, userID, svc.config.Session.KeySize) } +func (svc *Service) GetSessionDuration(ctx context.Context) time.Duration { + return svc.config.Session.Duration +} + //////////////////////////////////////////////////////////////////////////////// // Session create (second step of MFA) //////////////////////////////////////////////////////////////////////////////// @@ -281,7 +299,20 @@ func sessionCreateEndpoint(ctx context.Context, request interface{}, svc fleet.S return loginResponse{Err: err}, nil } } - return loginResponse{user, availableTeams, session.Key, nil}, nil + + // Calculate token expiration time if session duration is configured + var tokenExpiresAt *time.Time + if sessionDuration := svc.GetSessionDuration(ctx); sessionDuration > 0 { + expiresAt := time.Now().Add(sessionDuration).UTC() + tokenExpiresAt = &expiresAt + } + + return loginResponse{ + User: user, + AvailableTeams: availableTeams, + Token: session.Key, + TokenExpiresAt: tokenExpiresAt, + }, nil } func (svc *Service) CompleteMFA(ctx context.Context, token string) (*fleet.Session, *fleet.User, error) {