detached b90df44

fix(auth): use UTC timestamps for token generation and validation

Ensures consistent timezone handling across servers by using
time.Now().UTC() instead of time.Now() in all token-related code.
commit
b90df448cca1765d30aaefbaa1cb0e3bc76b973e
author
bnema · · 2026-02-03 19:47 UTC
parent
43db1bb
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +8 -8
M internal/adapters/in/http/auth/handler.go +3 -3
@@ -147,7 +147,7 @@ func (h *Handler) handlePassword(w http.ResponseWriter, r *http.Request) {
147147 response := PasswordResponse{
148148 Token: token,
149149 ExpiresIn: int(expiry.Seconds()),
150- IssuedAt: time.Now().Format(time.RFC3339),
150+ IssuedAt: time.Now().UTC().Format(time.RFC3339),
151151 }
152152
153153 w.Header().Set("Content-Type", "application/json")
@@ -244,7 +244,7 @@ func (h *Handler) handleToken(w http.ResponseWriter, r *http.Request) {
244244 response := dto.TokenResponse{
245245 Token: accessToken,
246246 ExpiresIn: 300, // 5 minutes in seconds
247- IssuedAt: time.Now().Format(time.RFC3339),
247+ IssuedAt: time.Now().UTC().Format(time.RFC3339),
248248 }
249249
250250 w.Header().Set("Content-Type", "application/json")
@@ -266,7 +266,7 @@ func (h *Handler) sendAnonymousToken(w http.ResponseWriter, log zerowrap.Logger)
266266 response := dto.TokenResponse{
267267 Token: "", // Empty token indicates limited access
268268 ExpiresIn: 60, // 1 minute
269- IssuedAt: time.Now().Format(time.RFC3339),
269+ IssuedAt: time.Now().UTC().Format(time.RFC3339),
270270 }
271271
272272 w.Header().Set("Content-Type", "application/json")
@@ -147,7 +147,7 @@ func (h *Handler) handlePassword(w http.ResponseWriter, r *http.Request) {
147 response := PasswordResponse{147 response := PasswordResponse{
148 Token: token,148 Token: token,
149 ExpiresIn: int(expiry.Seconds()),149 ExpiresIn: int(expiry.Seconds()),
150- IssuedAt: time.Now().Format(time.RFC3339),150+ IssuedAt: time.Now().UTC().Format(time.RFC3339),
151 }151 }
152 152
153 w.Header().Set("Content-Type", "application/json")153 w.Header().Set("Content-Type", "application/json")
@@ -244,7 +244,7 @@ func (h *Handler) handleToken(w http.ResponseWriter, r *http.Request) {
244 response := dto.TokenResponse{244 response := dto.TokenResponse{
245 Token: accessToken,245 Token: accessToken,
246 ExpiresIn: 300, // 5 minutes in seconds246 ExpiresIn: 300, // 5 minutes in seconds
247- IssuedAt: time.Now().Format(time.RFC3339),247+ IssuedAt: time.Now().UTC().Format(time.RFC3339),
248 }248 }
249 249
250 w.Header().Set("Content-Type", "application/json")250 w.Header().Set("Content-Type", "application/json")
@@ -266,7 +266,7 @@ func (h *Handler) sendAnonymousToken(w http.ResponseWriter, log zerowrap.Logger)
266 response := dto.TokenResponse{266 response := dto.TokenResponse{
267 Token: "", // Empty token indicates limited access267 Token: "", // Empty token indicates limited access
268 ExpiresIn: 60, // 1 minute268 ExpiresIn: 60, // 1 minute
269- IssuedAt: time.Now().Format(time.RFC3339),269+ IssuedAt: time.Now().UTC().Format(time.RFC3339),
270 }270 }
271 271
272 w.Header().Set("Content-Type", "application/json")272 w.Header().Set("Content-Type", "application/json")
M internal/domain/auth.go +1 -1
@@ -43,7 +43,7 @@ func (t *Token) IsExpired() bool {
4343 if t.ExpiresAt.IsZero() {
4444 return false // Never expires
4545 }
46- return time.Now().After(t.ExpiresAt)
46+ return time.Now().UTC().After(t.ExpiresAt)
4747 }
4848
4949 // SecretsBackend represents the type of secrets storage backend.
@@ -43,7 +43,7 @@ func (t *Token) IsExpired() bool {
43 if t.ExpiresAt.IsZero() {43 if t.ExpiresAt.IsZero() {
44 return false // Never expires44 return false // Never expires
45 }45 }
46- return time.Now().After(t.ExpiresAt)46+ return time.Now().UTC().After(t.ExpiresAt)
47 }47 }
48 48
49 // SecretsBackend represents the type of secrets storage backend.49 // SecretsBackend represents the type of secrets storage backend.
M internal/usecase/auth/service.go +4 -4
@@ -231,7 +231,7 @@ func ensureTokenNotExpired(tokenClaims *domain.TokenClaims, log zerowrap.Logger)
231231 return nil
232232 }
233233
234- if time.Now().Unix() > tokenClaims.ExpiresAt {
234+ if time.Now().UTC().Unix() > tokenClaims.ExpiresAt {
235235 log.Debug().Str("token_id", tokenClaims.ID).Msg("token has expired")
236236 return domain.ErrExpiredToken
237237 }
@@ -256,7 +256,7 @@ func (s *Service) isEphemeralAccessToken(claims *domain.TokenClaims) bool {
256256
257257 // Security: ensure token was recently issued to prevent attackers with
258258 // stolen secrets from creating arbitrary short-lived tokens
259- now := time.Now().Unix()
259+ now := time.Now().UTC().Unix()
260260 tokenAge := now - claims.IssuedAt
261261 if tokenAge > maxAccessTokenLifetimeSecs {
262262 return false // Old tokens require store validation
@@ -275,7 +275,7 @@ func (s *Service) GenerateToken(ctx context.Context, subject string, scopes []st
275275 log := zerowrap.FromCtx(ctx)
276276
277277 tokenID := uuid.New().String()
278- now := time.Now()
278+ now := time.Now().UTC()
279279
280280 // Build token
281281 token := &domain.Token{
@@ -345,7 +345,7 @@ func (s *Service) GenerateAccessToken(ctx context.Context, subject string, scope
345345 }
346346
347347 tokenID := uuid.New().String()
348- now := time.Now()
348+ now := time.Now().UTC()
349349
350350 claims := jwt.MapClaims{
351351 "jti": tokenID,
@@ -231,7 +231,7 @@ func ensureTokenNotExpired(tokenClaims *domain.TokenClaims, log zerowrap.Logger)
231 return nil231 return nil
232 }232 }
233 233
234- if time.Now().Unix() > tokenClaims.ExpiresAt {234+ if time.Now().UTC().Unix() > tokenClaims.ExpiresAt {
235 log.Debug().Str("token_id", tokenClaims.ID).Msg("token has expired")235 log.Debug().Str("token_id", tokenClaims.ID).Msg("token has expired")
236 return domain.ErrExpiredToken236 return domain.ErrExpiredToken
237 }237 }
@@ -256,7 +256,7 @@ func (s *Service) isEphemeralAccessToken(claims *domain.TokenClaims) bool {
256 256
257 // Security: ensure token was recently issued to prevent attackers with257 // Security: ensure token was recently issued to prevent attackers with
258 // stolen secrets from creating arbitrary short-lived tokens258 // stolen secrets from creating arbitrary short-lived tokens
259- now := time.Now().Unix()259+ now := time.Now().UTC().Unix()
260 tokenAge := now - claims.IssuedAt260 tokenAge := now - claims.IssuedAt
261 if tokenAge > maxAccessTokenLifetimeSecs {261 if tokenAge > maxAccessTokenLifetimeSecs {
262 return false // Old tokens require store validation262 return false // Old tokens require store validation
@@ -275,7 +275,7 @@ func (s *Service) GenerateToken(ctx context.Context, subject string, scopes []st
275 log := zerowrap.FromCtx(ctx)275 log := zerowrap.FromCtx(ctx)
276 276
277 tokenID := uuid.New().String()277 tokenID := uuid.New().String()
278- now := time.Now()278+ now := time.Now().UTC()
279 279
280 // Build token280 // Build token
281 token := &domain.Token{281 token := &domain.Token{
@@ -345,7 +345,7 @@ func (s *Service) GenerateAccessToken(ctx context.Context, subject string, scope
345 }345 }
346 346
347 tokenID := uuid.New().String()347 tokenID := uuid.New().String()
348- now := time.Now()348+ now := time.Now().UTC()
349 349
350 claims := jwt.MapClaims{350 claims := jwt.MapClaims{
351 "jti": tokenID,351 "jti": tokenID,

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 7

Tags 106

↑↓ moveenter openesc close