feat: seed secure demo merchant identity
This commit is contained in:
113
internal/identity/service.go
Normal file
113
internal/identity/service.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
var ErrUnauthenticated = errors.New("identity: unauthenticated")
|
||||
|
||||
type Session struct {
|
||||
UserID int64
|
||||
MerchantID int64
|
||||
Username string
|
||||
MerchantSourceKey string
|
||||
MerchantDisplayName string
|
||||
ProfileIsSynthetic bool
|
||||
CreatedAt time.Time
|
||||
IdleExpiresAt time.Time
|
||||
AbsoluteExpiresAt time.Time
|
||||
CSRFToken string
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
pool *pgxpool.Pool
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func New(pool *pgxpool.Pool) *Service { return &Service{pool: pool, now: time.Now} }
|
||||
|
||||
func (s *Service) Login(ctx context.Context, username, password string) (Session, string, error) {
|
||||
var userID, merchantID int64
|
||||
var passwordHash, sourceKey, displayName string
|
||||
var isActive, isSynthetic bool
|
||||
err := s.pool.QueryRow(ctx, `SELECT u.id,u.merchant_id,u.password_hash,u.is_active,m.source_key,m.display_name,m.profile_is_synthetic
|
||||
FROM merchant_users u JOIN merchants m ON m.id=u.merchant_id WHERE u.username=$1`, username).Scan(
|
||||
&userID, &merchantID, &passwordHash, &isActive, &sourceKey, &displayName, &isSynthetic,
|
||||
)
|
||||
if err != nil || !isActive || !VerifyPassword(password, passwordHash) {
|
||||
// Perform an Argon2 calculation for missing users to reduce enumeration timing.
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
_, _ = HashPassword("invalid-credential-padding")
|
||||
}
|
||||
return Session{}, "", ErrUnauthenticated
|
||||
}
|
||||
token, err := randomToken(32)
|
||||
if err != nil {
|
||||
return Session{}, "", err
|
||||
}
|
||||
csrf, err := randomToken(32)
|
||||
if err != nil {
|
||||
return Session{}, "", err
|
||||
}
|
||||
now := s.now().UTC()
|
||||
session := Session{UserID: userID, MerchantID: merchantID, Username: username, MerchantSourceKey: sourceKey,
|
||||
MerchantDisplayName: displayName, ProfileIsSynthetic: isSynthetic, CreatedAt: now,
|
||||
IdleExpiresAt: now.Add(30 * time.Minute), AbsoluteExpiresAt: now.Add(8 * time.Hour), CSRFToken: csrf}
|
||||
hash := sha256.Sum256([]byte(token))
|
||||
_, err = s.pool.Exec(ctx, `INSERT INTO auth_sessions(user_id,merchant_id,token_hash,csrf_token,created_at,idle_expires_at,absolute_expires_at,last_seen_at)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$5)`, userID, merchantID, hash[:], csrf, now, session.IdleExpiresAt, session.AbsoluteExpiresAt)
|
||||
if err != nil {
|
||||
return Session{}, "", fmt.Errorf("creating auth session: %w", err)
|
||||
}
|
||||
return session, token, nil
|
||||
}
|
||||
|
||||
func (s *Service) Resolve(ctx context.Context, token string) (Session, error) {
|
||||
if token == "" {
|
||||
return Session{}, ErrUnauthenticated
|
||||
}
|
||||
hash := sha256.Sum256([]byte(token))
|
||||
var session Session
|
||||
err := s.pool.QueryRow(ctx, `SELECT u.id,m.id,u.username,m.source_key,m.display_name,m.profile_is_synthetic,
|
||||
a.created_at,a.idle_expires_at,a.absolute_expires_at,a.csrf_token
|
||||
FROM auth_sessions a JOIN merchant_users u ON u.id=a.user_id JOIN merchants m ON m.id=a.merchant_id
|
||||
WHERE a.token_hash=$1 AND a.revoked_at IS NULL AND u.is_active
|
||||
AND a.idle_expires_at>now() AND a.absolute_expires_at>now()`, hash[:]).Scan(
|
||||
&session.UserID, &session.MerchantID, &session.Username, &session.MerchantSourceKey,
|
||||
&session.MerchantDisplayName, &session.ProfileIsSynthetic, &session.CreatedAt,
|
||||
&session.IdleExpiresAt, &session.AbsoluteExpiresAt, &session.CSRFToken,
|
||||
)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return Session{}, ErrUnauthenticated
|
||||
}
|
||||
if err != nil {
|
||||
return Session{}, fmt.Errorf("resolving auth session: %w", err)
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (s *Service) Logout(ctx context.Context, token string) error {
|
||||
hash := sha256.Sum256([]byte(token))
|
||||
_, err := s.pool.Exec(ctx, `UPDATE auth_sessions SET revoked_at=now() WHERE token_hash=$1 AND revoked_at IS NULL`, hash[:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("revoking auth session: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func randomToken(size int) (string, error) {
|
||||
data := make([]byte, size)
|
||||
if _, err := rand.Read(data); err != nil {
|
||||
return "", fmt.Errorf("generating secure token: %w", err)
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(data), nil
|
||||
}
|
||||
Reference in New Issue
Block a user