Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat basic rate limiting #501

Merged
merged 29 commits into from
Jan 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a914602
Merge branch 'main' into feat-basic-rate-limiting
like-a-bause Jan 17, 2023
79c07ec
feat: init rate limiting. functional on passcode/init
like-a-bause Jan 20, 2023
fb78016
feat: add basic rate limiting in password login
like-a-bause Jan 23, 2023
f9f1aae
test: fix the RateLimiterConfig test.
like-a-bause Jan 23, 2023
8741c50
test: fix test for real this time.
like-a-bause Jan 23, 2023
2ecb9f3
Merge branch 'main' into feat-basic-rate-limiting
like-a-bause Jan 23, 2023
3dedce7
feat: make limits for passcode/password separately configurable. Docu…
like-a-bause Jan 23, 2023
c6c3932
Update backend/config/config.go
like-a-bause Jan 24, 2023
a10e7ec
Update backend/docs/Config.md
like-a-bause Jan 24, 2023
283cfda
Update backend/docs/Config.md
like-a-bause Jan 24, 2023
188c24e
Update backend/rate_limiter/rate_limiter.go
like-a-bause Jan 24, 2023
5409250
Update backend/rate_limiter/rate_limiter.go
like-a-bause Jan 24, 2023
4ef06d2
Update backend/rate_limiter/rate_limiter.go
like-a-bause Jan 24, 2023
5483494
Update backend/docs/Config.md
like-a-bause Jan 24, 2023
f9fdcba
fix: use httplimit defined headers
like-a-bause Jan 24, 2023
2eee359
fix: rename backend -> store
like-a-bause Jan 24, 2023
1384661
fix: require redis.address when enabling redis
like-a-bause Jan 24, 2023
abeacd4
fix: rate_limiter_test.go
like-a-bause Jan 24, 2023
f57443e
fix: use echo context.Path as key prefix for rate limiter. use redis …
like-a-bause Jan 24, 2023
9403f40
Update backend/docs/Config.md
like-a-bause Jan 24, 2023
7591d00
Update backend/docs/Config.md
like-a-bause Jan 24, 2023
74baba7
Update backend/docs/Config.md
like-a-bause Jan 24, 2023
f5adfed
Merge branch 'main' into feat-basic-rate-limiting
like-a-bause Jan 25, 2023
c96f95c
fix: rename the Retry After Header
like-a-bause Jan 25, 2023
081553b
docs: adjust the Readme
like-a-bause Jan 25, 2023
a1673f7
fix: fix quickstart with redis compose file
like-a-bause Jan 25, 2023
dd37d30
fix: the password retry counter on button now counts down.
like-a-bause Jan 25, 2023
ddda0f6
Merge branch 'main' into feat-basic-rate-limiting
like-a-bause Jan 25, 2023
75eb675
fix: fix merge conflicts
like-a-bause Jan 25, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 22 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,28 +51,28 @@ If you want to use the Hanko backend API but prefer to build your own UI, you ca
We are currently in **Beta** and may still have critical bugs. Watch our releases, leave a star, join our [Slack community](https://www.hanko.io/community), or sign up to our [product news](https://www.hanko.io/updates) to follow the development. Here's a brief overview of the current roadmap:

| Status | Feature |
| :---: | :--- |
| | Passkeys |
| | Email passcodes |
| | Passwords |
| | JWT signing |
| | User management API |
| | 📢 Hanko Alpha Release |
| | `<hanko-auth>` web component |
| | Customizable CSS |
| | 📢 Hanko Beta Release |
| | JavaScript frontend SDK |
| | Passkey autofill ([Conditional UI](https://github.com/w3c/webauthn/wiki/Explainer:-WebAuthn-Conditional-UI)) |
| | Audit logs API |
| | Security Key support |
| | Mobile app support |
| ⚙️ | `<hanko-profile>` web component |
| ⚙️ | Priviledged sessions & step-up authentication |
| ⚙️ | OAuth plugin system (Sign in with Google/Apple/GitHub/...) |
| | Rate limiting (application level) |
| | Session management |
| | Custom translations for [hanko-elements](/frontend/elements/README.md) |
| | Email templating |
|:------:| :--- |
| | Passkeys |
| | Email passcodes |
| | Passwords |
| | JWT signing |
| | User management API |
| | 📢 Hanko Alpha Release |
| | `<hanko-auth>` web component |
| | Customizable CSS |
| | 📢 Hanko Beta Release |
| | JavaScript frontend SDK |
| | Passkey autofill ([Conditional UI](https://github.com/w3c/webauthn/wiki/Explainer:-WebAuthn-Conditional-UI)) |
| | Audit logs API |
| | Security Key support |
| | Mobile app support |
| | `<hanko-profile>` web component |
| ✅ | Rate limiting (application level) |
| ⚙️ | Priviledged sessions & step-up authentication |
| ⚙️ | OAuth plugin system (Sign in with Google/Apple/GitHub/...) |
| | Session management |
| | Custom translations for [hanko-elements](/frontend/elements/README.md) |
| | Email templating |

Additional features that have been requested or that we would like to build but are not (yet) on the roadmap:
- SMS passcode delivery
Expand Down
1 change: 1 addition & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions backend/Dockerfile.debug
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,8 +389,11 @@ To persist audit logs in the database, set `audit_log.storage.enabled` to `true`

### Rate Limiting

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 implements basic fixed-window rate limiting for the passcode/init and password/login endpoints to mitigate brute-force attacks.
It uses a combination of user-id/IP to mitigate DoS attacks on user accounts. You can choose between an in-memory and a redis store.

In production systems, you may want to hide the
Hanko service behind a proxy or gateway (e.g. Kong, Traefik) to provide additional network-based rate limiting.

## API specification

Expand Down
97 changes: 86 additions & 11 deletions backend/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,25 @@ import (
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/file"
"github.com/sethvargo/go-limiter/httplimit"
"log"
"strings"
"time"
)

// Config is the central configuration type
type Config struct {
Server Server `yaml:"server" json:"server" koanf:"server"`
Webauthn WebauthnSettings `yaml:"webauthn" json:"webauthn" koanf:"webauthn"`
Passcode Passcode `yaml:"passcode" json:"passcode" koanf:"passcode"`
Password Password `yaml:"password" json:"password" koanf:"password"`
Database Database `yaml:"database" json:"database" koanf:"database"`
Secrets Secrets `yaml:"secrets" json:"secrets" koanf:"secrets"`
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"`
Emails Emails `yaml:"emails" json:"emails" koanf:"emails"`
Server Server `yaml:"server" json:"server" koanf:"server"`
Webauthn WebauthnSettings `yaml:"webauthn" json:"webauthn" koanf:"webauthn"`
Passcode Passcode `yaml:"passcode" json:"passcode" koanf:"passcode"`
Password Password `yaml:"password" json:"password" koanf:"password"`
Database Database `yaml:"database" json:"database" koanf:"database"`
Secrets Secrets `yaml:"secrets" json:"secrets" koanf:"secrets"`
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"`
Emails Emails `yaml:"emails" json:"emails" koanf:"emails"`
RateLimiter RateLimiter `yaml:"rate_limiter" json:"rate_limiter" koanf:"rate_limiter"`
}

func Load(cfgFile *string) (*Config, error) {
Expand Down Expand Up @@ -59,6 +61,14 @@ func DefaultConfig() *Config {
Server: Server{
Public: ServerSettings{
Address: ":8000",
Cors: Cors{
ExposeHeaders: []string{
httplimit.HeaderRateLimitLimit,
httplimit.HeaderRateLimitRemaining,
httplimit.HeaderRateLimitReset,
httplimit.HeaderRetryAfter,
},
},
},
Admin: ServerSettings{
Address: ":8001",
Expand All @@ -68,7 +78,7 @@ func DefaultConfig() *Config {
RelyingParty: RelyingParty{
Id: "localhost",
DisplayName: "Hanko Authentication Service",
Origin: "http:https://localhost",
Origins: []string{"http:https://localhost"},
},
Timeout: 60000,
},
Expand All @@ -77,6 +87,10 @@ func DefaultConfig() *Config {
Port: "465",
},
TTL: 300,
Email: Email{
FromAddress: "[email protected]",
FromName: "Hanko",
},
},
Password: Password{
MinPasswordLength: 8,
Expand All @@ -102,6 +116,18 @@ func DefaultConfig() *Config {
RequireVerification: true,
MaxNumOfAddresses: 5,
},
RateLimiter: RateLimiter{
Enabled: true,
Store: RATE_LIMITER_STORE_IN_MEMORY,
PasswordLimits: RateLimits{
Tokens: 5,
Interval: 1 * time.Minute,
},
PasscodeLimits: RateLimits{
Tokens: 3,
Interval: 1 * time.Minute,
},
},
}
}

Expand Down Expand Up @@ -134,6 +160,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
}

Expand Down Expand Up @@ -364,3 +394,48 @@ var (
OutputStreamStdOut OutputStream = "stdout"
OutputStreamStdErr OutputStream = "stderr"
)

type RateLimiter struct {
Enabled bool `yaml:"enabled" json:"enabled" koanf:"enabled"`
Store RateLimiterStoreType `yaml:"store" json:"store" koanf:"store"`
Redis *RedisConfig `yaml:"redis_config" json:"redis_config" koanf:"redis_config"`
PasscodeLimits RateLimits `yaml:"passcode_limits" json:"passcode_limits" koanf:"passcode_limits"`
PasswordLimits RateLimits `yaml:"password_limits" json:"password_limits" koanf:"password_limits"`
}

type RateLimits struct {
Tokens uint64 `yaml:"tokens" json:"tokens" koanf:"tokens"`
Interval time.Duration `yaml:"interval" json:"interval" koanf:"interval"`
}

type RateLimiterStoreType string

const (
RATE_LIMITER_STORE_IN_MEMORY RateLimiterStoreType = "in_memory"
RATE_LIMITER_STORE_REDIS = "redis"
)

func (r *RateLimiter) Validate() error {
if r.Enabled {
switch r.Store {
case RATE_LIMITER_STORE_REDIS:
if r.Redis == nil {
return errors.New("when enabling the redis store you have to specify the redis config")
}
if r.Redis.Address == "" {
return errors.New("when enabling the redis store you have to specify the address where hanko can reach the redis instance")
}
case RATE_LIMITER_STORE_IN_MEMORY:
break
default:
return errors.New(string(r.Store) + " is not a valid rate limiter store.")
}
}
return nil
}

type RedisConfig struct {
//Address of redis in the form of host[:port][/database]
Address string `yaml:"address" json:"address" koanf:"address"`
Password string `yaml:"password" json:"password" koanf:"password"`
}
66 changes: 66 additions & 0 deletions backend/config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package config

import (
"testing"
)

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)
}
cfg.RateLimiter.Enabled = true
cfg.RateLimiter.Store = "in_memory"

if err := cfg.Validate(); err != nil {
t.Error(err)
}

cfg.RateLimiter.Store = "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.Store = "notvalid"
if err := cfg.Validate(); err == nil {
t.Error("notvalid is not a valid backend")
}
}
12 changes: 12 additions & 0 deletions backend/config/minimal-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
passcode:
smtp:
host: smtp.example.com
user: example
password: example
database:
url: postgres:https://postgres:[email protected]:5432/dummy
secrets:
keys:
- abcedfghijklmnopqrstuvwxyz
service:
name: Hanko Authentication Service
Loading