Files
pocket-id/backend/internal/auditlogs/known_browser.go
T

87 lines
2.5 KiB
Go

package auditlogs
import (
"errors"
"fmt"
"time"
"github.com/lestrrat-go/jwx/v4/jwt"
)
const (
KnownBrowserLifetime = 180 * 24 * time.Hour
knownBrowserJWTType = "known-browser"
)
type browserTokenService interface {
generate(userID string, lifetime time.Duration) (string, error)
verify(token, userID string) error
}
type browserTokens struct {
signer SessionTokenService
appURL string
}
// generate creates a notification-only browser marker using the shared session signer
func (s *browserTokens) generate(userID string, lifetime time.Duration) (string, error) {
if s.signer == nil {
return "", errors.New("session token signer is not initialized")
}
if userID == "" || lifetime <= 0 {
return "", errors.New("user ID and positive lifetime are required for a known-browser token")
}
// Bind recognition to one user and instance, with a purpose that access-token validation rejects
now := time.Now()
token, err := jwt.NewBuilder().
Subject(userID).
Issuer(s.appURL).
Audience([]string{s.appURL}).
IssuedAt(now).
Expiration(now.Add(lifetime)).
Claim("type", knownBrowserJWTType).
Build()
if err != nil {
return "", fmt.Errorf("failed to build known-browser token: %w", err)
}
// The shared signer owns the private key and pinned signing algorithm
return s.signer.SignSessionToken(token)
}
// verify validates recognition without granting authentication or reading browser state
func (s *browserTokens) verify(token, userID string) error {
if s.signer == nil {
return errors.New("session token signer is not initialized")
}
if userID == "" {
return errors.New("user ID is required for a known-browser token")
}
_, err := s.signer.VerifySessionToken(token,
jwt.WithIssuer(s.appURL),
jwt.WithAudience(s.appURL),
jwt.WithSubject(userID),
jwt.WithRequiredClaim(jwt.IssuedAtKey),
jwt.WithRequiredClaim(jwt.ExpirationKey),
jwt.WithClaimValue("type", knownBrowserJWTType),
)
if err != nil {
return fmt.Errorf("failed to verify known-browser token: %w", err)
}
return nil
}
func (s *service) rememberBrowser(userID, token string) (bool, string, error) {
// Only a valid token for the authenticated user can suppress the new-browser notification
known := token != "" && s.browserTokens.verify(token, userID) == nil
// Renew recognition after every successful sign-in without persisting browser state
renewed, err := s.browserTokens.generate(userID, KnownBrowserLifetime)
if err != nil {
return known, token, err
}
return known, renewed, nil
}