feat: seed secure demo merchant identity
This commit is contained in:
40
cmd/admin/main.go
Normal file
40
cmd/admin/main.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.amiirkhl.ir/starcoach-hackathon/zarinpal-analytics-backend/internal/config"
|
||||||
|
"git.amiirkhl.ir/starcoach-hackathon/zarinpal-analytics-backend/internal/database"
|
||||||
|
"git.amiirkhl.ir/starcoach-hackathon/zarinpal-analytics-backend/internal/identity"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("configuration failed", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := cfg.ValidateSeed(); err != nil {
|
||||||
|
logger.Error("seed configuration failed", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
pool, err := database.Open(ctx, cfg.DatabaseURL)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("database connection failed", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer pool.Close()
|
||||||
|
result, err := identity.SeedDemo(ctx, pool, cfg.DemoUsername, cfg.DemoPassword)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("demo account seed failed", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
logger.Info("demo account ready", "username", result.Username, "merchant_source_key", result.MerchantSourceKey,
|
||||||
|
"merchant_name", result.MerchantName, "password_source", "DEMO_PASSWORD")
|
||||||
|
}
|
||||||
10
go.mod
10
go.mod
@@ -2,13 +2,19 @@ module git.amiirkhl.ir/starcoach-hackathon/zarinpal-analytics-backend
|
|||||||
|
|
||||||
go 1.24.0
|
go 1.24.0
|
||||||
|
|
||||||
require github.com/jackc/pgx/v5 v5.7.5
|
require (
|
||||||
|
github.com/jackc/pgx/v5 v5.7.5
|
||||||
|
github.com/redis/go-redis/v9 v9.22.0
|
||||||
|
golang.org/x/crypto v0.41.0
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
golang.org/x/crypto v0.41.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
golang.org/x/sync v0.16.0 // indirect
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
golang.org/x/text v0.28.0 // indirect
|
golang.org/x/text v0.28.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
16
go.sum
16
go.sum
@@ -1,3 +1,9 @@
|
|||||||
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
|
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -9,17 +15,27 @@ github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
|||||||
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0=
|
||||||
|
github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||||
|
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||||
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
56
internal/identity/password.go
Normal file
56
internal/identity/password.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package identity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/argon2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
argonMemory = 64 * 1024
|
||||||
|
argonIterations = 3
|
||||||
|
argonParallelism = 2
|
||||||
|
argonSaltLength = 16
|
||||||
|
argonKeyLength = 32
|
||||||
|
)
|
||||||
|
|
||||||
|
func HashPassword(password string) (string, error) {
|
||||||
|
if len(password) < 12 {
|
||||||
|
return "", errors.New("identity: password must contain at least 12 characters")
|
||||||
|
}
|
||||||
|
salt := make([]byte, argonSaltLength)
|
||||||
|
if _, err := rand.Read(salt); err != nil {
|
||||||
|
return "", fmt.Errorf("generating password salt: %w", err)
|
||||||
|
}
|
||||||
|
hash := argon2.IDKey([]byte(password), salt, argonIterations, argonMemory, argonParallelism, argonKeyLength)
|
||||||
|
return fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s", argonMemory, argonIterations,
|
||||||
|
argonParallelism, base64.RawStdEncoding.EncodeToString(salt), base64.RawStdEncoding.EncodeToString(hash)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func VerifyPassword(password, encoded string) bool {
|
||||||
|
parts := strings.Split(encoded, "$")
|
||||||
|
if len(parts) != 6 || parts[1] != "argon2id" || parts[2] != "v=19" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var memory uint32
|
||||||
|
var iterations uint32
|
||||||
|
var parallelism uint8
|
||||||
|
if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &iterations, ¶llelism); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
expected, err := base64.RawStdEncoding.DecodeString(parts[5])
|
||||||
|
if err != nil || len(expected) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
actual := argon2.IDKey([]byte(password), salt, iterations, memory, parallelism, uint32(len(expected)))
|
||||||
|
return subtle.ConstantTimeCompare(actual, expected) == 1
|
||||||
|
}
|
||||||
24
internal/identity/password_test.go
Normal file
24
internal/identity/password_test.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package identity
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestHashPasswordAndVerifyPassword(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
hash, err := HashPassword("correct horse battery staple")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !VerifyPassword("correct horse battery staple", hash) {
|
||||||
|
t.Fatal("VerifyPassword() = false, want true")
|
||||||
|
}
|
||||||
|
if VerifyPassword("incorrect password", hash) {
|
||||||
|
t.Fatal("VerifyPassword() = true for incorrect password")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHashPasswordRejectsShortPassword(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if _, err := HashPassword("short"); err == nil {
|
||||||
|
t.Fatal("HashPassword() error = nil, want length error")
|
||||||
|
}
|
||||||
|
}
|
||||||
39
internal/identity/seed.go
Normal file
39
internal/identity/seed.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package identity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SeedResult struct {
|
||||||
|
Username string
|
||||||
|
MerchantSourceKey string
|
||||||
|
MerchantName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func SeedDemo(ctx context.Context, pool *pgxpool.Pool, username, password string) (SeedResult, error) {
|
||||||
|
hash, err := HashPassword(password)
|
||||||
|
if err != nil {
|
||||||
|
return SeedResult{}, err
|
||||||
|
}
|
||||||
|
var result SeedResult
|
||||||
|
var merchantID int64
|
||||||
|
err = pool.QueryRow(ctx, `SELECT m.id,m.source_key,m.display_name
|
||||||
|
FROM merchants m JOIN payment_sessions ps ON ps.merchant_id=m.id
|
||||||
|
GROUP BY m.id ORDER BY count(*) DESC,m.source_key LIMIT 1`).Scan(
|
||||||
|
&merchantID, &result.MerchantSourceKey, &result.MerchantName,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return SeedResult{}, fmt.Errorf("selecting demo merchant: %w", err)
|
||||||
|
}
|
||||||
|
_, err = pool.Exec(ctx, `INSERT INTO merchant_users(merchant_id,username,password_hash)
|
||||||
|
VALUES ($1,$2,$3) ON CONFLICT (username) DO UPDATE
|
||||||
|
SET merchant_id=excluded.merchant_id,password_hash=excluded.password_hash,is_active=true`, merchantID, username, hash)
|
||||||
|
if err != nil {
|
||||||
|
return SeedResult{}, fmt.Errorf("seeding demo merchant user: %w", err)
|
||||||
|
}
|
||||||
|
result.Username = username
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
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