mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-08 10:49:06 +02:00
87 lines
2.5 KiB
Go
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
|
|
}
|