113 lines
5.9 KiB
Gherkin
113 lines
5.9 KiB
Gherkin
Feature: Password and code hashing go through a domain Hasher interface
|
|
|
|
As the maintainer of RenewCMS
|
|
I want password and verification/reset-code hashing done by a domain hash.Hasher
|
|
interface, used only by use cases
|
|
So that internal/infrastructure/persistence/repositories/userRepository.go and
|
|
api/handlers/auth/authHandler.go stop calling bcrypt directly, and a use case can be
|
|
tested against a fake hasher instead of a real bcrypt cost factor
|
|
|
|
Background:
|
|
Given internal/domain/hash defines the interface Hasher with Hash(value string) (string, error)
|
|
and Compare(hashed string, value string) error
|
|
And internal/domain/user defines the sentinel user.ErrInvalidCredentials alongside the
|
|
existing user.ErrUserNotFound
|
|
And internal/infrastructure/hashing.BcryptHasher implements hash.Hasher and is asserted
|
|
with var _ hash.Hasher = &BcryptHasher{}
|
|
And api/dependecyInjection.go constructs one BcryptHasher and passes it to
|
|
CreateUserUseCase, UpdateUserUseCase and the new AuthenticateUserUseCase
|
|
And UserRepository.Create, UpdatePassword and UpdatePasswordResetCode store the string
|
|
they are given verbatim and never call bcrypt
|
|
And the auth Handler no longer imports golang.org/x/crypto/bcrypt; Login calls
|
|
AuthenticateUserUseCase.Authenticate instead of comparing a hash itself
|
|
|
|
Scenario: Creating a user stores a hashed password and a hashed verification code
|
|
Given a fake hasher that hashes any value "x" to "hashed:x"
|
|
When CreateUserUseCase.CreateUser is called with username "alice", password
|
|
"s3cretPassw0rd" and email "alice@example.com"
|
|
Then the repository's Create is called with a user whose Password is "hashed:s3cretPassw0rd"
|
|
And that user's VerificationCode is "hashed:<the generated code>", not the raw code
|
|
And the User returned by the use case has VerificationCode equal to the raw generated
|
|
code, not the hash
|
|
|
|
Scenario: A hashing failure while creating a user persists nothing
|
|
Given a fake hasher whose Hash always returns an error
|
|
When CreateUserUseCase.CreateUser is called with username "alice", password
|
|
"s3cretPassw0rd" and email "alice@example.com"
|
|
Then the use case returns that error
|
|
And the repository's Create is never called
|
|
|
|
Scenario Outline: Updating a password or a password reset code stores the hashed value
|
|
Given a fake hasher that hashes any value "x" to "hashed:x"
|
|
When UpdateUserUseCase.<Method> is called with user id 1 and "<plaintext>"
|
|
Then the repository's <Method> is called with "hashed:<plaintext>", not "<plaintext>"
|
|
|
|
Examples:
|
|
| Method | plaintext |
|
|
| UpdatePassword | newPassw0rd |
|
|
| UpdatePasswordResetCode | reset-code-123 |
|
|
|
|
Scenario Outline: A hashing failure during an update persists nothing
|
|
Given a fake hasher whose Hash always returns an error
|
|
When UpdateUserUseCase.<Method> is called with user id 1 and "<plaintext>"
|
|
Then the use case returns that error
|
|
And the repository's <Method> is never called
|
|
|
|
Examples:
|
|
| Method | plaintext |
|
|
| UpdatePassword | newPassw0rd |
|
|
| UpdatePasswordResetCode | reset-code-123 |
|
|
|
|
Scenario: Authenticating with the correct password returns the user
|
|
Given a user repository containing a user "alice" with stored password "hashed:hunter2example"
|
|
And a fake hasher whose Compare returns nil when given "hashed:hunter2example" and "hunter2example"
|
|
When AuthenticateUserUseCase.Authenticate is called with username "alice" and password
|
|
"hunter2example"
|
|
Then the result is that user, with no error
|
|
|
|
Scenario: Authenticating with the wrong password returns a domain sentinel error
|
|
Given a user repository containing a user "alice" with stored password "hashed:hunter2example"
|
|
And a fake hasher whose Compare returns an error for that password
|
|
When AuthenticateUserUseCase.Authenticate is called with username "alice" and password
|
|
"wrongpassword12"
|
|
Then the result is the error user.ErrInvalidCredentials
|
|
And errors.Is(err, user.ErrInvalidCredentials) is true
|
|
|
|
Scenario: Authenticating an unknown username never calls the hasher
|
|
Given a user repository with no user "bob"
|
|
When AuthenticateUserUseCase.Authenticate is called with username "bob" and password
|
|
"hunter2example"
|
|
Then the result is the error user.ErrUserNotFound
|
|
And the hasher's Compare is never called
|
|
|
|
Scenario Outline: Login over HTTP still can't distinguish a bad password from an unknown username
|
|
Given a user repository containing a user "alice" with stored password "hashed:hunter2example"
|
|
And a real BcryptHasher
|
|
When the client sends POST /v1/auth/login with username "<username>" and password "<password>"
|
|
Then the response status is 403
|
|
And the response body is the text apperr.LogsErrorMessage
|
|
And no jwt cookie is set
|
|
|
|
Examples:
|
|
| username | password | case |
|
|
| alice | wrongpassword12 | existing user, bad pass |
|
|
| bob | hunter2example | unknown username |
|
|
|
|
Scenario: Registering a user stores the hashed password, never the plaintext
|
|
Given an empty user repository
|
|
And a real BcryptHasher
|
|
When the client sends POST /v1/auth/register with username "alice", password
|
|
"s3cretPassw0rd" and email "alice@example.com"
|
|
Then the response status is 200
|
|
And a jwt cookie is set
|
|
And the user repository's stored Password for "alice" is not "s3cretPassw0rd"
|
|
And BcryptHasher.Compare("<the stored value>", "s3cretPassw0rd") returns nil
|
|
|
|
Scenario: A hashing failure during registration surfaces as 500 and sets no cookie
|
|
Given an empty user repository
|
|
And a fake hasher whose Hash always returns an error
|
|
When the client sends POST /v1/auth/register with username "alice", password
|
|
"s3cretPassw0rd" and email "alice@example.com"
|
|
Then the response status is 500
|
|
And no jwt cookie is set
|