Files
story/internal/onboarding/trusted_login_device.go
T

68 lines
2.8 KiB
Go
Raw Normal View History

package onboarding
import (
"context"
"net/http"
"time"
"github.com/jackc/pgx/v5"
)
const trustedLoginLifetime = 30 * 24 * time.Hour
func (s *Service) loginRealm() string {
if s.operatorLogin {
return "operator"
}
return "customer"
}
func (s *Service) trustedLoginCookieName() string {
name := "platform-" + s.loginRealm() + "-trusted-device"
if !s.options.AllowLocalHTTP {
name = "__Host-" + name
}
return name
}
func (s *Service) trustedLoginCookie(r *http.Request) string {
if c, err := r.Cookie(s.trustedLoginCookieName()); err == nil && validB64(c.Value, 32) {
return c.Value
}
return ""
}
// Only a successfully verified, single-use SMS can establish device trust.
// Bind it to the exact credential so an administrative password reset also
// invalidates previously trusted devices without needing a separate revocation.
func (s *Service) rememberLoginDevice(ctx context.Context, tx pgx.Tx, phoneHash string) (string, error) {
var encoded string
if err := tx.QueryRow(ctx, "SELECT COALESCE(password_hash,'') FROM "+s.phoneCredentialTable()+" WHERE phone_digest=$1 FOR SHARE", phoneHash).Scan(&encoded); err != nil {
return "", err
}
token := randomToken(32)
_, err := tx.Exec(ctx, `INSERT INTO trusted_login_device(token_hash,realm,phone_digest,credential_digest,verified_at,expires_at) VALUES($1,$2,$3,$4,$5,$6)`, digest([]byte(token)), s.loginRealm(), phoneHash, digest([]byte(encoded)), s.now(), s.now().Add(trustedLoginLifetime))
return token, err
}
func (s *Service) requireTrustedLoginDevice(ctx context.Context, tx pgx.Tx, phoneHash, encoded, token string) error {
trusted := false
if validB64(token, 32) {
err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM trusted_login_device WHERE token_hash=$1 AND realm=$2 AND phone_digest=$3 AND credential_digest=$4 AND expires_at>$5 AND revoked_at IS NULL)`, digest([]byte(token)), s.loginRealm(), phoneHash, digest([]byte(encoded)), s.now()).Scan(&trusted)
if err != nil {
return err
}
}
if !trusted {
return fail(403, "NEW_DEVICE_SMS_REQUIRED", "首次在此设备登录,请使用手机验证码完成验证")
}
return nil
}
func (s *Service) updateTrustedDevicesAfterPassword(ctx context.Context, tx pgx.Tx, phoneHash, oldHash, newHash, currentToken string) error {
// Retain only an existing, unexpired SMS verification on this browser. A
// password change must never turn an unverified browser into a trusted one.
_, err := tx.Exec(ctx, `UPDATE trusted_login_device SET credential_digest=CASE WHEN token_hash=$3 AND credential_digest=$4 AND expires_at>$6 THEN $5 ELSE credential_digest END,revoked_at=CASE WHEN token_hash=$3 AND credential_digest=$4 AND expires_at>$6 THEN NULL ELSE $6 END WHERE realm=$1 AND phone_digest=$2 AND revoked_at IS NULL`, s.loginRealm(), phoneHash, digest([]byte(currentToken)), digest([]byte(oldHash)), digest([]byte(newHash)), s.now())
return err
}