Skip to content

Commit 8e95ef7

Browse files
Merge pull request #10 from crydensync/feat/password-policy2
Feat/password policy2
2 parents 1d86ba1 + a0c53e4 commit 8e95ef7

4 files changed

Lines changed: 42 additions & 23 deletions

File tree

auth/breach_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func TestSignUp_RejectsBreachedPassword(t *testing.T) {
2929
ctx := context.Background()
3030
checker := &fakeBreachChecker{breached: true}
3131

32-
_, err := SignUp(ctx, users, hasher, ids, limiter, checker, audit, log, "proguy@example.com", "password123", "1.2.3.4")
32+
_, err := SignUp(ctx, users, hasher, ids, limiter, checker, audit, log, security.PasswordPolicy{}, "proguy@example.com", "password123", "1.2.3.4")
3333
if err != ErrPasswordBreached {
3434
t.Errorf("expected ErrPasswordBreached, got %v", err)
3535
}
@@ -43,7 +43,7 @@ func TestSignUp_BreachCheckerErrorFailsOpen(t *testing.T) {
4343
ctx := context.Background()
4444
checker := &fakeBreachChecker{err: errors.New("simulated HIBP outage")}
4545

46-
_, err := SignUp(ctx, users, hasher, ids, limiter, checker, audit, log, "proguy@example.com", "password123", "1.2.3.4")
46+
_, err := SignUp(ctx, users, hasher, ids, limiter, checker, audit, log, security.PasswordPolicy{}, "proguy@example.com", "password123", "1.2.3.4")
4747
if err != nil {
4848
t.Fatalf("expected signup to succeed (fail open) when the breach checker errors, got %v", err)
4949
}
@@ -61,7 +61,7 @@ func TestChangePassword_RejectsBreachedNewPassword(t *testing.T) {
6161
hash, _ := hasher.Hash("old-password")
6262
users.Create(ctx, storeUser("user-1", "raymondproguy@dev.com", hash))
6363

64-
err := ChangePassword(ctx, users, sessions, hasher, checker, audit, log, "user-1", "old-password", "password123")
64+
err := ChangePassword(ctx, users, sessions, hasher, checker, audit, log, security.PasswordPolicy{}, "user-1", "old-password", "password123")
6565
if err != ErrPasswordBreached {
6666
t.Errorf("expected ErrPasswordBreached, got %v", err)
6767
}
@@ -81,7 +81,7 @@ func TestChangePassword_WrongCurrentPasswordCheckedBeforeBreach(t *testing.T) {
8181
hash, _ := hasher.Hash("old-password")
8282
users.Create(ctx, storeUser("user-1", "raymondproguy@dev.com", hash))
8383

84-
err := ChangePassword(ctx, users, sessions, hasher, checker, audit, log, "user-1", "totally-wrong", "password123")
84+
err := ChangePassword(ctx, users, sessions, hasher, checker, audit, log, security.PasswordPolicy{}, "user-1", "totally-wrong", "password123")
8585
if err != ErrInvalidCredentials {
8686
t.Errorf("expected ErrInvalidCredentials (checked before breach check), got %v", err)
8787
}
@@ -95,7 +95,7 @@ func TestSignUp_BreachRejectionIsAudited(t *testing.T) {
9595
ctx := context.Background()
9696
checker := &fakeBreachChecker{breached: true}
9797

98-
SignUp(ctx, users, hasher, ids, limiter, checker, audit, log, "proguy@example.com", "password123", "1.2.3.4")
98+
SignUp(ctx, users, hasher, ids, limiter, checker, audit, log, security.PasswordPolicy{}, "proguy@example.com", "password123", "1.2.3.4")
9999

100100
events, err := audit.SearchByType(ctx, store.EventPasswordBreachRejected, 10)
101101
if err != nil {

security/webauthn_test.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,17 @@ func TestGoWebAuthnProvider_LoginRoundTrip(t *testing.T) {
9090
user.creds = append(user.creds, *cred)
9191
rp := virtualwebauthn.RelyingParty{Name: testRPDisplayName, ID: testRPID, Origin: testRPOrigin}
9292

93+
// virtualwebauthn's simulated credential starts at counter 0 and
94+
// never auto-increments — unlike many real hardware authenticators,
95+
// which do. Bump it manually here to exercise the pass-through
96+
// path this test actually cares about: does FinishLogin correctly
97+
// surface whatever counter value the authenticator reports, so a
98+
// caller can persist it and detect a future non-advancing (cloned-
99+
// authenticator) value? A counter of 0 is itself a legitimate,
100+
// spec-allowed value (many platform authenticators never track
101+
// one at all), so asserting "must be nonzero" was simply wrong.
102+
vCred.Counter = 7
103+
93104
assertion, session, err := provider.BeginLogin(user)
94105
if err != nil {
95106
t.Fatalf("BeginLogin failed: %v", err)
@@ -109,8 +120,8 @@ func TestGoWebAuthnProvider_LoginRoundTrip(t *testing.T) {
109120
if err != nil {
110121
t.Fatalf("FinishLogin failed: %v", err)
111122
}
112-
if updatedCred.Authenticator.SignCount == 0 {
113-
t.Error("expected the signature counter to have advanced past zero after a real login")
123+
if updatedCred.Authenticator.SignCount != 7 {
124+
t.Errorf("expected the returned credential to carry the authenticator's reported counter (7), got %d", updatedCred.Authenticator.SignCount)
114125
}
115126
}
116127

token/generator.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package token
33
import (
44
"crypto/rand"
55
"encoding/hex"
6+
"io"
67
)
78

89
// TokenGenerator defines generation of opaque, cryptographically random
@@ -21,6 +22,17 @@ type CryptoRandTokenGenerator struct {
2122
// ByteLength is the number of random bytes generated per token.
2223
// 32 bytes (256 bits) is the standard baseline for opaque tokens.
2324
ByteLength int
25+
// randReader defaults to crypto/rand.Reader (set by
26+
// NewCryptoRandTokenGenerator) and is never exposed publicly.
27+
// Deliberately injectable rather than calling crypto/rand.Read
28+
// directly, so a failure path can be tested by swapping this
29+
// field on a same-package instance — mutating the real global
30+
// crypto/rand.Reader instead (the previous approach) is not
31+
// portable: on at least one real platform, a failed read through
32+
// the actual global triggers the Go runtime's own unrecoverable
33+
// fatal-error path instead of returning a normal error, crashing
34+
// the whole test binary rather than failing one test.
35+
randReader io.Reader
2436
}
2537

2638
// NewCryptoRandTokenGenerator constructs a generator. byteLength must
@@ -31,12 +43,12 @@ func NewCryptoRandTokenGenerator(byteLength int) (*CryptoRandTokenGenerator, err
3143
// Reject anything below 128 bits — too weak for a session token.
3244
return nil, ErrTokenByteLengthTooShort
3345
}
34-
return &CryptoRandTokenGenerator{ByteLength: byteLength}, nil
46+
return &CryptoRandTokenGenerator{ByteLength: byteLength, randReader: rand.Reader}, nil
3547
}
3648

3749
func (g *CryptoRandTokenGenerator) New() (string, error) {
3850
buf := make([]byte, g.ByteLength)
39-
if _, err := rand.Read(buf); err != nil {
51+
if _, err := io.ReadFull(g.randReader, buf); err != nil {
4052
return "", err
4153
}
4254
return hex.EncodeToString(buf), nil

token/generator_rand_error_test.go

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
package token
22

33
import (
4-
"crypto/rand"
54
"errors"
6-
"io"
75
"testing"
86
)
97

10-
// failingReader always errors — swapped in for crypto/rand.Reader to
11-
// deterministically exercise the rand.Read failure path, which never
12-
// fails in practice under normal conditions.
8+
// failingReader always errors — injected directly into a
9+
// CryptoRandTokenGenerator instance (same package, unexported field)
10+
// to deterministically exercise the New() error path. This never
11+
// touches the real crypto/rand.Reader global — see randReader's doc
12+
// comment in generator.go for why mutating that global directly isn't
13+
// safe across platforms.
1314
type failingReader struct{}
1415

1516
func (failingReader) Read(p []byte) (int, error) {
@@ -18,24 +19,19 @@ func (failingReader) Read(p []byte) (int, error) {
1819

1920
func TestCryptoRandTokenGenerator_New_PropagatesRandReadError(t *testing.T) {
2021
// Regression test: New() used to swallow a rand.Read failure and
21-
// return ("", nil) — an empty string treated as a valid token
22-
// with no error to catch it. It must now return the real error.
23-
original := rand.Reader
24-
rand.Reader = failingReader{}
25-
defer func() { rand.Reader = original }()
26-
22+
// return ("", nil) — an empty string treated as a valid token with
23+
// no error to catch it. It must now return the real error.
2724
g, err := NewCryptoRandTokenGenerator(32)
2825
if err != nil {
2926
t.Fatalf("unexpected error constructing generator: %v", err)
3027
}
28+
g.randReader = failingReader{}
3129

3230
tok, err := g.New()
3331
if err == nil {
34-
t.Fatal("expected New() to return an error when rand.Read fails, got nil")
32+
t.Fatal("expected New() to return an error when the entropy source fails, got nil")
3533
}
3634
if tok != "" {
3735
t.Errorf("expected an empty token alongside the error, got %q", tok)
3836
}
3937
}
40-
41-
var _ io.Reader = failingReader{}

0 commit comments

Comments
 (0)