Skip to content

Commit

Permalink
feat: add haveibeenpwned.org password strength check (#1324)
Browse files Browse the repository at this point in the history
Uses Supabase's HIBP Go library to perform password strength checks
using the HaveIBeenPwned.org Pwned Passwords API.

You can configure this behavior by:

- `GOTRUE_PASSWORD_HIBP_ENABLED` to turn it on
- `GOTRUE_PASSWORD_HIBP_USER_AGENT` to specify your project's identifier
- `GOTRUE_PASSWORD_HIBP_FAIL_CLOSED` if the API is unavailable (or
unresponsive for 5 seconds) the response is ignored and any password is
accepted, set this to true to fail with a 500 error in such cases
- `GOTRUE_PASSWORD_HIBP_BLOOM_ENABLED` to enable a bloom filter cache
- `GOTRUE_PASSWORD_HIBP_BLOOM_ITEMS` to specify the maximum number of
pwned password hashes to be stored in the bloom filter
- `GOTRUE_PASSWORD_HIBP_BLOOM_FALSE_POSITIVES` to specify the maximum
number of false positives returned by the bloom filter, a value between
0 and 1 indicating _1 in X_

For bloom filters, use this calculator to understand the values:
https://hur.st/bloomfilter

By default 100,000 password hashes can be stored in the filter (about
100 hash prefixes). The filter resets at 80% of this value to ensure
that the cache is cleared and the actual false positive rate does not go
too high.
  • Loading branch information
hf authored Nov 30, 2023
1 parent 80172a1 commit c3acfe7
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 0 deletions.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ require (
)

require (
github.com/bits-and-blooms/bitset v1.10.0 // indirect
github.com/bits-and-blooms/bloom/v3 v3.6.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
github.com/gobuffalo/nulls v0.4.2 // indirect
github.com/supabase/hibp v0.0.0-20231124125943-d225752ae869 // indirect
)

require (
Expand Down
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.10.0 h1:ePXTeiPEazB5+opbv5fr8umg2R/1NlzgDsyepwsSr88=
github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bloom/v3 v3.6.0 h1:dTU0OVLJSoOhz9m68FTXMFfA39nR8U/nTCs1zb26mOI=
github.com/bits-and-blooms/bloom/v3 v3.6.0/go.mod h1:VKlUSvp0lFIYqxJjzdnSsZEw4iHb1kOL2tfHTgyJBHg=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/bombsimon/logrusr/v3 v3.0.0 h1:tcAoLfuAhKP9npBxWzSdpsvKPQt1XV02nSf2lZA82TQ=
github.com/bombsimon/logrusr/v3 v3.0.0/go.mod h1:PksPPgSFEL2I52pla2glgCyyd2OqOHAnFF5E+g8Ixco=
Expand Down Expand Up @@ -483,8 +487,11 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/supabase/hibp v0.0.0-20231124125943-d225752ae869 h1:VDuRtwen5Z7QQ5ctuHUse4wAv/JozkKZkdic5vUV4Lg=
github.com/supabase/hibp v0.0.0-20231124125943-d225752ae869/go.mod h1:eHX5nlSMSnyPjUrbYzeqrA8snCe2SKyfizKjU3dkfOw=
github.com/supabase/mailme v0.0.0-20230628061017-01f68480c747 h1:FIUdLV4o5JLsJno4Poum157kAxDKINeJo6liBfauLrI=
github.com/supabase/mailme v0.0.0-20230628061017-01f68480c747/go.mod h1:kWsnmPfUBZTavlXYkfJrE9unzmmRAIi/kqsxXfEWEY8=
github.com/twmb/murmur3 v1.1.6/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
Expand Down
24 changes: 24 additions & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
"github.com/supabase/gotrue/internal/models"
"github.com/supabase/gotrue/internal/observability"
"github.com/supabase/gotrue/internal/storage"
"github.com/supabase/gotrue/internal/utilities"
"github.com/supabase/hibp"
)

const (
Expand All @@ -33,6 +35,8 @@ type API struct {
config *conf.GlobalConfiguration
version string

hibpClient *hibp.PwnedClient

// overrideTime can be used to override the clock used by handlers. Should only be used in tests!
overrideTime func() time.Time
}
Expand Down Expand Up @@ -68,6 +72,26 @@ func (a *API) deprecationNotices(ctx context.Context) {
func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfiguration, db *storage.Connection, version string) *API {
api := &API{config: globalConfig, db: db, version: version}

if api.config.Password.HIBP.Enabled {
httpClient := &http.Client{
// all HIBP API requests should finish quickly to avoid
// unnecessary slowdowns
Timeout: 5 * time.Second,
}

api.hibpClient = &hibp.PwnedClient{
UserAgent: api.config.Password.HIBP.UserAgent,
HTTP: httpClient,
}

if api.config.Password.HIBP.Bloom.Enabled {
cache := utilities.NewHIBPBloomCache(api.config.Password.HIBP.Bloom.Items, api.config.Password.HIBP.Bloom.FalsePositives)
api.hibpClient.Cache = cache

logrus.Infof("Pwned passwords cache is %.2f KB", float64(cache.Cap())/(8*1024.0))
}
}

api.deprecationNotices(ctx)

xffmw, _ := xff.Default()
Expand Down
16 changes: 16 additions & 0 deletions internal/api/password.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"fmt"
"strings"

"github.com/sirupsen/logrus"
)

// WeakPasswordError encodes an error that a password does not meet strength
Expand Down Expand Up @@ -39,6 +41,20 @@ func (a *API) checkPasswordStrength(ctx context.Context, password string) error
}
}

if config.Password.HIBP.Enabled {
pwned, err := a.hibpClient.Check(ctx, password)
if err != nil {
if config.Password.HIBP.FailClosed {
return internalServerError("Unable to perform password strength check with HaveIBeenPwned.org.").WithInternalError(err)
} else {
logrus.WithError(err).Warn("Unable to perform password strength check with HaveIBeenPwned.org, pwned passwords are being allowed")
}
} else if pwned {
reasons = append(reasons, "pwned")
messages = append(messages, "Password is known to be weak and easy to guess, please choose a different one.")
}
}

if len(reasons) > 0 {
return &WeakPasswordError{
Message: strings.Join(messages, " "),
Expand Down
20 changes: 20 additions & 0 deletions internal/conf/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,30 @@ func (v *PasswordRequiredCharacters) Decode(value string) error {
return nil
}

// HIBPBloomConfiguration configures a bloom cache for pwned passwords. Use
// this tool to gauge the Items and FalsePositives values:
// https://hur.st/bloomfilter
type HIBPBloomConfiguration struct {
Enabled bool `json:"enabled"`
Items uint `json:"items" default:"100000"`
FalsePositives float64 `json:"false_positives" split_words:"true" default:"0.0000099"`
}

type HIBPConfiguration struct {
Enabled bool `json:"enabled"`
FailClosed bool `json:"fail_closed" split_words:"true"`

UserAgent string `json:"user_agent" split_words:"true" default:"https://github.com/supabase/gotrue"`

Bloom HIBPBloomConfiguration `json:"bloom"`
}

type PasswordConfiguration struct {
MinLength int `json:"min_length" split_words:"true"`

RequiredCharacters PasswordRequiredCharacters `json:"required_characters" split_words:"true"`

HIBP HIBPConfiguration `json:"hibp"`
}

// GlobalConfiguration holds all the configuration that applies to all instances.
Expand Down
76 changes: 76 additions & 0 deletions internal/utilities/hibpcache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package utilities

import (
"context"
"sync"

"github.com/bits-and-blooms/bloom/v3"
)

const (
// hibpHashLength is the length of a hex-encoded SHA1 hash.
hibpHashLength = 40
// hibpHashPrefixLength is the length of the hashed password prefix.
hibpHashPrefixLength = 5
)

type HIBPBloomCache struct {
sync.RWMutex

n uint
items uint
filter *bloom.BloomFilter
}

func NewHIBPBloomCache(n uint, fp float64) *HIBPBloomCache {
cache := &HIBPBloomCache{
n: n,
filter: bloom.NewWithEstimates(n, fp),
}

return cache
}

func (c *HIBPBloomCache) Cap() uint {
return c.filter.Cap()
}

func (c *HIBPBloomCache) Add(ctx context.Context, prefix []byte, suffixes [][]byte) error {
c.Lock()
defer c.Unlock()

c.items += uint(len(suffixes))

if c.items > (4*c.n)/5 {
// clear the filter if 80% full to keep the actual false
// positive rate low
c.filter.ClearAll()

// reduce memory footprint when this happens
c.filter.BitSet().Compact()

c.items = uint(len(suffixes))
}

var combined [hibpHashLength]byte
copy(combined[:], prefix)

for _, suffix := range suffixes {
copy(combined[hibpHashPrefixLength:], suffix)

c.filter.Add(combined[:])
}

return nil
}

func (c *HIBPBloomCache) Contains(ctx context.Context, prefix, suffix []byte) (bool, error) {
var combined [hibpHashLength]byte
copy(combined[:], prefix)
copy(combined[hibpHashPrefixLength:], suffix)

c.RLock()
defer c.RUnlock()

return c.filter.Test(combined[:]), nil
}

0 comments on commit c3acfe7

Please sign in to comment.