2026-04-25 13:21:39 +08:00

662 lines
23 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package auth
import (
"context"
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/hex"
"fmt"
"strconv"
"strings"
"time"
jwt "github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
"hyapp/pkg/idgen"
"hyapp/pkg/xerr"
authdomain "hyapp/services/user-service/internal/domain/auth"
userdomain "hyapp/services/user-service/internal/domain/user"
userservice "hyapp/services/user-service/internal/service/user"
)
const (
hashAlgBcrypt = "bcrypt"
tokenType = "Bearer"
loginPassword = "password"
loginThird = "third_party"
loginRefresh = "refresh"
loginSetPass = "set_password"
resultSuccess = "success"
resultFailed = "failed"
)
// AuthRepository 是认证用例的持久化边界。
// 跨表写入仍按用例保留事务入口,例如三方首次登录要一次性创建用户、默认短号、三方身份和 session。
type AuthRepository interface {
SetPassword(ctx context.Context, account authdomain.PasswordAccount) error
FindPasswordByDisplayUserID(ctx context.Context, displayUserID string, nowMs int64) (authdomain.PasswordAccount, error)
CreateSession(ctx context.Context, session authdomain.Session) error
FindActiveSessionByRefreshHash(ctx context.Context, refreshTokenHash string, nowMs int64) (authdomain.Session, error)
ReplaceSession(ctx context.Context, oldSessionID string, newSession authdomain.Session, revokedAtMs int64) error
RevokeSession(ctx context.Context, sessionID string, refreshTokenHash string, revokedAtMs int64) (bool, error)
FindThirdPartyIdentity(ctx context.Context, provider string, providerSubject string) (authdomain.ThirdPartyIdentity, error)
CreateThirdPartyUser(ctx context.Context, user userdomain.User, identity userdomain.Identity, thirdParty authdomain.ThirdPartyIdentity, session authdomain.Session) error
RecordLoginAudit(ctx context.Context, audit authdomain.LoginAudit) error
}
// UserRepository 为 auth service 提供登录签发前的用户快照读取。
// Auth service 不关心用户创建和短号修改细节,只需要判断用户是否可登录以及签发当前展示号。
type UserRepository interface {
GetUser(ctx context.Context, userID int64) (userdomain.User, error)
}
// IdentityRepository 为 auth service 提供靓号懒过期能力。
// 过期恢复仍由 identity repository 的事务完成,避免 auth service 手动改 users 和短号表。
type IdentityRepository interface {
ExpirePrettyDisplayUserID(ctx context.Context, command userdomain.ExpirePrettyDisplayUserIDCommand) (userdomain.Identity, error)
}
// Repository 是兼容旧 WithRepository 调用的组合接口。
// 新增实现可以分别注入 AuthRepository、UserRepository 和 IdentityRepository避免继续扩大单一接口。
type Repository interface {
AuthRepository
UserRepository
IdentityRepository
}
// ThirdPartyVerifier 把 provider credential 校验成稳定 provider_subject。
// gateway 不接触 provider secret三方校验必须停留在 user-service。
type ThirdPartyVerifier interface {
Verify(ctx context.Context, provider string, credential string) (authdomain.ThirdPartyProfile, error)
}
// Config 保存认证底座需要的稳定参数。
type Config struct {
Issuer string
AccessTokenTTLSec int64
RefreshTokenTTLSec int64
SigningAlg string
SigningSecret string
}
// Service 承载登录注册用例。
type Service struct {
cfg Config
authRepository AuthRepository
userRepository UserRepository
identityRepository IdentityRepository
idGenerator userservice.IDGenerator
displayUserIDAllocator userservice.DisplayUserIDAllocator
thirdPartyVerifier ThirdPartyVerifier
now func() time.Time
allocateMaxAttempts int
}
// Option 调整 auth service 的依赖和策略。
type Option func(*Service)
// New 创建认证服务。
func New(cfg Config, options ...Option) *Service {
svc := &Service{
cfg: cfg,
idGenerator: idgen.NewInt64Generator(0),
displayUserIDAllocator: userservice.NumericDisplayUserIDAllocator{},
thirdPartyVerifier: NewStaticThirdPartyVerifier([]string{"wechat"}),
now: time.Now,
allocateMaxAttempts: 8,
}
for _, option := range options {
option(svc)
}
return svc
}
// WithRepository 注入认证持久化实现。
func WithRepository(repository Repository) Option {
return func(s *Service) {
s.authRepository = repository
s.userRepository = repository
s.identityRepository = repository
}
}
// WithAuthRepository 注入纯认证持久化实现。
func WithAuthRepository(repository AuthRepository) Option {
return func(s *Service) {
if repository != nil {
s.authRepository = repository
}
}
}
// WithUserRepository 注入用户快照读取实现。
func WithUserRepository(repository UserRepository) Option {
return func(s *Service) {
if repository != nil {
s.userRepository = repository
}
}
}
// WithIdentityRepository 注入短号懒过期实现。
func WithIdentityRepository(repository IdentityRepository) Option {
return func(s *Service) {
if repository != nil {
s.identityRepository = repository
}
}
}
// WithIDGenerator 注入 user_id 发号器。
func WithIDGenerator(generator userservice.IDGenerator) Option {
return func(s *Service) {
if generator != nil {
s.idGenerator = generator
}
}
}
// WithDisplayUserIDAllocator 注入短号候选生成器。
func WithDisplayUserIDAllocator(allocator userservice.DisplayUserIDAllocator) Option {
return func(s *Service) {
if allocator != nil {
s.displayUserIDAllocator = allocator
}
}
}
// WithThirdPartyVerifier 注入三方 provider 校验器。
func WithThirdPartyVerifier(verifier ThirdPartyVerifier) Option {
return func(s *Service) {
if verifier != nil {
s.thirdPartyVerifier = verifier
}
}
}
// WithClock 注入时间函数,保证 session 轮换测试稳定。
func WithClock(now func() time.Time) Option {
return func(s *Service) {
if now != nil {
s.now = now
}
}
}
// WithDisplayUserIDAllocateMaxAttempts 配置默认短号分配重试次数。
func WithDisplayUserIDAllocateMaxAttempts(maxAttempts int) Option {
return func(s *Service) {
if maxAttempts > 0 {
s.allocateMaxAttempts = maxAttempts
}
}
}
// LoginPassword 使用当前 display_user_id 和用户后续设置的密码创建新 session。
// 用户不存在、未设置密码和密码错误统一返回 AUTH_FAILED避免暴露可枚举身份状态。
func (s *Service) LoginPassword(ctx context.Context, displayUserID string, password string, deviceID string, meta Meta) (authdomain.Token, error) {
displayUserID = strings.TrimSpace(displayUserID)
if displayUserID == "" || password == "" || strings.TrimSpace(deviceID) == "" {
return authdomain.Token{}, xerr.New(xerr.InvalidArgument, "display_user_id, password and device_id are required")
}
if s.authRepository == nil {
return authdomain.Token{}, xerr.New(xerr.Unavailable, "auth repository is not configured")
}
if !userdomain.ValidDisplayUserID(displayUserID) {
s.audit(ctx, meta, 0, loginPassword, "", resultFailed, string(xerr.AuthFailed))
return authdomain.Token{}, authFailed()
}
nowMs := s.now().UnixMilli()
accountRecord, err := s.authRepository.FindPasswordByDisplayUserID(ctx, displayUserID, nowMs)
if err != nil {
s.audit(ctx, meta, 0, loginPassword, "", resultFailed, string(xerr.AuthFailed))
return authdomain.Token{}, authFailed()
}
if err := verifyPassword(accountRecord.PasswordHash, password); err != nil {
s.audit(ctx, meta, accountRecord.UserID, loginPassword, "", resultFailed, string(xerr.AuthFailed))
return authdomain.Token{}, authFailed()
}
user, err := s.freshUser(ctx, accountRecord.UserID, nowMs, meta.RequestID)
if err != nil {
s.audit(ctx, meta, accountRecord.UserID, loginPassword, "", resultFailed, string(xerr.CodeOf(err)))
return authdomain.Token{}, err
}
if !user.CanLogin() {
s.audit(ctx, meta, user.UserID, loginPassword, "", resultFailed, string(xerr.UserDisabled))
return authdomain.Token{}, xerr.New(xerr.UserDisabled, "user is disabled")
}
session, refreshToken, err := s.newSession(user.UserID, deviceID)
if err != nil {
return authdomain.Token{}, err
}
if err := s.authRepository.CreateSession(ctx, session); err != nil {
s.audit(ctx, meta, user.UserID, loginPassword, "", resultFailed, string(xerr.CodeOf(err)))
return authdomain.Token{}, err
}
s.audit(ctx, meta, user.UserID, loginPassword, "", resultSuccess, "")
return s.issueToken(user, session.SessionID, refreshToken)
}
// LoginThirdParty 校验三方凭证,存在则登录,不存在则创建用户和三方绑定。
func (s *Service) LoginThirdParty(ctx context.Context, provider string, credential string, deviceID string, meta Meta) (authdomain.Token, bool, error) {
provider = strings.ToLower(strings.TrimSpace(provider))
if provider == "" || strings.TrimSpace(credential) == "" || strings.TrimSpace(deviceID) == "" {
return authdomain.Token{}, false, xerr.New(xerr.InvalidArgument, "provider, credential and device_id are required")
}
if s.authRepository == nil {
return authdomain.Token{}, false, xerr.New(xerr.Unavailable, "auth repository is not configured")
}
profile, err := s.thirdPartyVerifier.Verify(ctx, provider, credential)
if err != nil {
s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.AuthFailed))
return authdomain.Token{}, false, authFailed()
}
identity, err := s.authRepository.FindThirdPartyIdentity(ctx, provider, profile.ProviderSubject)
if err == nil {
return s.loginExistingThirdParty(ctx, identity, deviceID, meta)
}
if !xerr.IsCode(err, xerr.NotFound) {
s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.CodeOf(err)))
return authdomain.Token{}, false, err
}
return s.createThirdPartyUser(ctx, provider, profile, deviceID, meta)
}
// SetPassword 为已经三方登录创建出的用户首次设置密码。
// 该入口必须由 gateway 先校验 access token 后传入 user_id不能再接受客户端自报用户。
func (s *Service) SetPassword(ctx context.Context, userID int64, password string, meta Meta) error {
if userID <= 0 || password == "" {
return xerr.New(xerr.InvalidArgument, "user_id and password are required")
}
if s.authRepository == nil {
return xerr.New(xerr.Unavailable, "auth repository is not configured")
}
user, err := s.freshUser(ctx, userID, s.now().UnixMilli(), meta.RequestID)
if err != nil {
s.audit(ctx, meta, userID, loginSetPass, "", resultFailed, string(xerr.CodeOf(err)))
return err
}
if !user.CanLogin() {
s.audit(ctx, meta, userID, loginSetPass, "", resultFailed, string(xerr.UserDisabled))
return xerr.New(xerr.UserDisabled, "user is disabled")
}
passwordHash, err := hashPassword(password)
if err != nil {
return err
}
nowMs := s.now().UnixMilli()
err = s.authRepository.SetPassword(ctx, authdomain.PasswordAccount{
UserID: user.UserID,
DisplayUserID: user.CurrentDisplayUserID,
PasswordHash: passwordHash,
HashAlg: hashAlgBcrypt,
CreatedAtMs: nowMs,
UpdatedAtMs: nowMs,
})
if err != nil {
s.audit(ctx, meta, userID, loginSetPass, "", resultFailed, string(xerr.CodeOf(err)))
return err
}
s.audit(ctx, meta, userID, loginSetPass, "", resultSuccess, "")
return nil
}
// RefreshToken 轮换 refresh token旧 token 成功使用后立即失效。
func (s *Service) RefreshToken(ctx context.Context, refreshToken string, deviceID string, meta Meta) (authdomain.Token, error) {
if strings.TrimSpace(refreshToken) == "" || strings.TrimSpace(deviceID) == "" {
return authdomain.Token{}, xerr.New(xerr.InvalidArgument, "refresh_token and device_id are required")
}
if s.authRepository == nil {
return authdomain.Token{}, xerr.New(xerr.Unavailable, "auth repository is not configured")
}
nowMs := s.now().UnixMilli()
session, err := s.authRepository.FindActiveSessionByRefreshHash(ctx, hashRefreshToken(refreshToken), nowMs)
if err != nil {
reason := xerr.SessionRevoked
if xerr.IsCode(err, xerr.SessionExpired) {
reason = xerr.SessionExpired
}
s.audit(ctx, meta, 0, loginRefresh, "", resultFailed, string(reason))
return authdomain.Token{}, err
}
if subtle.ConstantTimeCompare([]byte(session.DeviceID), []byte(deviceID)) != 1 {
s.audit(ctx, meta, session.UserID, loginRefresh, "", resultFailed, string(xerr.SessionRevoked))
return authdomain.Token{}, xerr.New(xerr.SessionRevoked, "session revoked")
}
user, err := s.freshUser(ctx, session.UserID, nowMs, meta.RequestID)
if err != nil {
s.audit(ctx, meta, session.UserID, loginRefresh, "", resultFailed, string(xerr.CodeOf(err)))
return authdomain.Token{}, err
}
if !user.CanLogin() {
s.audit(ctx, meta, session.UserID, loginRefresh, "", resultFailed, string(xerr.UserDisabled))
return authdomain.Token{}, xerr.New(xerr.UserDisabled, "user is disabled")
}
newSession, newRefreshToken, err := s.newSession(session.UserID, deviceID)
if err != nil {
return authdomain.Token{}, err
}
if err := s.authRepository.ReplaceSession(ctx, session.SessionID, newSession, nowMs); err != nil {
s.audit(ctx, meta, session.UserID, loginRefresh, "", resultFailed, string(xerr.CodeOf(err)))
return authdomain.Token{}, err
}
s.audit(ctx, meta, session.UserID, loginRefresh, "", resultSuccess, "")
return s.issueToken(user, newSession.SessionID, newRefreshToken)
}
// Logout 失效指定 session 或 refresh token。
func (s *Service) Logout(ctx context.Context, sessionID string, refreshToken string, meta Meta) (bool, error) {
if strings.TrimSpace(sessionID) == "" && strings.TrimSpace(refreshToken) == "" {
return false, xerr.New(xerr.InvalidArgument, "session_id or refresh_token is required")
}
if s.authRepository == nil {
return false, xerr.New(xerr.Unavailable, "auth repository is not configured")
}
revoked, err := s.authRepository.RevokeSession(ctx, sessionID, hashRefreshToken(refreshToken), s.now().UnixMilli())
if err != nil {
s.audit(ctx, meta, 0, "logout", "", resultFailed, string(xerr.CodeOf(err)))
return false, err
}
s.audit(ctx, meta, 0, "logout", "", resultSuccess, "")
return revoked, nil
}
// TokenConfigValid 判断签发配置是否具备启动条件。
func (s *Service) TokenConfigValid() bool {
return s.cfg.Issuer != "" && s.cfg.AccessTokenTTLSec > 0 && s.cfg.RefreshTokenTTLSec > 0 && s.cfg.SigningAlg == jwt.SigningMethodHS256.Alg() && s.cfg.SigningSecret != ""
}
// Meta 是 gateway 透传到 user-service 的审计元信息。
type Meta struct {
RequestID string
ClientIP string
UserAgent string
}
func (s *Service) loginExistingThirdParty(ctx context.Context, identity authdomain.ThirdPartyIdentity, deviceID string, meta Meta) (authdomain.Token, bool, error) {
user, err := s.freshUser(ctx, identity.UserID, s.now().UnixMilli(), meta.RequestID)
if err != nil {
s.audit(ctx, meta, identity.UserID, loginThird, identity.Provider, resultFailed, string(xerr.CodeOf(err)))
return authdomain.Token{}, false, err
}
if !user.CanLogin() {
s.audit(ctx, meta, user.UserID, loginThird, identity.Provider, resultFailed, string(xerr.UserDisabled))
return authdomain.Token{}, false, xerr.New(xerr.UserDisabled, "user is disabled")
}
session, refreshToken, err := s.newSession(user.UserID, deviceID)
if err != nil {
return authdomain.Token{}, false, err
}
if err := s.authRepository.CreateSession(ctx, session); err != nil {
return authdomain.Token{}, false, err
}
s.audit(ctx, meta, user.UserID, loginThird, identity.Provider, resultSuccess, "")
token, err := s.issueToken(user, session.SessionID, refreshToken)
return token, false, err
}
func (s *Service) createThirdPartyUser(ctx context.Context, provider string, profile authdomain.ThirdPartyProfile, deviceID string, meta Meta) (authdomain.Token, bool, error) {
var lastErr error
for attempt := 0; attempt < s.allocateMaxAttempts; attempt++ {
user, displayIdentity := s.newUserWithIdentity(attempt)
if !userdomain.ValidDisplayUserID(displayIdentity.DisplayUserID) {
continue
}
session, refreshToken, err := s.newSession(user.UserID, deviceID)
if err != nil {
return authdomain.Token{}, false, err
}
thirdParty := authdomain.ThirdPartyIdentity{
UserID: user.UserID,
Provider: provider,
ProviderSubject: profile.ProviderSubject,
ProviderUnionID: profile.ProviderUnionID,
CreatedAtMs: user.CreatedAtMs,
UpdatedAtMs: user.UpdatedAtMs,
}
err = s.authRepository.CreateThirdPartyUser(ctx, user, displayIdentity, thirdParty, session)
if err == nil {
s.audit(ctx, meta, user.UserID, loginThird, provider, resultSuccess, "")
token, tokenErr := s.issueToken(user, session.SessionID, refreshToken)
return token, true, tokenErr
}
if xerr.IsCode(err, xerr.DisplayUserIDExists) {
lastErr = err
continue
}
if xerr.IsCode(err, xerr.Conflict) {
identity, findErr := s.authRepository.FindThirdPartyIdentity(ctx, provider, profile.ProviderSubject)
if findErr == nil {
return s.loginExistingThirdParty(ctx, identity, deviceID, meta)
}
}
s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.CodeOf(err)))
return authdomain.Token{}, false, err
}
if lastErr != nil {
s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.DisplayUserIDAllocateFailed))
}
return authdomain.Token{}, false, xerr.New(xerr.DisplayUserIDAllocateFailed, "display_user_id allocation failed")
}
func (s *Service) newUserWithIdentity(attempt int) (userdomain.User, userdomain.Identity) {
userID := s.idGenerator.NewInt64()
nowMs := s.now().UnixMilli()
displayUserID := s.displayUserIDAllocator.NextDisplayUserID(userID, attempt)
return userdomain.User{
UserID: userID,
DefaultDisplayUserID: displayUserID,
CurrentDisplayUserID: displayUserID,
CurrentDisplayUserIDKind: userdomain.DisplayUserIDKindDefault,
Status: userdomain.StatusActive,
CreatedAtMs: nowMs,
UpdatedAtMs: nowMs,
}, userdomain.Identity{
UserID: userID,
DisplayUserID: displayUserID,
DefaultDisplayUserID: displayUserID,
DisplayUserIDKind: userdomain.DisplayUserIDKindDefault,
Status: userdomain.DisplayUserIDStatusActive,
}
}
func (s *Service) newSession(userID int64, deviceID string) (authdomain.Session, string, error) {
refreshToken, err := randomToken("refresh")
if err != nil {
return authdomain.Session{}, "", err
}
nowMs := s.now().UnixMilli()
sessionID := idgen.New("sess")
return authdomain.Session{
SessionID: sessionID,
UserID: userID,
RefreshTokenHash: hashRefreshToken(refreshToken),
DeviceID: strings.TrimSpace(deviceID),
ExpiresAtMs: nowMs + s.cfg.RefreshTokenTTLSec*1000,
CreatedAtMs: nowMs,
UpdatedAtMs: nowMs,
}, refreshToken, nil
}
func (s *Service) issueToken(user userdomain.User, sessionID string, refreshToken string) (authdomain.Token, error) {
user = user.NormalizeDisplayUserIDState()
now := s.now()
expiresAt := now.Add(time.Duration(s.cfg.AccessTokenTTLSec) * time.Second)
claims := jwt.MapClaims{
"iss": s.cfg.Issuer,
"sub": strconv.FormatInt(user.UserID, 10),
"user_id": float64(user.UserID),
"display_user_id": user.CurrentDisplayUserID,
"default_display_user_id": user.DefaultDisplayUserID,
"display_user_id_kind": string(user.CurrentDisplayUserIDKind),
"display_user_id_expires_at_ms": user.CurrentDisplayUserIDExpiresAtMs,
"sid": sessionID,
"typ": "access",
"iat": now.Unix(),
"exp": expiresAt.Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
accessToken, err := token.SignedString([]byte(s.cfg.SigningSecret))
if err != nil {
return authdomain.Token{}, err
}
return authdomain.Token{
UserID: user.UserID,
DisplayUserID: user.CurrentDisplayUserID,
DefaultDisplayUserID: user.DefaultDisplayUserID,
DisplayUserIDKind: string(user.CurrentDisplayUserIDKind),
DisplayUserIDExpiresAtMs: user.CurrentDisplayUserIDExpiresAtMs,
SessionID: sessionID,
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresInSec: s.cfg.AccessTokenTTLSec,
TokenType: tokenType,
}, nil
}
func (s *Service) freshUser(ctx context.Context, userID int64, nowMs int64, requestID string) (userdomain.User, error) {
if s.userRepository == nil {
return userdomain.User{}, xerr.New(xerr.Unavailable, "user repository is not configured")
}
user, err := s.userRepository.GetUser(ctx, userID)
if err != nil {
return userdomain.User{}, err
}
user = user.NormalizeDisplayUserIDState()
if !user.DisplayUserIDExpired(nowMs) {
return user, nil
}
if s.identityRepository == nil {
return userdomain.User{}, xerr.New(xerr.Unavailable, "identity repository is not configured")
}
if _, err := s.identityRepository.ExpirePrettyDisplayUserID(ctx, userdomain.ExpirePrettyDisplayUserIDCommand{
UserID: userID,
RequestID: requestID,
NowMs: nowMs,
}); err != nil {
return userdomain.User{}, err
}
return s.userRepository.GetUser(ctx, userID)
}
func (s *Service) audit(ctx context.Context, meta Meta, userID int64, loginType string, provider string, result string, failureCode string) {
if s.authRepository == nil {
return
}
_ = s.authRepository.RecordLoginAudit(ctx, authdomain.LoginAudit{
RequestID: meta.RequestID,
UserID: userID,
LoginType: loginType,
Provider: provider,
Result: result,
FailureCode: failureCode,
ClientIP: meta.ClientIP,
UserAgent: meta.UserAgent,
CreatedAtMs: s.now().UnixMilli(),
})
}
// StaticThirdPartyVerifier 是本地 v1 verifier。
// credential 在首版本地实现中就是 provider_subject真实 SDK 可替换该接口。
type StaticThirdPartyVerifier struct {
allowed map[string]bool
}
// NewStaticThirdPartyVerifier 创建 provider allowlist 校验器。
func NewStaticThirdPartyVerifier(providers []string) *StaticThirdPartyVerifier {
allowed := make(map[string]bool, len(providers))
for _, provider := range providers {
provider = strings.ToLower(strings.TrimSpace(provider))
if provider != "" {
allowed[provider] = true
}
}
return &StaticThirdPartyVerifier{allowed: allowed}
}
// Verify 把本地 credential 解析为 provider_subject。
func (v *StaticThirdPartyVerifier) Verify(_ context.Context, provider string, credential string) (authdomain.ThirdPartyProfile, error) {
provider = strings.ToLower(strings.TrimSpace(provider))
subject := strings.TrimSpace(credential)
if !v.allowed[provider] || subject == "" {
return authdomain.ThirdPartyProfile{}, authFailed()
}
return authdomain.ThirdPartyProfile{ProviderSubject: subject}, nil
}
func hashPassword(password string) (string, error) {
body, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(body), nil
}
func verifyPassword(passwordHash string, password string) error {
if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(password)); err != nil {
return authFailed()
}
return nil
}
func randomToken(prefix string) (string, error) {
var entropy [32]byte
if _, err := rand.Read(entropy[:]); err != nil {
return "", err
}
return fmt.Sprintf("%s_%s", prefix, base64.RawURLEncoding.EncodeToString(entropy[:])), nil
}
func hashRefreshToken(refreshToken string) string {
if refreshToken == "" {
return ""
}
sum := sha256.Sum256([]byte(refreshToken))
return hex.EncodeToString(sum[:])
}
func authFailed() error {
return xerr.New(xerr.AuthFailed, "authentication failed")
}