mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-05 00:02:12 +00:00
92 lines
2.6 KiB
Go
92 lines
2.6 KiB
Go
package token
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/warmbly/warmbly/internal/errx"
|
|
"github.com/warmbly/warmbly/internal/models"
|
|
)
|
|
|
|
func (s *tokenService) GetSession(ctx context.Context, sessionID uuid.UUID) (*models.Session, *errx.Error) {
|
|
sess, err := s.getSession(ctx, sessionID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if sess != nil {
|
|
return sess, nil
|
|
}
|
|
|
|
sess, err = s.tokenRepository.GetSession(ctx, sessionID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Best effort: the session is already resolved, and failing to cache it
|
|
// only costs the next request a database read. Returning the error here
|
|
// meant a Redis write that was refused, on quota or anything else, failed
|
|
// every authenticated request with a 500.
|
|
_ = s.saveSession(ctx, sess, SessionTTL)
|
|
return sess, nil
|
|
}
|
|
|
|
func sameTokenIssueTime(a, b time.Time) bool {
|
|
// JWT issued-at precision can differ from DB timestamp precision.
|
|
// Compare at second precision to avoid false mismatches.
|
|
return a.UTC().Truncate(time.Second).Equal(b.UTC().Truncate(time.Second))
|
|
}
|
|
|
|
func (s *tokenService) ValidateAccessToken(ctx context.Context, accessToken string) (*models.Session, *errx.Error) {
|
|
t, err := s.VerifyTokenFor(PurposeAccess, accessToken)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if t.ExpiresAt.Before(time.Now()) {
|
|
return nil, errx.ErrToken
|
|
}
|
|
|
|
session, err := s.GetSession(ctx, t.SessionID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// A revoked session is dead immediately. Revocation busts the Redis cache,
|
|
// so the next read here re-loads the row with revoked_at set and the token
|
|
// stops working without waiting for the cache TTL or a refresh.
|
|
if session.RevokedAt != nil {
|
|
return nil, errx.ErrToken
|
|
}
|
|
|
|
if session.AccessNonce != t.Nonce || !sameTokenIssueTime(session.LastRefreshedAt, t.IssuedAt.Time) {
|
|
return nil, errx.ErrToken
|
|
}
|
|
|
|
return session, nil
|
|
}
|
|
|
|
// VerifyTokenFor is VerifyToken plus the check that the token was minted for
|
|
// this flow.
|
|
//
|
|
// A token with no purpose is treated as an access token: tokens issued before
|
|
// the claim existed are still inside their 12-hour window during a deploy, and
|
|
// refusing them would sign everybody out. Every other flow demands its purpose
|
|
// explicitly, so that leniency cannot be used to spend a reset or challenge
|
|
// token as a session.
|
|
func (s *tokenService) VerifyTokenFor(purpose, tokenStr string) (*TokenClaims, *errx.Error) {
|
|
claims, err := s.VerifyToken(tokenStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
got := claims.Purpose
|
|
if got == "" {
|
|
got = PurposeAccess
|
|
}
|
|
if got != purpose {
|
|
return nil, errx.ErrToken
|
|
}
|
|
return claims, nil
|
|
}
|