mirror of
https://github.com/teamhanko/hanko.git
synced 2025-11-01 22:28:27 +08:00
feat: init rate limiting. functional on passcode/init
This commit is contained in:
@ -21,6 +21,7 @@ COPY session session/
|
||||
COPY mail mail/
|
||||
COPY audit_log audit_log/
|
||||
COPY pagination pagination/
|
||||
COPY rate_limiter rate_limiter/
|
||||
|
||||
# Build
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=$TARGETARCH go build -a -o hanko main.go
|
||||
|
||||
@ -22,6 +22,7 @@ COPY session session/
|
||||
COPY mail mail/
|
||||
COPY audit_log audit_log/
|
||||
COPY pagination pagination/
|
||||
COPY rate_limiter rate_limiter/
|
||||
|
||||
# Build
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -gcflags="all=-N -l" -a -o hanko main.go
|
||||
|
||||
@ -389,6 +389,7 @@ To persist audit logs in the database, set `audit_log.storage.enabled` to `true`
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
// TODO
|
||||
Currently, Hanko backend does not implement rate limiting in any way. In production systems, you may want to hide the
|
||||
Hanko service behind a proxy or gateway (e.g. Kong, Traefik) that provides rate limiting.
|
||||
|
||||
|
||||
@ -23,8 +23,22 @@ type Config struct {
|
||||
Service Service `yaml:"service" json:"service" koanf:"service"`
|
||||
Session Session `yaml:"session" json:"session" koanf:"session"`
|
||||
AuditLog AuditLog `yaml:"audit_log" json:"audit_log" koanf:"audit_log"`
|
||||
RateLimiter RateLimiter `yaml:"rate_limiter" json:"rate_limiter" koanf:"rate_limiter"`
|
||||
}
|
||||
|
||||
const (
|
||||
// HeaderRateLimitLimit, HeaderRateLimitRemaining, and HeaderRateLimitReset
|
||||
// are the recommended return header values from IETF on rate limiting. Reset
|
||||
// is in UTC time.
|
||||
HeaderRateLimitLimit = "X-RateLimit-Limit"
|
||||
HeaderRateLimitRemaining = "X-RateLimit-Remaining"
|
||||
HeaderRateLimitReset = "X-RateLimit-Reset"
|
||||
|
||||
// HeaderRetryAfter is the header used to indicate when a client should retry
|
||||
// requests (when the rate limit expires), in UTC time.
|
||||
HeaderRetryAfter = "Retry-After"
|
||||
)
|
||||
|
||||
func Load(cfgFile *string) (*Config, error) {
|
||||
k := koanf.New(".")
|
||||
var err error
|
||||
@ -58,6 +72,14 @@ func DefaultConfig() *Config {
|
||||
Server: Server{
|
||||
Public: ServerSettings{
|
||||
Address: ":8000",
|
||||
Cors: Cors{
|
||||
ExposeHeaders: []string{
|
||||
HeaderRateLimitLimit,
|
||||
HeaderRateLimitRemaining,
|
||||
HeaderRateLimitReset,
|
||||
HeaderRetryAfter,
|
||||
},
|
||||
},
|
||||
},
|
||||
Admin: ServerSettings{
|
||||
Address: ":8001",
|
||||
@ -67,7 +89,7 @@ func DefaultConfig() *Config {
|
||||
RelyingParty: RelyingParty{
|
||||
Id: "localhost",
|
||||
DisplayName: "Hanko Authentication Service",
|
||||
Origin: "http://localhost",
|
||||
Origins: []string{"http://localhost"},
|
||||
},
|
||||
Timeout: 60000,
|
||||
},
|
||||
@ -76,6 +98,10 @@ func DefaultConfig() *Config {
|
||||
Port: "465",
|
||||
},
|
||||
TTL: 300,
|
||||
Email: Email{
|
||||
FromAddress: "passcode@hanko.io",
|
||||
FromName: "Hanko",
|
||||
},
|
||||
},
|
||||
Password: Password{
|
||||
MinPasswordLength: 8,
|
||||
@ -97,6 +123,9 @@ func DefaultConfig() *Config {
|
||||
OutputStream: OutputStreamStdOut,
|
||||
},
|
||||
},
|
||||
RateLimiter: RateLimiter{
|
||||
Enabled: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,6 +158,10 @@ func (c *Config) Validate() error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to validate session settings: %w", err)
|
||||
}
|
||||
err = c.RateLimiter.Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to validate rate-limiter settings: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -354,3 +387,42 @@ var (
|
||||
OutputStreamStdOut OutputStream = "stdout"
|
||||
OutputStreamStdErr OutputStream = "stderr"
|
||||
)
|
||||
|
||||
type RateLimiter struct {
|
||||
Enabled bool `yaml:"enabled" json:"enabled" koanf:"enabled"`
|
||||
Backend RateLimiterBackendType `yaml:"backend" json:"backend" koanf:"backend"`
|
||||
Redis *RedisConfig `yaml:"redis_config" json:"redis_config" koanf:"redis_config"`
|
||||
Tokens *uint64 `yaml:"tokens" json:"tokens" koanf:"tokens"`
|
||||
Interval *time.Duration `yaml:"interval" json:"interval" koanf:"interval"`
|
||||
}
|
||||
|
||||
type RateLimiterBackendType string
|
||||
|
||||
const (
|
||||
RATE_LIMITER_BACKEND_IN_MEMORY RateLimiterBackendType = "in_memory"
|
||||
RATE_LIMITER_BACKEND_REDIS = "redis"
|
||||
)
|
||||
|
||||
func (r *RateLimiter) Validate() error {
|
||||
if r.Enabled {
|
||||
switch r.Backend {
|
||||
case RATE_LIMITER_BACKEND_REDIS:
|
||||
if r.Redis == nil {
|
||||
return errors.New("when enabling the redis backend you have to specify the redis config")
|
||||
}
|
||||
case RATE_LIMITER_BACKEND_IN_MEMORY:
|
||||
break
|
||||
default:
|
||||
return errors.New(string(r.Backend) + " is not a valid rate limiter backend.")
|
||||
}
|
||||
if r.Tokens == nil || r.Interval == nil {
|
||||
return errors.New("Please specify tokens and interval")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type RedisConfig struct {
|
||||
Address string `yaml:"address" json:"address" koanf:"address"`
|
||||
Password string `yaml:"password" json:"password" koanf:"password"`
|
||||
}
|
||||
|
||||
76
backend/config/config_test.go
Normal file
76
backend/config/config_test.go
Normal file
@ -0,0 +1,76 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDefaultConfigNotEnoughForValidation(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Error("The default config is missing mandatory parameters. This should not validate without error.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseValidConfig(t *testing.T) {
|
||||
configPath := "./config.yaml"
|
||||
cfg, err := Load(&configPath)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinimalConfigValidates(t *testing.T) {
|
||||
configPath := "./minimal-config.yaml"
|
||||
cfg, err := Load(&configPath)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimiterConfig(t *testing.T) {
|
||||
configPath := "./minimal-config.yaml"
|
||||
cfg, err := Load(&configPath)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
var five uint64 = 5
|
||||
aMinute := 1 * time.Minute
|
||||
cfg.RateLimiter.Enabled = true
|
||||
cfg.RateLimiter.Backend = "in_memory"
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
cfg.RateLimiter.Tokens = &five
|
||||
cfg.RateLimiter.Interval = &aMinute
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
cfg.RateLimiter.Backend = "redis"
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Error("when specifying redis, the redis config should also be specified")
|
||||
}
|
||||
cfg.RateLimiter.Redis = &RedisConfig{
|
||||
Address: "127.0.0.1:9876",
|
||||
Password: "password",
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
cfg.RateLimiter.Backend = "notvalid"
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Error("notvalid is not a valid backend")
|
||||
}
|
||||
}
|
||||
12
backend/config/minimal-config.yaml
Normal file
12
backend/config/minimal-config.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
passcode:
|
||||
smtp:
|
||||
host: smtp.example.com
|
||||
user: example
|
||||
password: example
|
||||
database:
|
||||
url: postgres://postgres:123456@127.0.0.1:5432/dummy
|
||||
secrets:
|
||||
keys:
|
||||
- abcedfghijklmnopqrstuvwxyz
|
||||
service:
|
||||
name: Hanko Authentication Service
|
||||
@ -8,11 +8,14 @@ require (
|
||||
github.com/gobuffalo/pop/v6 v6.1.0
|
||||
github.com/gobuffalo/validate/v3 v3.3.3
|
||||
github.com/gofrs/uuid v4.3.1+incompatible
|
||||
github.com/gomodule/redigo v1.8.2
|
||||
github.com/knadh/koanf v1.4.4
|
||||
github.com/labstack/echo/v4 v4.9.1
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.8
|
||||
github.com/nicksnyder/go-i18n/v2 v2.2.1
|
||||
github.com/rs/zerolog v1.28.0
|
||||
github.com/sethvargo/go-limiter v0.7.2
|
||||
github.com/sethvargo/go-redisstore v0.3.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/stretchr/testify v1.8.1
|
||||
golang.org/x/crypto v0.5.0
|
||||
|
||||
@ -160,6 +160,8 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k=
|
||||
github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
@ -455,6 +457,11 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
|
||||
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sethvargo/go-limiter v0.6.0/go.mod h1:C0kbSFbiriE5k2FFOe18M1YZbAR2Fiwf72uGu0CXCcU=
|
||||
github.com/sethvargo/go-limiter v0.7.2 h1:FgC4N7RMpV5gMrUdda15FaFTkQ/L4fEqM7seXMs4oO8=
|
||||
github.com/sethvargo/go-limiter v0.7.2/go.mod h1:C0kbSFbiriE5k2FFOe18M1YZbAR2Fiwf72uGu0CXCcU=
|
||||
github.com/sethvargo/go-redisstore v0.3.0 h1:yCDGc7ERWfa9BMgjhMhYcH8k+y85bRx0nziupGhjPkc=
|
||||
github.com/sethvargo/go-redisstore v0.3.0/go.mod h1:rY+FgiPpRrdpi4wETGHdMf6YlJnGiziAt2R8gXaFFxg=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"github.com/gobuffalo/pop/v6"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/sethvargo/go-limiter"
|
||||
"github.com/teamhanko/hanko/backend/audit_log"
|
||||
"github.com/teamhanko/hanko/backend/config"
|
||||
"github.com/teamhanko/hanko/backend/crypto"
|
||||
@ -13,6 +14,7 @@ import (
|
||||
"github.com/teamhanko/hanko/backend/mail"
|
||||
"github.com/teamhanko/hanko/backend/persistence"
|
||||
"github.com/teamhanko/hanko/backend/persistence/models"
|
||||
"github.com/teamhanko/hanko/backend/rate_limiter"
|
||||
"github.com/teamhanko/hanko/backend/session"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gopkg.in/gomail.v2"
|
||||
@ -31,6 +33,7 @@ type PasscodeHandler struct {
|
||||
sessionManager session.Manager
|
||||
cfg *config.Config
|
||||
auditLogger auditlog.Logger
|
||||
rateLimiter limiter.Store
|
||||
}
|
||||
|
||||
var maxPasscodeTries = 3
|
||||
@ -40,6 +43,10 @@ func NewPasscodeHandler(cfg *config.Config, persister persistence.Persister, ses
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create new renderer: %w", err)
|
||||
}
|
||||
var rateLimiter limiter.Store
|
||||
if cfg.RateLimiter.Enabled {
|
||||
rateLimiter = rate_limiter.NewRateLimiter(cfg.RateLimiter)
|
||||
}
|
||||
return &PasscodeHandler{
|
||||
mailer: mailer,
|
||||
renderer: renderer,
|
||||
@ -51,6 +58,7 @@ func NewPasscodeHandler(cfg *config.Config, persister persistence.Persister, ses
|
||||
sessionManager: sessionManager,
|
||||
cfg: cfg,
|
||||
auditLogger: auditLogger,
|
||||
rateLimiter: rateLimiter,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -81,6 +89,13 @@ func (h *PasscodeHandler) Init(c echo.Context) error {
|
||||
return dto.NewHTTPError(http.StatusBadRequest).SetInternal(errors.New("user not found"))
|
||||
}
|
||||
|
||||
if h.rateLimiter != nil {
|
||||
err := rate_limiter.Limit(h.rateLimiter, userId, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
passcode, err := h.passcodeGenerator.Generate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate passcode: %w", err)
|
||||
|
||||
75
backend/rate_limiter/rate_limiter.go
Normal file
75
backend/rate_limiter/rate_limiter.go
Normal file
@ -0,0 +1,75 @@
|
||||
package rate_limiter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/gomodule/redigo/redis"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/sethvargo/go-limiter"
|
||||
"github.com/sethvargo/go-limiter/memorystore"
|
||||
"github.com/sethvargo/go-redisstore"
|
||||
"github.com/teamhanko/hanko/backend/config"
|
||||
"github.com/teamhanko/hanko/backend/dto"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewRateLimiter(cfg config.RateLimiter) limiter.Store {
|
||||
if cfg.Backend == config.RATE_LIMITER_BACKEND_REDIS {
|
||||
//ctx := context.Background()
|
||||
store, err := redisstore.New(&redisstore.Config{
|
||||
Tokens: *cfg.Tokens,
|
||||
Interval: *cfg.Interval,
|
||||
Dial: func() (redis.Conn, error) {
|
||||
return redis.Dial("tcp", cfg.Redis.Address,
|
||||
redis.DialPassword(cfg.Redis.Password))
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
//defer store.Close(ctx)
|
||||
return store
|
||||
}
|
||||
// else return in_memory
|
||||
store, err := memorystore.New(&memorystore.Config{
|
||||
// Number of tokens allowed per interval.
|
||||
Tokens: *cfg.Tokens,
|
||||
|
||||
// Interval until tokens reset.
|
||||
Interval: *cfg.Interval,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
func Limit(store limiter.Store, userId uuid.UUID, c echo.Context) error {
|
||||
key := userId.String() + "/" + c.RealIP()
|
||||
// Take from the store.
|
||||
limit, remaining, reset, ok, err := store.Take(context.Background(), key)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//resetTime := time.Unix(0, int64(reset)).UTC().Format(time.RFC1123)
|
||||
resetTime := int(math.Floor(time.Unix(0, int64(reset)).UTC().Sub(time.Now().UTC()).Seconds()))
|
||||
log.Println(resetTime)
|
||||
|
||||
// Set headers (we do this regardless of whether the request is permitted).
|
||||
c.Response().Header().Set(config.HeaderRateLimitLimit, strconv.FormatUint(limit, 10))
|
||||
c.Response().Header().Set(config.HeaderRateLimitRemaining, strconv.FormatUint(remaining, 10))
|
||||
c.Response().Header().Set(config.HeaderRateLimitReset, strconv.Itoa(resetTime))
|
||||
|
||||
// Fail if there were no tokens remaining.
|
||||
if !ok {
|
||||
c.Response().Header().Set(config.HeaderRetryAfter, strconv.Itoa(resetTime))
|
||||
return dto.NewHTTPError(http.StatusTooManyRequests)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
68
backend/rate_limiter/rate_limiter_test.go
Normal file
68
backend/rate_limiter/rate_limiter_test.go
Normal file
@ -0,0 +1,68 @@
|
||||
package rate_limiter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/teamhanko/hanko/backend/config"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewRateLimiter(t *testing.T) {
|
||||
aMinute := 1 * time.Minute
|
||||
var five uint64 = 5
|
||||
cfg := config.RateLimiter{
|
||||
Enabled: true,
|
||||
Backend: config.RATE_LIMITER_BACKEND_IN_MEMORY,
|
||||
Redis: nil,
|
||||
Tokens: &five,
|
||||
Interval: &aMinute,
|
||||
}
|
||||
|
||||
rl := NewRateLimiter(cfg)
|
||||
// Take 5 tokens: should be good.
|
||||
for i := 0; i < 5; i++ {
|
||||
tokens, remaining, reset, ok, e := rl.Take(context.Background(), "some-key")
|
||||
log.Printf("Tokens: %v, Remaining: %v, Reset: %v, ok: %v, error: %v\n", tokens, remaining, time.Unix(0, int64(reset)).String(), ok, e)
|
||||
if e != nil {
|
||||
t.Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
tokens, remaining, reset, ok, e := rl.Take(context.Background(), "some-key")
|
||||
log.Printf("Tokens: %v, Remaining: %v, Reset: %v, ok: %v, error: %v\n", tokens, remaining, time.Unix(0, int64(reset)).String(), ok, e)
|
||||
if ok {
|
||||
t.Error("Taking a token should fail at this point")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRateLimiterRedis(t *testing.T) {
|
||||
aMinute := 1 * time.Minute
|
||||
var five uint64 = 5
|
||||
cfg := config.RateLimiter{
|
||||
Enabled: true,
|
||||
Backend: config.RATE_LIMITER_BACKEND_REDIS,
|
||||
Redis: &config.RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
Password: "",
|
||||
},
|
||||
Tokens: &five,
|
||||
Interval: &aMinute,
|
||||
}
|
||||
|
||||
rl := NewRateLimiter(cfg)
|
||||
// Take 5 tokens: should be good.
|
||||
for i := 0; i < 6; i++ {
|
||||
tokens, remaining, e := rl.Get(context.Background(), "some-key")
|
||||
log.Printf("Tokens: %v, Remaining: %v\n", tokens, remaining)
|
||||
if e != nil {
|
||||
t.Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
// Try to take the sixth token, should faild
|
||||
_, _, e := rl.Get(context.Background(), "some-key")
|
||||
if e == nil {
|
||||
t.Error("Taking a token should fail at this point")
|
||||
}
|
||||
}
|
||||
35
deploy/docker-compose/config-rate-limiting.yaml
Normal file
35
deploy/docker-compose/config-rate-limiting.yaml
Normal file
@ -0,0 +1,35 @@
|
||||
database:
|
||||
user: hanko
|
||||
password: hanko
|
||||
host: postgresd
|
||||
port: 5432
|
||||
dialect: postgres
|
||||
passcode:
|
||||
email:
|
||||
from_address: no-reply@hanko.io
|
||||
smtp:
|
||||
host: "mailslurper"
|
||||
port: "2500"
|
||||
secrets:
|
||||
keys:
|
||||
- abcedfghijklmnopqrstuvwxyz
|
||||
service:
|
||||
name: Hanko Authentication Service
|
||||
server:
|
||||
public:
|
||||
cors:
|
||||
enabled: true
|
||||
allow_credentials: true
|
||||
allow_origins:
|
||||
- "*"
|
||||
webauthn:
|
||||
relying_party:
|
||||
origin: "http://localhost:8888"
|
||||
session:
|
||||
cookie:
|
||||
secure: false # is needed for safari, because safari does not store secure cookies on localhost
|
||||
rate_limiter:
|
||||
enabled: true
|
||||
backend: "in_memory"
|
||||
tokens: 2
|
||||
interval: 1m
|
||||
80
deploy/docker-compose/quickstart-with-redis.yaml
Normal file
80
deploy/docker-compose/quickstart-with-redis.yaml
Normal file
@ -0,0 +1,80 @@
|
||||
services:
|
||||
hanko-migrate:
|
||||
build: ../../backend
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ./config.yaml
|
||||
target: /etc/config/config.yaml
|
||||
command: --config /etc/config/config.yaml migrate up
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
postgresd:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- intranet
|
||||
hanko:
|
||||
depends_on:
|
||||
hanko-migrate:
|
||||
condition: service_completed_successfully
|
||||
build: ../../backend
|
||||
ports:
|
||||
- '8000:8000' # public
|
||||
- '8001:8001' # admin
|
||||
restart: unless-stopped
|
||||
command: serve --config /etc/config/config.yaml all
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ./config-rate-limiting.yaml
|
||||
target: /etc/config/config.yaml
|
||||
networks:
|
||||
- intranet
|
||||
environment:
|
||||
- PASSWORD_ENABLED
|
||||
postgresd:
|
||||
image: postgres:12-alpine
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
- POSTGRES_USER=hanko
|
||||
- POSTGRES_PASSWORD=hanko
|
||||
- POSTGRES_DB=hanko
|
||||
healthcheck:
|
||||
test: pg_isready -U hanko -d hanko
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
networks:
|
||||
- intranet
|
||||
elements:
|
||||
build: ../../frontend
|
||||
ports:
|
||||
- "9500:80"
|
||||
networks:
|
||||
- intranet
|
||||
quickstart:
|
||||
build: ../../quickstart
|
||||
ports:
|
||||
- "8888:8080"
|
||||
environment:
|
||||
- HANKO_URL=http://localhost:8000
|
||||
- HANKO_URL_INTERNAL=http://hanko:8000
|
||||
- HANKO_ELEMENT_URL=http://localhost:9500/element.hanko-auth.js
|
||||
- HANKO_FRONTEND_SDK_URL=http://localhost:9500/sdk.umd.js
|
||||
networks:
|
||||
- intranet
|
||||
mailslurper:
|
||||
image: marcopas/docker-mailslurper:latest
|
||||
ports:
|
||||
- '8080:8080' # web UI
|
||||
- '8085:8085'
|
||||
networks:
|
||||
- intranet
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
networks:
|
||||
- intranet
|
||||
networks:
|
||||
intranet:
|
||||
@ -46,7 +46,7 @@ class PasscodeClient extends Client {
|
||||
|
||||
if (response.status === 429) {
|
||||
const retryAfter = parseInt(
|
||||
response.headers.get("X-Retry-After") || "0",
|
||||
response.headers.get("Retry-After") || "0",
|
||||
10
|
||||
);
|
||||
|
||||
|
||||
@ -48,7 +48,7 @@ class PasswordClient extends Client {
|
||||
throw new InvalidPasswordError();
|
||||
} else if (response.status === 429) {
|
||||
const retryAfter = parseInt(
|
||||
response.headers.get("X-Retry-After") || "0",
|
||||
response.headers.get("Retry-After") || "0",
|
||||
10
|
||||
);
|
||||
|
||||
|
||||
@ -74,7 +74,7 @@ describe("PasscodeClient.initialize()", () => {
|
||||
passcodeRetryAfter
|
||||
);
|
||||
expect(passcodeClient.state.write).toHaveBeenCalledTimes(1);
|
||||
expect(response.headers.get).toHaveBeenCalledWith("X-Retry-After");
|
||||
expect(response.headers.get).toHaveBeenCalledWith("Retry-After");
|
||||
});
|
||||
|
||||
it("should throw error when API response is not ok", async () => {
|
||||
|
||||
@ -63,7 +63,7 @@ describe("PasswordClient.login()", () => {
|
||||
passwordRetryAfter
|
||||
);
|
||||
expect(passwordClient.state.write).toHaveBeenCalledTimes(1);
|
||||
expect(response.headers.get).toHaveBeenCalledWith("X-Retry-After");
|
||||
expect(response.headers.get).toHaveBeenCalledWith("Retry-After");
|
||||
});
|
||||
|
||||
it("should throw error when API response is not ok", async () => {
|
||||
|
||||
Reference in New Issue
Block a user