Authentication Flows
This document describes the end-to-end authentication flows in BrightChain from both the browser (React) and API (Express) perspectives. For the raw HTTP endpoint reference, see Authentication API Reference.
System Overview
BrightChain authentication combines several mechanisms:
- JWT tokens (HS256, 7-day expiry) for stateless API auth
- ECIES challenge-response for passwordless login
- Mnemonic phrases for account recovery
- Argon2id + ECIES backup codes for emergency recovery
- Server-side sessions via
BrightChainSessionAdapterfor logout invalidation - Role-based access control via middleware
Component Map
| Layer | Component | Responsibility |
|---|---|---|
| Frontend | authService (brightchain-react/src/services/auth.ts) | Login, register, logout, verify, refresh, change password |
| Frontend | authenticatedApi (brightchain-react/src/services/authenticatedApi.ts) | Axios client with automatic Bearer token injection |
| Frontend | api (brightchain-react/src/services/api.ts) | Base Axios client with Accept-Language header |
| Frontend | AuthProvider (from express-suite-react-components) | React context for auth state, wraps the app |
| Frontend | useAuth hook | Access userData, isAuthenticated, auth functions |
| Backend | BrightDbUserController (brightchain-node-express-suite) | Base controller with all auth routes |
| Backend | UserController (brightchain-api-lib) | Extends base with direct-challenge and backup code routes |
| Backend | AuthService (brightchain-api-lib) | ECIES challenge verification |
| Backend | BrightDbAuthService (brightchain-node-express-suite) | Register, login, password change, mnemonic recovery, token signing |
| Backend | createJwtAuthMiddleware | JWT verification, attaches memberContext to request |
| Backend | createRoleMiddleware | RBAC enforcement |
| Backend | BrightChainSessionAdapter | Server-side session management for logout |
| Shared | IRequestUser, ITokenPayload, Member (brightchain-lib) | Shared identity types |
Flow 1: Registration
Browser API
│ │
│ POST /user/register │
│ { username, email, password, │
│ mnemonic? } │
│──────────────────────────────>│
│ │── validate fields
│ │── if mnemonic provided:
│ │ validate format (MnemonicRegex)
│ │ compute HMAC, check uniqueness
│ │ create Member with forceMnemonic
│ │── else:
│ │ create Member (server-generated)
│ │── hash password (bcrypt)
│ │── create EnergyAccount (1000J trial)
│ │── create BrightHub social profile
│ │── sign JWT
│ │
│ 201 { token, memberId, │
│ energyBalance } │
│<──────────────────────────────│
│ │
│── (app does NOT auto-login │
│ after register; user is │
│ redirected to login page) │
The mnemonic is generated during member creation (or provided by the user) and should be displayed to the user once for safekeeping. It is the only way to recover the account if the password is lost (aside from backup codes).
Optionally, the user can supply their own BIP39 mnemonic phrase (12, 15, 18, 21, or 24 words). When provided, the server validates the format against MnemonicRegex, checks HMAC-based uniqueness (to prevent two accounts sharing the same mnemonic), and passes it to Member.newMember via the forceMnemonic parameter to derive the user’s cryptographic identity deterministically. If the mnemonic’s HMAC already exists, registration is rejected with a Validation_MnemonicInUse error.
Frontend code path
// brightchain-react/src/services/auth.ts
const result = await api.post('/user/register', {
username, email, password, timezone,
...(mnemonic ? { mnemonic } : {}),
});
// Returns { success: true, message } on 201
// Returns { error, errorType? } on failure
Flow 2: Password Login
Browser API
│ │
│ POST /user/login │
│ { username, password } │
│──────────────────────────────>│
│ │── validate fields
│ │── look up user by username
│ │── verify password hash
│ │── get energy balance
│ │── sign JWT
│ │
│ 200 { token, memberId, │
│ energyBalance } │
│<──────────────────────────────│
│ │
│── store token in localStorage │
│── store user in localStorage │
│── setUser() via AuthProvider │
The frontend login() function accepts an isEmail flag. When true, the identifier is sent as email instead of username.
Frontend code path
const response = await api.post('/user/login', {
[isEmail ? 'email' : 'username']: identifier,
password,
});
// On success: { token: response.data.token }
// Also calls setUser(response.data.user) if user data is present
localStorage.setItem('authToken', token);
Flow 3: ECIES Challenge Login (Passwordless)
This is a two-step flow. The client first requests a challenge, then signs it with the member’s private key.
Browser API
│ │
│ POST /user/request-direct- │
│ login │
│──────────────────────────────>│
│ │── generate timestamp (8 bytes)
│ │── generate nonce (32 bytes)
│ │── sign(timestamp ‖ nonce) with system key
│ │── concat: timestamp ‖ nonce ‖ signature
│ │
│ 200 { challenge, │
│ serverPublicKey } │
│<──────────────────────────────│
│ │
│── sign challenge with member │
│ private key │
│ │
│ POST /user/direct-challenge │
│ { challenge, signature, │
│ username } │
│──────────────────────────────>│
│ │── parse challenge buffer
│ │── verify timestamp not expired (<5min)
│ │── verify server signature
│ │── look up member
│ │── verify member signature
│ │── check nonce not reused (replay guard)
│ │── store nonce in UsedDirectLoginToken
│ │── sign JWT
│ │
│ 200 { token, user, │
│ serverPublicKey } │
│<──────────────────────────────│
│ │
│── store token + user │
Challenge buffer layout
Offset Length Content
0 8 Timestamp (big-endian uint64, ms since epoch)
8 32 Random nonce
40 64 Server ECIES signature over bytes [0..40)
Flow 4: App Load / Token Verification
When the app loads, the AuthProvider checks for a stored token and verifies it.
Browser API
│ │
│── read authToken from │
│ localStorage │
│ │
│ GET /user/verify │
│ Authorization: Bearer {tok} │
│──────────────────────────────>│
│ │── JWT middleware verifies token
│ │── build user DTO from decoded token
│ │
│ 200 { user } │
│<──────────────────────────────│
│ │
│── setUser(user) │
│── setIsAuthenticated(true) │
Frontend code path
const response = await api.get('/user/verify', {
headers: { Authorization: `Bearer ${token}` },
});
return response.data.user; // IRequestUser
If the token is expired or invalid, the frontend clears localStorage and shows the login page.
Flow 5: Authenticated API Requests
All authenticated requests use the authenticatedApi Axios instance, which is created by createAuthenticatedApiClient from express-suite-react-components. It automatically reads the token from localStorage and injects the Authorization: Bearer header.
import authenticatedApi from './authenticatedApi';
// Token is injected automatically
const response = await authenticatedApi.get('/user/profile');
The base api instance (used for unauthenticated calls like login/register) adds an Accept-Language header from localStorage.
Flow 6: Token Refresh
Browser API
│ │
│ GET /user/refresh-token │
│ Authorization: Bearer {tok} │
│──────────────────────────────>│
│ │── verify current JWT
│ │── re-sign with fresh expiry
│ │
│ 200 { token, user } │
│ Authorization: Bearer {new} │
│<──────────────────────────────│
│ │
│── update authToken in │
│ localStorage │
│── update user in localStorage │
│── setUser(updatedUser) │
Frontend code path
const refreshResponse = await authenticatedApi.get('/user/refresh-token');
const newToken = refreshResponse.headers['authorization'];
if (newToken?.startsWith('Bearer ')) {
localStorage.setItem('authToken', newToken.slice(7));
}
if (refreshResponse.data.user) {
localStorage.setItem('user', JSON.stringify(refreshResponse.data.user));
}
Flow 7: Password Change
Browser API
│ │
│ POST /user/change-password │
│ Authorization: Bearer {tok} │
│ { currentPassword, │
│ newPassword } │
│──────────────────────────────>│
│ │── validate fields
│ │── verify current password
│ │── hash new password
│ │── update member record
│ │
│ 200 { memberId, success } │
│<──────────────────────────────│
Flow 8: Account Recovery (Mnemonic)
Browser API
│ │
│ POST /user/recover │
│ { email, mnemonic, │
│ newPassword } │
│──────────────────────────────>│
│ │── validate fields
│ │── look up user by email
│ │── verify mnemonic
│ │── set new password
│ │
│ 200 { data } │
│<──────────────────────────────│
Returns 401 if the mnemonic is invalid or credentials don’t match.
Flow 9: Backup Code Recovery
Backup codes provide an alternative recovery path. Each user can generate 10 codes, encrypted with ECIES and hashed with Argon2id.
Browser API
│ │
│ POST /user/backup-codes │
│──────────────────────────────>│── generate 10 codes
│ 200 { backupCodes: [...] } │
│<──────────────────────────────│
│ │
│ (user stores codes safely) │
│ │
│ POST /user/recover-backup │
│ { backupCode, newPassword? } │
│──────────────────────────────>│── verify code against stored hashes
│ │── consume code (one-time use)
│ │── optionally set new password
│ 200 { codeCount: 9 } │
│<──────────────────────────────│
Additional backup code endpoints:
GET /user/backup-codes— returns{ codeCount }(remaining unused codes)PUT /user/backup-codes— regenerates all codes, invalidating old ones
Flow 10: Logout
Logout is both client-side and server-side. The server invalidates the session via BrightChainSessionAdapter.
Browser API
│ │
│ POST /user/logout │
│ Authorization: Bearer {tok} │
│──────────────────────────────>│
│ │── validate token via session adapter
│ │── delete session record
│ │
│ 200 { message: "Success" } │
│<──────────────────────────────│
│ │
│── localStorage.removeItem │
│ ('authToken') │
│── localStorage.removeItem │
│ ('user') │
│── setUser(null) │
│── navigate('/login') │
Flow 11: User Settings
Settings are user-facing preferences (timezone, dark mode, language, currency, direct challenge toggle). They are stored in the user_settings MongoDB collection, separate from the member store’s replication settings.
GET /user/settings— returns current preferencesPOST /user/settings— partial update (only provided fields change)
Security Summary
| Concern | Implementation |
|---|---|
| Password storage | bcrypt (10 salt rounds) |
| Token algorithm | HS256 (HMAC-SHA256) |
| Token expiry | 7 days |
| Token storage (client) | localStorage |
| Challenge expiry | 5 minutes |
| Replay prevention | Nonce stored in UsedDirectLoginToken |
| Backup codes | Argon2id hash + ECIES encryption |
| Session invalidation | Server-side via BrightChainSessionAdapter |
| RBAC | Middleware checks MemberType and role arrays |
| Request language | Accept-Language header from localStorage |
Token storage note
Tokens are stored in localStorage, which is accessible to any JavaScript running on the page (XSS risk). For production hardening, consider migrating to httpOnly cookies with SameSite=Strict.
Configuration
Environment Variables
JWT_SECRET=your-jwt-secret
MNEMONIC_HMAC_SECRET=your-hmac-secret
MNEMONIC_ENCRYPTION_KEY=your-encryption-key
Constants
ECIES.SIGNATURE_SIZE = 64; // bytes
LoginChallengeExpiration = 300000; // 5 minutes in ms
File Map
brightchain-node-express-suite/src/lib/
├── controllers/user.ts # Base controller (register, login, profile, settings, etc.)
├── services/auth.ts # BrightDbAuthService (register, login, token, recovery)
├── services/sessionAdapter.ts # BrightChainSessionAdapter (session management)
└── validation/userValidation.ts # Input validation
brightchain-api-lib/src/lib/
├── controllers/api/user.ts # Extended controller (direct-challenge, backup codes)
├── services/auth.ts # AuthService (ECIES challenge verification)
├── middlewares/authentication.ts # JWT + RBAC middleware
└── auth/
├── aclDocumentStore.ts
├── ecdsaNodeAuthenticator.ts
└── poolAclStore.ts
brightchain-react/src/
├── services/auth.ts # Frontend auth service
├── services/api.ts # Base Axios instance
├── services/authenticatedApi.ts # Token-injecting Axios instance
└── app/app.tsx # AuthProvider setup