mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-15 08:02:42 +00:00
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
85 lines
3.0 KiB
Go
85 lines
3.0 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/warmbly/warmbly/internal/errx"
|
|
"github.com/warmbly/warmbly/internal/observability/errs"
|
|
"github.com/warmbly/warmbly/internal/pkg/crypt"
|
|
)
|
|
|
|
// A trusted device is a browser the account owner asked to remember after
|
|
// entering an emailed login code. The browser keeps a random 256-bit token and
|
|
// the server keeps only its SHA-256, bound to the user, so a token is useless
|
|
// for any other account and a leaked cache reveals nothing replayable.
|
|
//
|
|
// It skips the emailed code and nothing else: the password, the captcha, TOTP
|
|
// and the sign-in risk assessment all still apply. The window is absolute,
|
|
// never extended by use, and every trusted device is forgotten when the
|
|
// password changes or the user signs out everywhere.
|
|
|
|
func getTrustedDeviceKey(userID uuid.UUID, tokenHash string) string {
|
|
return getTrustedDevicePrefix(userID) + tokenHash
|
|
}
|
|
|
|
func getTrustedDevicePrefix(userID uuid.UUID) string {
|
|
return "trusted_device:" + userID.String() + ":"
|
|
}
|
|
|
|
// trustDevice mints a token for this browser. An empty result means the
|
|
// device could not be remembered, which only costs the user a code next time.
|
|
func (s *authService) trustDevice(ctx context.Context, userID uuid.UUID) string {
|
|
b := make([]byte, 32)
|
|
if _, err := rand.Read(b); err != nil {
|
|
errs.CaptureException(err)
|
|
return ""
|
|
}
|
|
tok := base64.RawURLEncoding.EncodeToString(b)
|
|
if err := s.cache.SetEx(ctx, getTrustedDeviceKey(userID, crypt.SHA256(tok)), "1", TrustedDeviceTTL).Err(); err != nil {
|
|
errs.CaptureExceptionContext(ctx, err, errs.Tag("area", "trusted_device"))
|
|
return ""
|
|
}
|
|
return tok
|
|
}
|
|
|
|
// isTrustedDevice reports whether token was issued to this user and is still
|
|
// inside its window. A cache error fails closed into "send a code".
|
|
func (s *authService) isTrustedDevice(ctx context.Context, userID uuid.UUID, token string) bool {
|
|
token = strings.TrimSpace(token)
|
|
if token == "" || len(token) > 128 || s.cache == nil {
|
|
return false
|
|
}
|
|
n, err := s.cache.Exists(ctx, getTrustedDeviceKey(userID, crypt.SHA256(token))).Result()
|
|
return err == nil && n > 0
|
|
}
|
|
|
|
// ForgetTrustedDevices drops every remembered device for the user, so the next
|
|
// sign-in from each of them asks for an emailed code again. An error means
|
|
// some may survive, so callers must not report the revocation as done.
|
|
func (s *authService) ForgetTrustedDevices(ctx context.Context, userID uuid.UUID) *errx.Error {
|
|
if s.cache == nil {
|
|
return nil
|
|
}
|
|
iter := s.cache.Scan(ctx, 0, getTrustedDevicePrefix(userID)+"*", 100).Iterator()
|
|
var keys []string
|
|
for iter.Next(ctx) {
|
|
keys = append(keys, iter.Val())
|
|
}
|
|
if err := iter.Err(); err != nil {
|
|
errs.CaptureExceptionContext(ctx, err, errs.Tag("area", "trusted_device"))
|
|
return errx.InternalError()
|
|
}
|
|
if len(keys) == 0 {
|
|
return nil
|
|
}
|
|
if err := s.cache.Del(ctx, keys...).Err(); err != nil {
|
|
errs.CaptureExceptionContext(ctx, err, errs.Tag("area", "trusted_device"))
|
|
return errx.InternalError()
|
|
}
|
|
return nil
|
|
}
|