- Docker Desktop installed and running
- Start the stack:
docker compose up -d - API available at http://localhost:5080
admin@acme.test/Test123!/ tenant_slug:acmemember@acme.test/Test123!/ tenant_slug:acme
$login = Invoke-RestMethod -Uri "http://localhost:5080/api/auth/login" -Method POST -ContentType "application/json" -Body '{"email":"admin@acme.test","password":"Test123!","tenant_slug":"acme"}'
$login | ConvertTo-Json
# Expected: access_token, refresh_token, user objectSave tokens for subsequent tests:
$accessToken = $login.access_token
$refreshToken = $login.refresh_token
$headers = @{ Authorization = "Bearer $accessToken" }$refresh = Invoke-RestMethod -Uri "http://localhost:5080/api/auth/refresh" -Method POST -ContentType "application/json" -Body "{`"refresh_token`":`"$refreshToken`"}"
$refresh | ConvertTo-Json
# Expected: new access_token, new refresh_token
# Old refresh token should now be invalidUpdate tokens:
$accessToken = $refresh.access_token
$refreshToken = $refresh.refresh_token
$headers = @{ Authorization = "Bearer $accessToken" }Invoke-RestMethod -Uri "http://localhost:5080/api/auth/validate" -Method GET -Headers $headers
# Expected: valid = true, user_id, tenant_id, etc.Invoke-RestMethod -Uri "http://localhost:5080/api/auth/logout" -Method POST -Headers $headers -ContentType "application/json" -Body "{`"refresh_token`":`"$refreshToken`"}"
# Expected: { success: true, message: "Logged out successfully" }Verify refresh token is now invalid:
try {
Invoke-RestMethod -Uri "http://localhost:5080/api/auth/refresh" -Method POST -ContentType "application/json" -Body "{`"refresh_token`":`"$refreshToken`"}"
} catch {
$_.Exception.Response.StatusCode # Should be 401 Unauthorized
}# Login again first
$login = Invoke-RestMethod -Uri "http://localhost:5080/api/auth/login" -Method POST -ContentType "application/json" -Body '{"email":"admin@acme.test","password":"Test123!","tenant_slug":"acme"}'
$accessToken = $login.access_token
$refreshToken = $login.refresh_token
$headers = @{ Authorization = "Bearer $accessToken" }
# Logout all (no refresh_token in body)
Invoke-RestMethod -Uri "http://localhost:5080/api/auth/logout" -Method POST -Headers $headers -ContentType "application/json" -Body '{}'
# Expected: All refresh tokens for this user are revoked$register = Invoke-RestMethod -Uri "http://localhost:5080/api/auth/register" -Method POST -ContentType "application/json" -Body '{
"email": "newuser@example.com",
"password": "SecurePass123!",
"first_name": "New",
"last_name": "User",
"tenant_slug": "acme"
}'
$register | ConvertTo-Json
# Expected: 201 Created, access_token, refresh_token, user object# Missing fields
try {
Invoke-RestMethod -Uri "http://localhost:5080/api/auth/register" -Method POST -ContentType "application/json" -Body '{
"email": "",
"password": "short",
"tenant_slug": "acme"
}'
} catch {
$_.ErrorDetails.Message | ConvertFrom-Json | ConvertTo-Json
# Expected: 400 with validation errors
}try {
Invoke-RestMethod -Uri "http://localhost:5080/api/auth/register" -Method POST -ContentType "application/json" -Body '{
"email": "admin@acme.test",
"password": "AnotherPass123!",
"first_name": "Duplicate",
"last_name": "User",
"tenant_slug": "acme"
}'
} catch {
$_.ErrorDetails.Message | ConvertFrom-Json
# Expected: 409 Conflict - "Email already registered"
}$forgot = Invoke-RestMethod -Uri "http://localhost:5080/api/auth/forgot-password" -Method POST -ContentType "application/json" -Body '{
"email": "admin@acme.test",
"tenant_slug": "acme"
}'
$forgot | ConvertTo-Json
# In Development: Returns reset_token
# In Production: Returns generic success messageSave reset token (development only):
$resetToken = $forgot.reset_tokenInvoke-RestMethod -Uri "http://localhost:5080/api/auth/reset-password" -Method POST -ContentType "application/json" -Body "{`"token`":`"$resetToken`",`"new_password`":`"NewSecurePass123!`"}"
# Expected: { success: true, message: "Password reset successfully" }Verify new password works:
$login = Invoke-RestMethod -Uri "http://localhost:5080/api/auth/login" -Method POST -ContentType "application/json" -Body '{"email":"admin@acme.test","password":"NewSecurePass123!","tenant_slug":"acme"}'
$login | ConvertTo-Json
# Expected: Successful loginReset password back:
# Request new reset token
$forgot = Invoke-RestMethod -Uri "http://localhost:5080/api/auth/forgot-password" -Method POST -ContentType "application/json" -Body '{"email":"admin@acme.test","tenant_slug":"acme"}'
$resetToken = $forgot.reset_token
# Reset back to original
Invoke-RestMethod -Uri "http://localhost:5080/api/auth/reset-password" -Method POST -ContentType "application/json" -Body "{`"token`":`"$resetToken`",`"new_password`":`"Test123!`"}"try {
Invoke-RestMethod -Uri "http://localhost:5080/api/auth/reset-password" -Method POST -ContentType "application/json" -Body '{"token":"invalid_token","new_password":"NewPass123!"}'
} catch {
$_.ErrorDetails.Message | ConvertFrom-Json
# Expected: 400 - "Invalid reset token"
}After 1 hour, the reset token expires. This can be tested by:
- Generating a reset token
- Manually updating the
expires_atin the database to the past - Attempting to use the token
# Same email can register in different tenant
$register = Invoke-RestMethod -Uri "http://localhost:5080/api/auth/register" -Method POST -ContentType "application/json" -Body '{
"email": "newuser@example.com",
"password": "SecurePass123!",
"first_name": "Globex",
"last_name": "User",
"tenant_slug": "globex"
}'
$register | ConvertTo-Json
# Expected: 201 Created - same email, different tenant| Test | Endpoint | Expected |
|---|---|---|
| 1 | POST /api/auth/login | Returns access_token + refresh_token |
| 2 | POST /api/auth/refresh | Returns new tokens, rotates old |
| 3 | GET /api/auth/validate | Token still valid |
| 4 | POST /api/auth/logout | Revokes specific token |
| 5 | POST /api/auth/logout | Revokes all tokens |
| 6 | POST /api/auth/register | 201 + auto-login |
| 7 | POST /api/auth/register | 400 validation errors |
| 8 | POST /api/auth/register | 409 duplicate email |
| 9 | POST /api/auth/forgot-password | Returns reset token (dev) |
| 10 | POST /api/auth/reset-password | Password changed |
| 11 | POST /api/auth/reset-password | 400 invalid token |
| 12 | POST /api/auth/reset-password | 400 expired token |
| 13 | POST /api/auth/register | Cross-tenant isolation |
- RefreshToken entity with tenant isolation
- PasswordResetToken entity with tenant isolation
- POST /api/auth/logout (revoke tokens)
- POST /api/auth/refresh (token rotation)
- POST /api/auth/register (new user registration)
- POST /api/auth/forgot-password (request reset)
- POST /api/auth/reset-password (confirm reset)
- Refresh token rotation for security
- Password reset invalidates all sessions
- Email sending (deferred - requires email service)
- PHASE8_EXECUTION.md with test scripts