63 lines
3.9 KiB
Gherkin
63 lines
3.9 KiB
Gherkin
Feature: API responses never expose a user's secret fields
|
|
|
|
As the maintainer of RenewCMS
|
|
I want every HTTP response that includes user data to carry only public fields, never a
|
|
password hash, a verification or password-reset code hash, or their expiry timestamps
|
|
So that a bug, a future endpoint, or a client inspecting a response body can never recover a
|
|
credential or a still-valid reset/verification secret
|
|
|
|
Background:
|
|
Given the public shape of a user in any API response is exactly eight fields: id, username,
|
|
email, is_verified, created_at, updated_at, display_name and role
|
|
And password, password_reset_code, verification_code, verification_expiration,
|
|
password_reset_expiration and token_version never appear in any response body, under any key
|
|
name, regardless of their value or whether they are set
|
|
|
|
Scenario: A successful verification response carries only public fields
|
|
Given a user repository containing user id 7, username "alice", email "alice@example.com",
|
|
is_verified false, whose stored VerificationCode is the bcrypt hash of "abc123" and whose
|
|
VerificationExpiration is one hour from now
|
|
And a valid jwt cookie for user id 7
|
|
When the client sends POST /v1/auth/verify with body {"code": "abc123"}
|
|
Then the response status is 200
|
|
And the response body is a JSON object with exactly the keys id, username, email,
|
|
is_verified, created_at, updated_at, display_name and role
|
|
And the response body does not contain the keys password, password_reset_code,
|
|
verification_code, verification_expiration, password_reset_expiration or token_version
|
|
And the response body's is_verified is true
|
|
And the response body's display_name is ""
|
|
|
|
Scenario: A rejected verification attempt never echoes any user field
|
|
Given a user repository containing user id 7, username "alice", is_verified false, whose
|
|
stored VerificationCode is the bcrypt hash of "abc123" and whose VerificationExpiration is
|
|
one hour from now
|
|
And a valid jwt cookie for user id 7
|
|
When the client sends POST /v1/auth/verify with body {"code": "wrong-code"}
|
|
Then the response status is 403
|
|
And the response body is the plain-text apperr.VerificationFailedErrorMessage, not JSON, and
|
|
contains no user field at all
|
|
|
|
Scenario: Registering while no verified account exists never echoes any user field
|
|
Given the user repository is empty
|
|
When the client sends POST /v1/auth/register with username "bob", password "hunter2example12",
|
|
email "bob@example.com"
|
|
Then the response status is 200
|
|
And the response body contains no key named password, password_reset_code,
|
|
verification_code, verification_expiration or password_reset_expiration
|
|
|
|
Scenario Outline: No other auth endpoint under /v1/auth leaks a user field
|
|
Given a user repository containing user id 7, username "alice", email "alice@example.com",
|
|
is_verified true, with a non-empty PasswordResetCode or VerificationCode where the endpoint
|
|
under test would set one
|
|
When the client sends <method> <path> with <body>
|
|
Then the response status is <status>
|
|
And the response body contains no key named password, password_reset_code,
|
|
verification_code, verification_expiration or password_reset_expiration
|
|
|
|
Examples:
|
|
| method | path | body | status |
|
|
| POST | /v1/auth/login | {"username":"alice","password":"hunter2example12"} | 200 |
|
|
| POST | /v1/auth/logout | {} | 200 |
|
|
| POST | /v1/auth/password-reset | {"email":"alice@example.com"} | 200 |
|
|
| POST | /v1/auth/password-reset/confirm | {"email":"alice@example.com","code":"wrong","password":"hunter2example12"} | 403 |
|