Skip to main content

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​

Recommended Token Storage
  • 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:

EndpointDescription
GET /api/storefront/ordersUser's order history
GET /api/storefront/basket/userUser's basket
POST /api/storefront/checkout/*Checkout operations
GET /api/storefront/profileUser profile
*/api/storefront/addresses/*Address management
POST /api/storefront/gdpr/*GDPR data requests

Error Responses​

StatusMeaning
401 UnauthorizedMissing or invalid token
403 ForbiddenValid token but insufficient permissions (or license issue)
400 Bad RequestInvalid 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" }
  • identifier is 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 202 whether or not an account exists; 429 means 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.
  • verify answers 200 with 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 202 with { "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 use change-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.