feat: init rate limiting. functional on passcode/init

This commit is contained in:
Felix Dubrownik
2023-01-20 19:02:36 +01:00
parent a914602b5f
commit 79c07ec0b5
17 changed files with 460 additions and 14 deletions

View File

@ -21,6 +21,7 @@ COPY session session/
COPY mail mail/ COPY mail mail/
COPY audit_log audit_log/ COPY audit_log audit_log/
COPY pagination pagination/ COPY pagination pagination/
COPY rate_limiter rate_limiter/
# Build # Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=$TARGETARCH go build -a -o hanko main.go RUN CGO_ENABLED=0 GOOS=linux GOARCH=$TARGETARCH go build -a -o hanko main.go

View File

@ -22,6 +22,7 @@ COPY session session/
COPY mail mail/ COPY mail mail/
COPY audit_log audit_log/ COPY audit_log audit_log/
COPY pagination pagination/ COPY pagination pagination/
COPY rate_limiter rate_limiter/
# Build # Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -gcflags="all=-N -l" -a -o hanko main.go RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -gcflags="all=-N -l" -a -o hanko main.go

View File

@ -389,6 +389,7 @@ To persist audit logs in the database, set `audit_log.storage.enabled` to `true`
### Rate Limiting ### Rate Limiting
// TODO
Currently, Hanko backend does not implement rate limiting in any way. In production systems, you may want to hide the 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. Hanko service behind a proxy or gateway (e.g. Kong, Traefik) that provides rate limiting.

View File

@ -14,17 +14,31 @@ import (
// Config is the central configuration type // Config is the central configuration type
type Config struct { type Config struct {
Server Server `yaml:"server" json:"server" koanf:"server"` Server Server `yaml:"server" json:"server" koanf:"server"`
Webauthn WebauthnSettings `yaml:"webauthn" json:"webauthn" koanf:"webauthn"` Webauthn WebauthnSettings `yaml:"webauthn" json:"webauthn" koanf:"webauthn"`
Passcode Passcode `yaml:"passcode" json:"passcode" koanf:"passcode"` Passcode Passcode `yaml:"passcode" json:"passcode" koanf:"passcode"`
Password Password `yaml:"password" json:"password" koanf:"password"` Password Password `yaml:"password" json:"password" koanf:"password"`
Database Database `yaml:"database" json:"database" koanf:"database"` Database Database `yaml:"database" json:"database" koanf:"database"`
Secrets Secrets `yaml:"secrets" json:"secrets" koanf:"secrets"` Secrets Secrets `yaml:"secrets" json:"secrets" koanf:"secrets"`
Service Service `yaml:"service" json:"service" koanf:"service"` Service Service `yaml:"service" json:"service" koanf:"service"`
Session Session `yaml:"session" json:"session" koanf:"session"` Session Session `yaml:"session" json:"session" koanf:"session"`
AuditLog AuditLog `yaml:"audit_log" json:"audit_log" koanf:"audit_log"` 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) { func Load(cfgFile *string) (*Config, error) {
k := koanf.New(".") k := koanf.New(".")
var err error var err error
@ -58,6 +72,14 @@ func DefaultConfig() *Config {
Server: Server{ Server: Server{
Public: ServerSettings{ Public: ServerSettings{
Address: ":8000", Address: ":8000",
Cors: Cors{
ExposeHeaders: []string{
HeaderRateLimitLimit,
HeaderRateLimitRemaining,
HeaderRateLimitReset,
HeaderRetryAfter,
},
},
}, },
Admin: ServerSettings{ Admin: ServerSettings{
Address: ":8001", Address: ":8001",
@ -67,7 +89,7 @@ func DefaultConfig() *Config {
RelyingParty: RelyingParty{ RelyingParty: RelyingParty{
Id: "localhost", Id: "localhost",
DisplayName: "Hanko Authentication Service", DisplayName: "Hanko Authentication Service",
Origin: "http://localhost", Origins: []string{"http://localhost"},
}, },
Timeout: 60000, Timeout: 60000,
}, },
@ -76,6 +98,10 @@ func DefaultConfig() *Config {
Port: "465", Port: "465",
}, },
TTL: 300, TTL: 300,
Email: Email{
FromAddress: "passcode@hanko.io",
FromName: "Hanko",
},
}, },
Password: Password{ Password: Password{
MinPasswordLength: 8, MinPasswordLength: 8,
@ -97,6 +123,9 @@ func DefaultConfig() *Config {
OutputStream: OutputStreamStdOut, OutputStream: OutputStreamStdOut,
}, },
}, },
RateLimiter: RateLimiter{
Enabled: false,
},
} }
} }
@ -129,6 +158,10 @@ func (c *Config) Validate() error {
if err != nil { if err != nil {
return fmt.Errorf("failed to validate session settings: %w", err) 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 return nil
} }
@ -354,3 +387,42 @@ var (
OutputStreamStdOut OutputStream = "stdout" OutputStreamStdOut OutputStream = "stdout"
OutputStreamStdErr OutputStream = "stderr" 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"`
}

View 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")
}
}

View 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

View File

@ -8,11 +8,14 @@ require (
github.com/gobuffalo/pop/v6 v6.1.0 github.com/gobuffalo/pop/v6 v6.1.0
github.com/gobuffalo/validate/v3 v3.3.3 github.com/gobuffalo/validate/v3 v3.3.3
github.com/gofrs/uuid v4.3.1+incompatible github.com/gofrs/uuid v4.3.1+incompatible
github.com/gomodule/redigo v1.8.2
github.com/knadh/koanf v1.4.4 github.com/knadh/koanf v1.4.4
github.com/labstack/echo/v4 v4.9.1 github.com/labstack/echo/v4 v4.9.1
github.com/lestrrat-go/jwx/v2 v2.0.8 github.com/lestrrat-go/jwx/v2 v2.0.8
github.com/nicksnyder/go-i18n/v2 v2.2.1 github.com/nicksnyder/go-i18n/v2 v2.2.1
github.com/rs/zerolog v1.28.0 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/spf13/cobra v1.6.1
github.com/stretchr/testify v1.8.1 github.com/stretchr/testify v1.8.1
golang.org/x/crypto v0.5.0 golang.org/x/crypto v0.5.0

View File

@ -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.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 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/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 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/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 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/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 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 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 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 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=

View File

@ -6,6 +6,7 @@ import (
"github.com/gobuffalo/pop/v6" "github.com/gobuffalo/pop/v6"
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/sethvargo/go-limiter"
"github.com/teamhanko/hanko/backend/audit_log" "github.com/teamhanko/hanko/backend/audit_log"
"github.com/teamhanko/hanko/backend/config" "github.com/teamhanko/hanko/backend/config"
"github.com/teamhanko/hanko/backend/crypto" "github.com/teamhanko/hanko/backend/crypto"
@ -13,6 +14,7 @@ import (
"github.com/teamhanko/hanko/backend/mail" "github.com/teamhanko/hanko/backend/mail"
"github.com/teamhanko/hanko/backend/persistence" "github.com/teamhanko/hanko/backend/persistence"
"github.com/teamhanko/hanko/backend/persistence/models" "github.com/teamhanko/hanko/backend/persistence/models"
"github.com/teamhanko/hanko/backend/rate_limiter"
"github.com/teamhanko/hanko/backend/session" "github.com/teamhanko/hanko/backend/session"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"gopkg.in/gomail.v2" "gopkg.in/gomail.v2"
@ -31,6 +33,7 @@ type PasscodeHandler struct {
sessionManager session.Manager sessionManager session.Manager
cfg *config.Config cfg *config.Config
auditLogger auditlog.Logger auditLogger auditlog.Logger
rateLimiter limiter.Store
} }
var maxPasscodeTries = 3 var maxPasscodeTries = 3
@ -40,6 +43,10 @@ func NewPasscodeHandler(cfg *config.Config, persister persistence.Persister, ses
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create new renderer: %w", err) 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{ return &PasscodeHandler{
mailer: mailer, mailer: mailer,
renderer: renderer, renderer: renderer,
@ -51,6 +58,7 @@ func NewPasscodeHandler(cfg *config.Config, persister persistence.Persister, ses
sessionManager: sessionManager, sessionManager: sessionManager,
cfg: cfg, cfg: cfg,
auditLogger: auditLogger, auditLogger: auditLogger,
rateLimiter: rateLimiter,
}, nil }, nil
} }
@ -81,6 +89,13 @@ func (h *PasscodeHandler) Init(c echo.Context) error {
return dto.NewHTTPError(http.StatusBadRequest).SetInternal(errors.New("user not found")) 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() passcode, err := h.passcodeGenerator.Generate()
if err != nil { if err != nil {
return fmt.Errorf("failed to generate passcode: %w", err) return fmt.Errorf("failed to generate passcode: %w", err)

View 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
}

View 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")
}
}

View 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

View 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:

View File

@ -46,7 +46,7 @@ class PasscodeClient extends Client {
if (response.status === 429) { if (response.status === 429) {
const retryAfter = parseInt( const retryAfter = parseInt(
response.headers.get("X-Retry-After") || "0", response.headers.get("Retry-After") || "0",
10 10
); );

View File

@ -48,7 +48,7 @@ class PasswordClient extends Client {
throw new InvalidPasswordError(); throw new InvalidPasswordError();
} else if (response.status === 429) { } else if (response.status === 429) {
const retryAfter = parseInt( const retryAfter = parseInt(
response.headers.get("X-Retry-After") || "0", response.headers.get("Retry-After") || "0",
10 10
); );

View File

@ -74,7 +74,7 @@ describe("PasscodeClient.initialize()", () => {
passcodeRetryAfter passcodeRetryAfter
); );
expect(passcodeClient.state.write).toHaveBeenCalledTimes(1); 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 () => { it("should throw error when API response is not ok", async () => {

View File

@ -63,7 +63,7 @@ describe("PasswordClient.login()", () => {
passwordRetryAfter passwordRetryAfter
); );
expect(passwordClient.state.write).toHaveBeenCalledTimes(1); 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 () => { it("should throw error when API response is not ok", async () => {