Authentication
Quark Commerce uses JWT (JSON Web Tokens) for authentication. The auth endpoints are at /api/auth/.
Auth Flow
Endpoints
Register
POST /api/auth/register
Content-Type: application/json
{
"email": "user@example.com",
"password": "SecurePassword123!",
"firstName": "John",
"lastName": "Doe"
}
Response: 200 OK
{ "message": "Registration successful" }
Login
POST /api/auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "SecurePassword123!"
}
Response: 200 OK
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "dGhpcyBpcyBhIHJlZnJlc2g...",
"user": {
"id": "guid",
"email": "user@example.com",
"firstName": "John",
"lastName": "Doe",
"roles": ["Customer"]
}
}
Change Password
POST /api/auth/change-password
Authorization: Bearer <token>
Content-Type: application/json
{
"currentPassword": "OldPassword123!",
"newPassword": "NewPassword456!"
}
Forgot Password
POST /api/auth/forgot-password
Content-Type: application/json
{
"email": "user@example.com",
"resetBaseUrl": "https://store.example.com/reset-password"
}
The API sends a password reset email (if the email exists) with a link to {resetBaseUrl}?token={token}. The response always returns success to avoid revealing whether the email exists.
Reset Password
POST /api/auth/reset-password
Content-Type: application/json
{
"token": "reset-token-from-email",
"newPassword": "NewSecurePassword789!"
}
Security Best Practices
- Access token: Store in memory only (JavaScript variable). Never in localStorage or sessionStorage.
- Refresh token: Store in an HttpOnly cookie (set by the server or managed by your app).
- This prevents XSS attacks from accessing tokens.
Protected Endpoints
Most storefront endpoints are public (product browsing, categories, etc.). These endpoints require authentication:
| Endpoint | Description |
|---|---|
GET /api/storefront/orders | User's order history |
GET /api/storefront/basket/user | User's basket |
POST /api/storefront/checkout/* | Checkout operations |
GET /api/storefront/profile | User profile |
*/api/storefront/addresses/* | Address management |
POST /api/storefront/gdpr/* | GDPR data requests |
Error Responses
| Status | Meaning |
|---|---|
401 Unauthorized | Missing or invalid token |
403 Forbidden | Valid token but insufficient permissions (or license issue) |
400 Bad Request | Invalid request body |
Sessions: Refresh Tokens
Every successful sign-in (login, code/verify, code/register) returns an access token and a refresh token:
{
"token": "<JWT, short-lived>",
"expiration": "2026-09-21T10:00:00Z",
"refreshToken": "<opaque, single-use>",
"refreshTokenExpiration": "2026-11-20T09:00:00Z",
"hasPassword": false,
"isNewUser": true
}
POST /api/auth/refresh
{ "refreshToken": "…" }
returns a new pair. Refresh tokens rotate: the one you sent stops working, so always store the newest. A client must serialize its refreshes — two parallel calls with the same token means one of them fails (within a few seconds that is treated as a harmless race; presenting an already-rotated token later is treated as theft and ends the whole session). POST /api/auth/logout with the refresh token ends the session on that device; resetting a password ends every session. Lifetimes: Jwt:ExpirationMinutes (default 60) and Jwt:RefreshTokenDays (default 60, sliding with use).
Sign-in with a One-Time Code
Password-less sign-in by e-mail address or phone number — also the simplest way in for customers migrated from another platform, whose passwords cannot be carried over.
POST /api/auth/code/request { "identifier": "ayse@example.com" } → 202
POST /api/auth/code/verify { "identifier": "ayse@example.com", "code": "123456", "deviceName": "iPhone 17" }
identifieris an e-mail address or a phone number in E.164 form (+905321234567). National formats are rejected — the client knows the customer's country, the API does not.- The request answers
202whether or not an account exists;429means throttled (one code per minute, five per hour per identifier) or a malformed identifier. Codes have six digits, live 10 minutes, allow five attempts and are stored only as a hash. verifyanswers200with a session. For an unknown e-mail address the account is created on the spot (isNewUser: true, no password, e-mail verified).- For a verified phone number with no account it answers
202with{ "registrationRequired": true, "registrationTicket": "…" }: accounts are identified by e-mail, so finish with
POST /api/auth/code/register { "registrationTicket": "…", "email": "ayse@example.com", "firstName": "Ayşe", "lastName": "Yılmaz" }
which answers 200 with a session, or 409 when that e-mail already has an account — a verified phone never takes over an existing e-mail account. A phone number shared by several accounts is treated as unknown rather than guessed.
POST /api/auth/set-password(authorized) gives a password to an account that has none; accounts with a password usechange-password.
Delivery uses the store's notification providers (e-mail: SMTP; SMS: Twilio or any INotificationProvider). For development machines without a provider, Auth:LogSignInCodes=true writes codes to the application log — never enable it where logs are shared.
Password Storage
Passwords are stored as salted PBKDF2-HMAC-SHA256 (600,000 iterations) in a self-describing format, so the algorithm and its cost can change without a data migration. Hashes from earlier versions (unsalted SHA-256) still verify and are upgraded transparently at the customer's next successful sign-in.