refactor(app): share cookieLifetime between link and passkey sign-in
- commit
- 24ccd67f678c2c19401172da1ea5e6b308de427e
- author
- brice · · 2026-10-12 15:46 UTC
- parent
- 7233711
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 | |||
| 132 | 132 | if err != nil { | |
| 133 | 133 | return "", 0, err | |
| 134 | 134 | } | |
| 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 { | |
| 144 | 137 | return "", 0, ports.ErrLinkRejected | |
| 145 | 138 | } | |
| 146 | 139 | 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 | 156 | func (s webSessions) Verify(ctx context.Context, token string) (domain.Principal, bool, error) { | |
| 150 | 157 | p, err := s.Sessions.Verify(ctx, token) | |
| 151 | 158 | if errors.Is(err, domain.ErrUnauthenticated) { | |
| @@ -173,13 +180,8 @@ func (p webPasskeys) FinishLogin(ctx context.Context, ceremony string, response | |||
| 173 | 180 | if err != nil { | |
| 174 | 181 | return "", 0, err | |
| 175 | 182 | } | |
| 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 { | |
| 183 | 185 | return "", 0, ports.ErrLinkRejected | |
| 184 | 186 | } | |
| 185 | 187 | 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, err | 133 | return "", 0, err | ||
| 134 | } | 134 | } | ||
| 135 | - | // The cookie lasts as long as the session, which the expiry of the token | 135 | + | lifetime, ok := cookieLifetime(expiresAt) |
| 136 | - | // that made the link may cut short. The session token carries its own | 136 | + | 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.ErrLinkRejected | 137 | return "", 0, ports.ErrLinkRejected | ||
| 145 | } | 138 | } | ||
| 146 | return token, lifetime, nil | 139 | 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, err | 181 | return "", 0, err | ||
| 175 | } | 182 | } | ||
| 176 | - | // Same lifetime as webSessions.Login: the session token carries its own | 183 | + | 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.ErrLinkRejected | 185 | return "", 0, ports.ErrLinkRejected | ||
| 184 | } | 186 | } | ||
| 185 | return token, lifetime, nil | 187 | return token, lifetime, nil | ||