detached 24ccd67

refactor(app): share cookieLifetime between link and passkey sign-in

commit
24ccd67f678c2c19401172da1ea5e6b308de427e
author
brice · · 2026-10-12 15:46 UTC
parent
7233711
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +18 -16
M internal/app/web.go +18 -16
@@ -132,20 +132,27 @@ func (s webSessions) Login(ctx context.Context, nonce string) (string, time.Dura
132132 if err != nil {
133133 return "", 0, err
134134 }
135- // The cookie lasts as long as the session, which the expiry of the token
136- // that made the link may cut short. The session token carries its own
137- // expiry, so a cookie that outlives it by under a second is harmless.
138- // The expiry is truncated to the second, hence the slack on the full TTL.
139- lifetime := usecase.SessionTTL
140- if d := time.Until(expiresAt); d < usecase.SessionTTL-time.Second {
141- lifetime = d.Round(time.Second)
142- }
143- if lifetime < time.Second {
135+ lifetime, ok := cookieLifetime(expiresAt)
136+ if !ok {
144137 return "", 0, ports.ErrLinkRejected
145138 }
146139 return token, lifetime, nil
147140 }
148141
142+// cookieLifetime is how long the session cookie lasts for a session that
143+// expires at expiresAt: as long as the session, which the expiry of the token
144+// that made a login link may cut short. The session token carries its own
145+// expiry, so a cookie that outlives it by under a second is harmless. The
146+// expiry is truncated to the second, hence the slack on the full TTL. ok is
147+// false when under a second is left: there is no session to give.
148+func cookieLifetime(expiresAt time.Time) (lifetime time.Duration, ok bool) {
149+ lifetime = usecase.SessionTTL
150+ if d := time.Until(expiresAt); d < usecase.SessionTTL-time.Second {
151+ lifetime = d.Round(time.Second)
152+ }
153+ return lifetime, lifetime >= time.Second
154+}
155+
149156 func (s webSessions) Verify(ctx context.Context, token string) (domain.Principal, bool, error) {
150157 p, err := s.Sessions.Verify(ctx, token)
151158 if errors.Is(err, domain.ErrUnauthenticated) {
@@ -173,13 +180,8 @@ func (p webPasskeys) FinishLogin(ctx context.Context, ceremony string, response
173180 if err != nil {
174181 return "", 0, err
175182 }
176- // Same lifetime as webSessions.Login: the session token carries its own
177- // expiry, the cookie lasts as long.
178- lifetime := usecase.SessionTTL
179- if d := time.Until(expiresAt); d < usecase.SessionTTL-time.Second {
180- lifetime = d.Round(time.Second)
181- }
182- if lifetime < time.Second {
183+ lifetime, ok := cookieLifetime(expiresAt)
184+ if !ok {
183185 return "", 0, ports.ErrLinkRejected
184186 }
185187 return token, lifetime, nil
@@ -132,20 +132,27 @@ func (s webSessions) Login(ctx context.Context, nonce string) (string, time.Dura
132 if err != nil {132 if err != nil {
133 return "", 0, err133 return "", 0, err
134 }134 }
135- // The cookie lasts as long as the session, which the expiry of the token135+ lifetime, ok := cookieLifetime(expiresAt)
136- // that made the link may cut short. The session token carries its own136+ if !ok {
137- // expiry, so a cookie that outlives it by under a second is harmless.
138- // The expiry is truncated to the second, hence the slack on the full TTL.
139- lifetime := usecase.SessionTTL
140- if d := time.Until(expiresAt); d < usecase.SessionTTL-time.Second {
141- lifetime = d.Round(time.Second)
142- }
143- if lifetime < time.Second {
144 return "", 0, ports.ErrLinkRejected137 return "", 0, ports.ErrLinkRejected
145 }138 }
146 return token, lifetime, nil139 return token, lifetime, nil
147 }140 }
148 141
142+// cookieLifetime is how long the session cookie lasts for a session that
143+// expires at expiresAt: as long as the session, which the expiry of the token
144+// that made a login link may cut short. The session token carries its own
145+// expiry, so a cookie that outlives it by under a second is harmless. The
146+// expiry is truncated to the second, hence the slack on the full TTL. ok is
147+// false when under a second is left: there is no session to give.
148+func cookieLifetime(expiresAt time.Time) (lifetime time.Duration, ok bool) {
149+ lifetime = usecase.SessionTTL
150+ if d := time.Until(expiresAt); d < usecase.SessionTTL-time.Second {
151+ lifetime = d.Round(time.Second)
152+ }
153+ return lifetime, lifetime >= time.Second
154+}
155+
149 func (s webSessions) Verify(ctx context.Context, token string) (domain.Principal, bool, error) {156 func (s webSessions) Verify(ctx context.Context, token string) (domain.Principal, bool, error) {
150 p, err := s.Sessions.Verify(ctx, token)157 p, err := s.Sessions.Verify(ctx, token)
151 if errors.Is(err, domain.ErrUnauthenticated) {158 if errors.Is(err, domain.ErrUnauthenticated) {
@@ -173,13 +180,8 @@ func (p webPasskeys) FinishLogin(ctx context.Context, ceremony string, response
173 if err != nil {180 if err != nil {
174 return "", 0, err181 return "", 0, err
175 }182 }
176- // Same lifetime as webSessions.Login: the session token carries its own183+ lifetime, ok := cookieLifetime(expiresAt)
177- // expiry, the cookie lasts as long.184+ if !ok {
178- lifetime := usecase.SessionTTL
179- if d := time.Until(expiresAt); d < usecase.SessionTTL-time.Second {
180- lifetime = d.Round(time.Second)
181- }
182- if lifetime < time.Second {
183 return "", 0, ports.ErrLinkRejected185 return "", 0, ports.ErrLinkRejected
184 }186 }
185 return token, lifetime, nil187 return token, lifetime, nil

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 45

↑↓ moveenter openesc close