fix(auth): clarify admin login flow
Use admin-specific Telegram copy and define the missing credential API. Replace predictable bootstrap passwords with random one-time secrets.
This commit is contained in:
102
docs/backend/ADMIN-CREDENTIAL-AUTH-HANDOFF.md
Normal file
102
docs/backend/ADMIN-CREDENTIAL-AUTH-HANDOFF.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# Admin credential authentication handoff
|
||||||
|
|
||||||
|
## Current production state
|
||||||
|
|
||||||
|
`admin.gorbushka.market` can authenticate through the existing Telegram
|
||||||
|
session flow. Login/password authentication is not implemented by the live
|
||||||
|
backend, so the frontend must not validate or embed administrator credentials.
|
||||||
|
|
||||||
|
The existing `/admin-login` Ed25519 page is also not production-ready because
|
||||||
|
the backend challenge/verify endpoints do not exist.
|
||||||
|
|
||||||
|
## Required backend API
|
||||||
|
|
||||||
|
Tenant identity comes only from nginx's trusted `X-Storefront-Host` header.
|
||||||
|
Never accept a tenant or marketplace identifier from the login request body.
|
||||||
|
|
||||||
|
### Create session
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/identity/v1/session
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"login": "gorbushka",
|
||||||
|
"password": "<secret>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Success:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"accessToken": "<short-lived JWT>",
|
||||||
|
"refreshToken": "<rotating opaque token>",
|
||||||
|
"expiresAt": "2026-08-21T04:00:00Z",
|
||||||
|
"mustChangePassword": true,
|
||||||
|
"user": {
|
||||||
|
"id": "<id>",
|
||||||
|
"login": "gorbushka",
|
||||||
|
"displayName": "Gorbushka administrator",
|
||||||
|
"roles": ["MARKETPLACE_ADMIN"],
|
||||||
|
"tenantId": "<tenant-id>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Errors:
|
||||||
|
|
||||||
|
- `400` malformed request.
|
||||||
|
- `401 INVALID_CREDENTIALS` with one generic message for unknown login and
|
||||||
|
wrong password.
|
||||||
|
- `403 TENANT_DISABLED` or `TENANT_MISMATCH`.
|
||||||
|
- `429 RATE_LIMITED` with `Retry-After`.
|
||||||
|
|
||||||
|
### Session lifecycle
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/identity/v1/session/refresh
|
||||||
|
DELETE /api/identity/v1/session
|
||||||
|
POST /api/identity/v1/session/change-password
|
||||||
|
GET /api/identity/v1/session/permissions
|
||||||
|
```
|
||||||
|
|
||||||
|
`change-password` accepts `{ currentPassword, newPassword }`. While
|
||||||
|
`mustChangePassword` is true, every non-auth admin endpoint returns
|
||||||
|
`403 PASSWORD_CHANGE_REQUIRED`.
|
||||||
|
|
||||||
|
## Provisioning and security requirements
|
||||||
|
|
||||||
|
- Generate a random one-time bootstrap password. Do not use the documented
|
||||||
|
deterministic `{slug}2026$` pattern in production.
|
||||||
|
- Store only an Argon2id password hash with a unique salt.
|
||||||
|
- Never log passwords, refresh tokens, authorization headers, or session IDs.
|
||||||
|
- Rate-limit by tenant, login, and source IP; add exponential backoff.
|
||||||
|
- Rotate refresh tokens and revoke the full token family on reuse.
|
||||||
|
- Enforce tenant and role authorization on every admin endpoint. Angular
|
||||||
|
guards are UI only.
|
||||||
|
- Audit login success/failure, password change, refresh-token reuse, logout,
|
||||||
|
and lockout without recording secrets.
|
||||||
|
|
||||||
|
## Required nginx invariants
|
||||||
|
|
||||||
|
Backend nginx changes must preserve:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
proxy_set_header X-Storefront-Host $storefront_host;
|
||||||
|
proxy_set_header Origin "";
|
||||||
|
|
||||||
|
add_header Access-Control-Allow-Headers \
|
||||||
|
"Authorization, Content-Type, AdminWebSessionID, WebSessionID, Currency, X-Language, X-Region, X-Requested-With" always;
|
||||||
|
```
|
||||||
|
|
||||||
|
For `Origin: https://admin.gorbushka.market`, `$storefront_host` must be
|
||||||
|
`gorbushka.market`. The API upstream remains `https://127.0.0.1:445` unless
|
||||||
|
the backend team deliberately changes the listening address.
|
||||||
|
|
||||||
|
## Frontend follow-up after backend delivery
|
||||||
|
|
||||||
|
Add the credential form to the admin-only login shell, submit only over HTTPS,
|
||||||
|
store the returned admin session separately from customer auth, force the
|
||||||
|
password-change screen when requested, and keep Telegram as an optional
|
||||||
|
fallback. Do not expose a non-functional credential form before the API ships.
|
||||||
@@ -79,7 +79,7 @@ Nothing here is done yet — this is the setup a backend dev does on day one.
|
|||||||
4. Implement the **bootstrap config endpoint** (§1a) — without it the frontend cannot render for any tenant.
|
4. Implement the **bootstrap config endpoint** (§1a) — without it the frontend cannot render for any tenant.
|
||||||
5. Implement the Telegram session endpoints — the login flow is fully built client-side and blocked only on these.
|
5. Implement the Telegram session endpoints — the login flow is fully built client-side and blocked only on these.
|
||||||
6. Implement `GET /api/identity/v1/session/permissions` ([TRACK-S §2](TRACK-S-SECURITY-RBAC-CONTRACT.md)) — frontend route guards derive from it.
|
6. Implement `GET /api/identity/v1/session/permissions` ([TRACK-S §2](TRACK-S-SECURITY-RBAC-CONTRACT.md)) — frontend route guards derive from it.
|
||||||
7. Seed per-marketplace bootstrap admins ([TRACK-S §8](TRACK-S-SECURITY-RBAC-CONTRACT.md)): login = marketplace slug, password = `{slug}2026$`, `mustChangePassword: true`.
|
7. Seed per-marketplace bootstrap admins ([TRACK-S §8](TRACK-S-SECURITY-RBAC-CONTRACT.md)): login = marketplace slug, cryptographically random one-time password delivered out of band, `mustChangePassword: true`.
|
||||||
|
|
||||||
Steps 4–6 unblock the entire frontend. Everything after is feature work.
|
Steps 4–6 unblock the entire frontend. Everything after is feature work.
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,8 @@ Customer/seller PII is exposed only to roles that need it for their scope (e.g.
|
|||||||
Each marketplace ships with one bootstrap `MARKETPLACE_ADMIN` account, seeded at provisioning time (Phase 9 launch step):
|
Each marketplace ships with one bootstrap `MARKETPLACE_ADMIN` account, seeded at provisioning time (Phase 9 launch step):
|
||||||
|
|
||||||
- `login` = marketplace slug (`projectName`)
|
- `login` = marketplace slug (`projectName`)
|
||||||
- `password` = `{projectName}2026$`, flagged `mustChangePassword: true`
|
- `password` = cryptographically random one-time secret delivered out of band,
|
||||||
|
flagged `mustChangePassword: true` (never derive it from the marketplace slug)
|
||||||
- Login succeeds but every non-auth request 403s with `PASSWORD_CHANGE_REQUIRED` until password is changed.
|
- Login succeeds but every non-auth request 403s with `PASSWORD_CHANGE_REQUIRED` until password is changed.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
<app-icon name="lock" [size]="40" />
|
<app-icon name="lock" [size]="40" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>{{ 'auth.loginRequired' | translate }}</h2>
|
<h2>{{ (mode === 'admin' ? 'auth.adminLoginRequired' : 'auth.loginRequired') | translate }}</h2>
|
||||||
<p class="login-desc">{{ 'auth.loginDescription' | translate }}</p>
|
<p class="login-desc">{{ (mode === 'admin' ? 'auth.adminLoginDescription' : 'auth.loginDescription') | translate }}</p>
|
||||||
|
|
||||||
@if (status() === 'checking') {
|
@if (status() === 'checking') {
|
||||||
<div class="login-status checking">
|
<div class="login-status checking">
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<svg class="tg-icon" width="22" height="22" viewBox="0 0 24 24" fill="currentColor">
|
<svg class="tg-icon" width="22" height="22" viewBox="0 0 24 24" fill="currentColor">
|
||||||
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/>
|
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/>
|
||||||
</svg>
|
</svg>
|
||||||
{{ 'auth.loginWithTelegram' | translate }}
|
{{ (mode === 'admin' ? 'auth.adminLoginWithTelegram' : 'auth.loginWithTelegram') | translate }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- @if (loginUrl()) {
|
<!-- @if (loginUrl()) {
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="login-note">{{ 'auth.loginNote' | translate }}</p>
|
<p class="login-note">{{ (mode === 'admin' ? 'auth.adminLoginNote' : 'auth.loginNote') | translate }}</p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1154,6 +1154,10 @@ export const en: Translations = {
|
|||||||
auth: {
|
auth: {
|
||||||
loginRequired: 'Login required',
|
loginRequired: 'Login required',
|
||||||
loginDescription: 'Please log in via Telegram to proceed with your order',
|
loginDescription: 'Please log in via Telegram to proceed with your order',
|
||||||
|
adminLoginRequired: 'Admin panel sign-in',
|
||||||
|
adminLoginDescription: 'Sign in through Telegram with an administrator account',
|
||||||
|
adminLoginWithTelegram: 'Sign in as administrator',
|
||||||
|
adminLoginNote: 'The admin panel will open after verification',
|
||||||
checking: 'Checking...',
|
checking: 'Checking...',
|
||||||
loginWithTelegram: 'Log in with Telegram',
|
loginWithTelegram: 'Log in with Telegram',
|
||||||
orScanQr: 'Or scan the QR code',
|
orScanQr: 'Or scan the QR code',
|
||||||
|
|||||||
@@ -1154,6 +1154,10 @@ export const hy: Translations = {
|
|||||||
auth: {
|
auth: {
|
||||||
loginRequired: 'Պահանջվում է մուտք',
|
loginRequired: 'Պահանջվում է մուտք',
|
||||||
loginDescription: 'Պատվերի համար մուտք գործեք Telegram-ով',
|
loginDescription: 'Պատվերի համար մուտք գործեք Telegram-ով',
|
||||||
|
adminLoginRequired: 'Մուտք կառավարման վահանակ',
|
||||||
|
adminLoginDescription: 'Մուտք գործեք Telegram-ով՝ ադմինիստրատորի հաշվով',
|
||||||
|
adminLoginWithTelegram: 'Մուտք գործել որպես ադմինիստրատոր',
|
||||||
|
adminLoginNote: 'Ստուգումից հետո կբացվի կառավարման վահանակը',
|
||||||
checking: 'Ստուգում...',
|
checking: 'Ստուգում...',
|
||||||
loginWithTelegram: 'Մուտք Telegram-ով',
|
loginWithTelegram: 'Մուտք Telegram-ով',
|
||||||
orScanQr: 'Կամ սքանավորեք QR կոդը',
|
orScanQr: 'Կամ սքանավորեք QR կոդը',
|
||||||
|
|||||||
@@ -1154,6 +1154,10 @@ export const ru: Translations = {
|
|||||||
auth: {
|
auth: {
|
||||||
loginRequired: 'Требуется авторизация',
|
loginRequired: 'Требуется авторизация',
|
||||||
loginDescription: 'Для оформления заказа войдите через Telegram',
|
loginDescription: 'Для оформления заказа войдите через Telegram',
|
||||||
|
adminLoginRequired: 'Вход в панель управления',
|
||||||
|
adminLoginDescription: 'Войдите через Telegram с аккаунтом администратора',
|
||||||
|
adminLoginWithTelegram: 'Войти как администратор',
|
||||||
|
adminLoginNote: 'После подтверждения откроется панель управления',
|
||||||
checking: 'Проверка...',
|
checking: 'Проверка...',
|
||||||
loginWithTelegram: 'Войти через Telegram',
|
loginWithTelegram: 'Войти через Telegram',
|
||||||
orScanQr: 'Или отсканируйте QR-код',
|
orScanQr: 'Или отсканируйте QR-код',
|
||||||
|
|||||||
@@ -1153,6 +1153,10 @@ export interface Translations {
|
|||||||
auth: {
|
auth: {
|
||||||
loginRequired: string;
|
loginRequired: string;
|
||||||
loginDescription: string;
|
loginDescription: string;
|
||||||
|
adminLoginRequired: string;
|
||||||
|
adminLoginDescription: string;
|
||||||
|
adminLoginWithTelegram: string;
|
||||||
|
adminLoginNote: string;
|
||||||
checking: string;
|
checking: string;
|
||||||
loginWithTelegram: string;
|
loginWithTelegram: string;
|
||||||
orScanQr: string;
|
orScanQr: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user