mirror of
https://github.com/bckelley/cgm-remote-monitor.git
synced 2026-08-24 03:14:12 -05:00
Add comprehensive authorization and security documentation and test specifications
Introduces `authorization-security-spec.md` and `authorization-test-spec.md` to document API authentication requirements (API_SECRET, JWT, Shiro permissions, brute-force protection) and their corresponding test cases, including identification of coverage gaps. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 055897c4-83a5-4136-93d8-ff61ec1a77a8 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 48221946-c783-44b0-a3b8-0be79161ef82 Replit-Helium-Checkpoint-Created: true
This commit is contained in:
@@ -0,0 +1,490 @@
|
||||
# Authorization and Security Requirements Specification
|
||||
|
||||
**Document Version:** 1.0
|
||||
**Last Updated:** January 2026
|
||||
**Status:** Draft
|
||||
**Related Documents:** [Security Audit](../security-audit.md), [API Layer Audit](../api-layer-audit.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
This document formally specifies the authentication and authorization requirements for Nightscout's API system. It serves as a contract for:
|
||||
|
||||
1. **Client developers** - Understanding how to authenticate with Nightscout
|
||||
2. **Maintainers** - Preserving security behavior during refactoring
|
||||
3. **Testers** - Validating security behavior against formal requirements
|
||||
4. **Security auditors** - Understanding the expected security posture
|
||||
|
||||
---
|
||||
|
||||
## 2. Terminology
|
||||
|
||||
| Term | Definition |
|
||||
|------|------------|
|
||||
| **API_SECRET** | A shared secret (minimum 12 characters) used for admin-level authentication |
|
||||
| **API_SECRET Hash** | The SHA-1 or SHA-512 digest of the API_SECRET, transmitted instead of the raw secret |
|
||||
| **Access Token** | A subject-specific token derived from the subject name and a digest of the subject ID |
|
||||
| **JWT** | JSON Web Token signed with a dedicated JWT signing key for time-limited authentication |
|
||||
| **JWT Signing Key** | A separate key (loaded from `randomString` file or set via `setJWTKey`) used to sign/verify JWTs |
|
||||
| **Subject** | An entity (user, device, application) that can authenticate |
|
||||
| **Role** | A named collection of permissions that can be assigned to subjects |
|
||||
| **Permission** | An Apache Shiro-style string defining allowed actions (e.g., `api:entries:read`) |
|
||||
| **Shiro Trie** | A data structure for efficient wildcard permission matching |
|
||||
|
||||
---
|
||||
|
||||
## 3. Authentication Requirements
|
||||
|
||||
### 3.1 API_SECRET Authentication
|
||||
|
||||
#### REQ-AUTH-001: API_SECRET Minimum Length
|
||||
|
||||
The API_SECRET environment variable MUST be at least 12 characters long.
|
||||
|
||||
| Input | Expected Behavior | Requirement ID |
|
||||
|-------|-------------------|----------------|
|
||||
| `API_SECRET` ≥ 12 chars | Server starts, secret is valid | REQ-AUTH-001a |
|
||||
| `API_SECRET` < 12 chars | Server logs error, secret is null | REQ-AUTH-001b |
|
||||
| `API_SECRET` not set | Server runs with limited functionality | REQ-AUTH-001c |
|
||||
|
||||
**Implementation Reference:** `lib/server/env.js`
|
||||
|
||||
```javascript
|
||||
if (readENV('API_SECRET').length < consts.MIN_PASSPHRASE_LENGTH) {
|
||||
env.err.push({desc: 'API_SECRET should be at least ' + consts.MIN_PASSPHRASE_LENGTH + ' characters'});
|
||||
}
|
||||
```
|
||||
|
||||
#### REQ-AUTH-002: API_SECRET Transmission
|
||||
|
||||
The API_SECRET MUST be transmitted as a hash (SHA-1 or SHA-512), never in plaintext.
|
||||
|
||||
| Method | Header/Parameter | Format | Requirement ID |
|
||||
|--------|------------------|--------|----------------|
|
||||
| Header | `api-secret` | SHA-1 (40 chars) or SHA-512 (128 chars) hex string | REQ-AUTH-002a |
|
||||
| Query | `?secret=` | SHA-1 (40 chars) or SHA-512 (128 chars) hex string | REQ-AUTH-002b |
|
||||
| Body | `body.secret` | SHA-1 (40 chars) or SHA-512 (128 chars) hex string | REQ-AUTH-002c |
|
||||
|
||||
**Example (SHA-1):**
|
||||
```
|
||||
API_SECRET: "this is my long pass phrase"
|
||||
SHA-1 Hash: "b723e97aa97846eb92d5264f084b2823f57c4aa1"
|
||||
```
|
||||
|
||||
**Note:** Either SHA-1 or SHA-512 hashes are accepted per REQ-AUTH-003.
|
||||
|
||||
#### REQ-AUTH-003: API_SECRET Hash Algorithms
|
||||
|
||||
The system MUST accept both SHA-1 and SHA-512 hashes of the API_SECRET.
|
||||
|
||||
| Algorithm | Hash Length | Requirement ID |
|
||||
|-----------|-------------|----------------|
|
||||
| SHA-1 | 40 hex chars | REQ-AUTH-003a |
|
||||
| SHA-512 | 128 hex chars | REQ-AUTH-003b |
|
||||
|
||||
**Rationale:** SHA-512 support provides a migration path to stronger hashing.
|
||||
|
||||
#### REQ-AUTH-004: API_SECRET Authorization Level
|
||||
|
||||
A valid API_SECRET grants full admin permissions (`*`).
|
||||
|
||||
**Shiro Permission:** `*` (all permissions)
|
||||
|
||||
---
|
||||
|
||||
### 3.2 Access Token Authentication
|
||||
|
||||
#### REQ-AUTH-010: Access Token Format
|
||||
|
||||
Access tokens MUST be derived from the subject name and a digest computed by the enclave.
|
||||
|
||||
**Format:** `{abbreviation}-{digest_prefix}`
|
||||
|
||||
| Component | Description | Requirement ID |
|
||||
|-----------|-------------|----------------|
|
||||
| Abbreviation | First 10 alphanumeric chars of subject name, lowercase | REQ-AUTH-010a |
|
||||
| Digest Prefix | First 16 chars of digest from `enclave.getSubjectHash(subject._id)` | REQ-AUTH-010b |
|
||||
|
||||
**Implementation Detail:** The digest is computed as SHA-1 of `apiKeySHA1 + subject._id`, where `apiKeySHA1` is the SHA-1 hash of the API_SECRET. This double-hashing provides an additional layer of indirection.
|
||||
|
||||
**Reference:** `lib/server/enclave.js:getSubjectHash()`
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Subject Name: "Loop App"
|
||||
Access Token: "loopapp-a1b2c3d4e5f6g7h8"
|
||||
```
|
||||
|
||||
#### REQ-AUTH-011: Access Token Locations
|
||||
|
||||
Access tokens MAY be provided in the following locations:
|
||||
|
||||
| Location | Priority | Requirement ID |
|
||||
|----------|----------|----------------|
|
||||
| `api-secret` header | 1 (checked if not API_SECRET hash) | REQ-AUTH-011a |
|
||||
| `?token=` query parameter | 2 | REQ-AUTH-011b |
|
||||
| `body.token` | 3 | REQ-AUTH-011c |
|
||||
|
||||
#### REQ-AUTH-012: Access Token Resolution
|
||||
|
||||
When a valid access token is provided, the system MUST:
|
||||
|
||||
1. Locate the corresponding subject
|
||||
2. Retrieve the subject's roles
|
||||
3. Merge subject roles with default roles
|
||||
4. Return combined permissions
|
||||
|
||||
---
|
||||
|
||||
### 3.3 JWT Authentication
|
||||
|
||||
#### REQ-AUTH-020: JWT Generation
|
||||
|
||||
The system MUST generate JWTs when a valid access token is provided to the authorization endpoint.
|
||||
|
||||
| Endpoint | Method | Input | Output | Requirement ID |
|
||||
|----------|--------|-------|--------|----------------|
|
||||
| `/api/v2/authorization/request/{token}` | GET | Access token | JWT | REQ-AUTH-020a |
|
||||
|
||||
**JWT Payload:**
|
||||
```json
|
||||
{
|
||||
"accessToken": "subject-access-token",
|
||||
"iat": 1705000000,
|
||||
"exp": 1705003600
|
||||
}
|
||||
```
|
||||
|
||||
#### REQ-AUTH-021: JWT Signature
|
||||
|
||||
JWTs MUST be signed using HMAC-SHA256 with a dedicated JWT signing key.
|
||||
|
||||
**Implementation Detail:** The signing key is stored in `secrets[jwtKey]` and is loaded from a `randomString` file in the cache directory, or can be set via `env.enclave.setJWTKey()`. This is separate from the API_SECRET.
|
||||
|
||||
**Reference:** `lib/server/enclave.js:signJWT()`, `lib/server/enclave.js:readKey()`
|
||||
|
||||
#### REQ-AUTH-022: JWT Expiration
|
||||
|
||||
JWTs MUST have an expiration time. Default: 8 hours.
|
||||
|
||||
**Implementation Detail:** The default lifetime is `'8h'` as defined in `enclave.signJWT()`. This can be overridden by passing a custom lifetime parameter.
|
||||
|
||||
**Reference:** `lib/server/enclave.js:58`
|
||||
|
||||
#### REQ-AUTH-023: JWT Validation
|
||||
|
||||
When a JWT is provided, the system MUST:
|
||||
|
||||
1. Verify the signature using the JWT signing key (same key used in REQ-AUTH-021)
|
||||
2. Check expiration time
|
||||
3. Extract the access token from payload
|
||||
4. Resolve permissions via access token
|
||||
|
||||
**Reference:** `lib/server/enclave.js:verifyJWT()`
|
||||
|
||||
#### REQ-AUTH-024: JWT Transmission
|
||||
|
||||
JWTs MUST be transmitted via the `Authorization` header.
|
||||
|
||||
**Format:** `Authorization: Bearer {jwt}`
|
||||
|
||||
| Input | Expected Behavior | Requirement ID |
|
||||
|-------|-------------------|----------------|
|
||||
| Valid JWT | Extract access token, resolve permissions | REQ-AUTH-024a |
|
||||
| Expired JWT | Return 401 Unauthorized | REQ-AUTH-024b |
|
||||
| Invalid signature | Return 401 Unauthorized | REQ-AUTH-024c |
|
||||
| Malformed JWT | Return 401 Unauthorized | REQ-AUTH-024d |
|
||||
|
||||
---
|
||||
|
||||
## 4. Authorization Requirements
|
||||
|
||||
### 4.1 Role-Based Access Control
|
||||
|
||||
#### REQ-AUTHZ-001: Default Roles
|
||||
|
||||
The system MUST provide the following built-in roles:
|
||||
|
||||
| Role Name | Permissions | Description | Requirement ID |
|
||||
|-----------|-------------|-------------|----------------|
|
||||
| `admin` | `*` | Full access | REQ-AUTHZ-001a |
|
||||
| `denied` | (none) | No permissions | REQ-AUTHZ-001b |
|
||||
| `status-only` | `api:status:read` | Read status only | REQ-AUTHZ-001c |
|
||||
| `readable` | `*:*:read` | Read all data | REQ-AUTHZ-001d |
|
||||
| `careportal` | `api:treatments:create` | Create treatments | REQ-AUTHZ-001e |
|
||||
| `devicestatus-upload` | `api:devicestatus:create` | Upload device status | REQ-AUTHZ-001f |
|
||||
| `activity` | `api:activity:create` | Create activity records | REQ-AUTHZ-001g |
|
||||
|
||||
#### REQ-AUTHZ-002: Custom Roles
|
||||
|
||||
Administrators MUST be able to create custom roles with arbitrary permission sets.
|
||||
|
||||
**Storage:** MongoDB collection `auth_roles`
|
||||
|
||||
#### REQ-AUTHZ-003: Default Permissions
|
||||
|
||||
Unauthenticated requests MUST receive permissions based on `AUTH_DEFAULT_ROLES` environment variable.
|
||||
|
||||
| Setting | Effect | Requirement ID |
|
||||
|---------|--------|----------------|
|
||||
| `readable` | Unauthenticated can read all data | REQ-AUTHZ-003a |
|
||||
| `denied` | Unauthenticated have no permissions | REQ-AUTHZ-003b |
|
||||
| Comma-separated roles | Merge permissions from listed roles | REQ-AUTHZ-003c |
|
||||
|
||||
### 4.2 Shiro Permission Model
|
||||
|
||||
#### REQ-AUTHZ-010: Permission Format
|
||||
|
||||
Permissions MUST follow the Apache Shiro format: `domain:action:instance`
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
api:entries:read - Read entries via API
|
||||
api:treatments:create - Create treatments
|
||||
api:*:* - All API operations
|
||||
* - Full admin access
|
||||
```
|
||||
|
||||
#### REQ-AUTHZ-011: Wildcard Matching
|
||||
|
||||
The permission system MUST support wildcard matching at any level.
|
||||
|
||||
| Pattern | Matches | Requirement ID |
|
||||
|---------|---------|----------------|
|
||||
| `*` | All permissions | REQ-AUTHZ-011a |
|
||||
| `api:*:*` | All API operations | REQ-AUTHZ-011b |
|
||||
| `api:entries:*` | All entry operations | REQ-AUTHZ-011c |
|
||||
| `*:*:read` | All read operations | REQ-AUTHZ-011d |
|
||||
|
||||
#### REQ-AUTHZ-012: Permission Checking
|
||||
|
||||
Permission checks MUST use the Shiro Trie data structure for efficient wildcard matching.
|
||||
|
||||
---
|
||||
|
||||
## 5. Brute-Force Protection Requirements
|
||||
|
||||
### 5.1 IP-Based Delay List
|
||||
|
||||
#### REQ-BRUTE-001: Failed Authentication Tracking
|
||||
|
||||
The system MUST track failed authentication attempts by IP address.
|
||||
|
||||
**Implementation Reference:** `lib/authorization/delaylist.js`
|
||||
|
||||
#### REQ-BRUTE-002: Progressive Delay
|
||||
|
||||
After a failed authentication attempt, subsequent requests from the same IP MUST be delayed.
|
||||
|
||||
| Parameter | Default Value | Configurable | Requirement ID |
|
||||
|-----------|---------------|--------------|----------------|
|
||||
| Delay per failure | 5000ms | Yes (`settings.authFailDelay`) | REQ-BRUTE-002a |
|
||||
| Delay accumulation | Cumulative | No | REQ-BRUTE-002b |
|
||||
| Max delay | No limit | No | REQ-BRUTE-002c |
|
||||
|
||||
**Behavior:**
|
||||
```
|
||||
1st failure: 5 second delay
|
||||
2nd failure: 10 second delay (cumulative)
|
||||
3rd failure: 15 second delay (cumulative)
|
||||
...
|
||||
```
|
||||
|
||||
#### REQ-BRUTE-003: Delay Expiration
|
||||
|
||||
Failed request entries SHOULD be cleaned up after a period of inactivity.
|
||||
|
||||
| Parameter | Value | Requirement ID |
|
||||
|-----------|-------|----------------|
|
||||
| Expiration age | 60 seconds after last delay (`FAIL_AGE`) | REQ-BRUTE-003a |
|
||||
| Cleanup mechanism | One-shot setTimeout after 30 seconds | REQ-BRUTE-003b |
|
||||
|
||||
**Implementation Note:** The current implementation uses a single `setTimeout(30000)` call at module initialization to clean up entries older than `FAIL_AGE` (60 seconds). This is a one-shot cleanup, not a recurring interval. Entries created after the cleanup runs may persist until server restart. This is a known limitation.
|
||||
|
||||
**Reference:** `lib/authorization/delaylist.js:45-53`
|
||||
|
||||
#### REQ-BRUTE-004: Successful Authentication Clears Delay
|
||||
|
||||
A successful authentication MUST immediately clear the delay for that IP.
|
||||
|
||||
#### REQ-BRUTE-005: Failed Authentication Notification
|
||||
|
||||
Failed authentication attempts MUST trigger an admin notification.
|
||||
|
||||
**Notification Content:**
|
||||
- Title: "Failed authentication"
|
||||
- Message: IP address and warning about potential misconfiguration
|
||||
|
||||
---
|
||||
|
||||
## 6. Subject Management Requirements
|
||||
|
||||
### 6.1 Subject CRUD Operations
|
||||
|
||||
#### REQ-SUBJ-001: Subject Creation
|
||||
|
||||
Subjects MUST be creatable via the admin API.
|
||||
|
||||
**Required Fields:**
|
||||
| Field | Type | Description | Requirement ID |
|
||||
|-------|------|-------------|----------------|
|
||||
| `name` | String | Display name for the subject | REQ-SUBJ-001a |
|
||||
| `roles` | Array | List of role names assigned | REQ-SUBJ-001b |
|
||||
|
||||
**Auto-generated Fields:**
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `_id` | ObjectID | Unique identifier |
|
||||
| `created_at` | ISO 8601 | Creation timestamp |
|
||||
| `accessToken` | String | Generated access token |
|
||||
| `digest` | String | Token digest for matching |
|
||||
|
||||
#### REQ-SUBJ-002: Subject Modification
|
||||
|
||||
Subjects MUST be modifiable via the admin API.
|
||||
|
||||
#### REQ-SUBJ-003: Subject Deletion
|
||||
|
||||
Subjects MUST be deletable via the admin API.
|
||||
|
||||
**Behavior:** Once a subject is deleted, its access token becomes unusable because the subject lookup will fail. There is no explicit token revocation mechanism; invalidation occurs because the subject record no longer exists in the database.
|
||||
|
||||
**Note:** Existing JWTs containing the deleted subject's access token will fail on the next permission resolution when the subject cannot be found.
|
||||
|
||||
### 6.2 Role Management
|
||||
|
||||
#### REQ-ROLE-001: Role Creation
|
||||
|
||||
Custom roles MUST be creatable via the admin API.
|
||||
|
||||
**Required Fields:**
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `name` | String | Unique role name |
|
||||
| `permissions` | Array | List of Shiro-format permissions |
|
||||
|
||||
#### REQ-ROLE-002: Built-in Role Behavior
|
||||
|
||||
Built-in roles (admin, denied, readable, etc.) are defined in code and merged with database roles at runtime.
|
||||
|
||||
**Note:** The current implementation does not explicitly protect built-in roles from deletion attempts via the admin API. Deleting a built-in role from the database has no effect since the role is re-added from `storage.defaultRoles` on reload. This behavior is implementation-specific and may change.
|
||||
|
||||
---
|
||||
|
||||
## 7. Socket.IO Authentication Requirements
|
||||
|
||||
### 7.1 Storage Namespace
|
||||
|
||||
#### REQ-SOCK-001: Subscription Authentication
|
||||
|
||||
The `/storage` WebSocket namespace MUST require authentication for data subscriptions.
|
||||
|
||||
**Message Format:**
|
||||
```javascript
|
||||
socket.emit('subscribe', {
|
||||
accessToken: 'subject-access-token',
|
||||
collections: ['entries', 'treatments']
|
||||
});
|
||||
```
|
||||
|
||||
#### REQ-SOCK-002: Per-Collection Authorization
|
||||
|
||||
Access to collections via WebSocket MUST respect the subject's permissions.
|
||||
|
||||
| Permission | Required For |
|
||||
|------------|--------------|
|
||||
| `api:entries:read` | Subscribe to entries |
|
||||
| `api:treatments:read` | Subscribe to treatments |
|
||||
| `api:treatments:create` | dbAdd to treatments |
|
||||
|
||||
### 7.2 Alarm Namespace
|
||||
|
||||
#### REQ-SOCK-010: Alarm Subscription
|
||||
|
||||
The `/alarm` namespace MUST require a valid access token for subscription.
|
||||
|
||||
---
|
||||
|
||||
## 8. Error Handling Requirements
|
||||
|
||||
### 8.1 Authentication Errors
|
||||
|
||||
#### REQ-ERR-001: Unauthorized Response
|
||||
|
||||
Invalid authentication MUST return HTTP 401 Unauthorized.
|
||||
|
||||
**Response Format:**
|
||||
```json
|
||||
{
|
||||
"status": 401,
|
||||
"message": "Unauthorized"
|
||||
}
|
||||
```
|
||||
|
||||
#### REQ-ERR-002: Forbidden Response
|
||||
|
||||
Valid authentication with insufficient permissions MUST return HTTP 403 Forbidden.
|
||||
|
||||
---
|
||||
|
||||
## 9. Traceability Matrix
|
||||
|
||||
| Requirement | Test File | Test Case | Status |
|
||||
|-------------|-----------|-----------|--------|
|
||||
| REQ-AUTH-001a | `security.test.js` | "should work fine set" | Covered |
|
||||
| REQ-AUTH-001b | `security.test.js` | "should not work short" | Covered |
|
||||
| REQ-AUTH-002a | `security.test.js` | "should work fine set" | Covered |
|
||||
| REQ-AUTH-003a | `verifyauth.test.js` | SHA-1 verification | Covered |
|
||||
| REQ-AUTH-003b | `verifyauth.test.js` | SHA-512 verification | Covered |
|
||||
| REQ-AUTH-011a | `api.security.test.js` | "Data load should succeed with token in place of a secret" | Covered |
|
||||
| REQ-AUTH-011b | `api.security.test.js` | "Data load should succeed with GET token" | Covered |
|
||||
| REQ-AUTH-020a | `api.security.test.js` | "Should return a JWT on token" | Covered |
|
||||
| REQ-AUTH-024a | `api.security.test.js` | "Data load should succeed with a bearer token" | Covered |
|
||||
| REQ-AUTH-024c | `api.security.test.js` | "Data load fail succeed with a false bearer token" | Covered |
|
||||
| REQ-AUTHZ-003b | `api.security.test.js` | "Data load should fail unauthenticated" | Covered |
|
||||
| REQ-BRUTE-002 | `verifyauth.test.js` | "should fail unauthorized and delay subsequent attempts" | Covered |
|
||||
| REQ-BRUTE-004 | Implicit in `verifyauth.test.js` | Successful auth clears delay | Implicit |
|
||||
| REQ-SOCK-001 | N/A | WebSocket subscription auth | Not Covered |
|
||||
| REQ-SOCK-002 | N/A | Per-collection authorization | Not Covered |
|
||||
| REQ-SUBJ-001 | N/A | Subject creation | Not Covered |
|
||||
| REQ-ROLE-001 | N/A | Role creation | Not Covered |
|
||||
|
||||
---
|
||||
|
||||
## 10. Coverage Gaps and Recommendations
|
||||
|
||||
### 10.1 Identified Gaps
|
||||
|
||||
| Gap | Priority | Recommendation |
|
||||
|-----|----------|----------------|
|
||||
| WebSocket authentication testing | High | Add tests for `/storage` and `/alarm` subscription auth |
|
||||
| Subject/Role CRUD testing | Medium | Add API tests for admin tools endpoints |
|
||||
| JWT expiration testing | Medium | Add test for expired JWT rejection |
|
||||
| Permission wildcard testing | Low | Add comprehensive Shiro pattern tests |
|
||||
|
||||
### 10.2 Future Enhancements
|
||||
|
||||
| Enhancement | Description | Priority |
|
||||
|-------------|-------------|----------|
|
||||
| Token expiration | Add expiration to access tokens | Medium |
|
||||
| Refresh tokens | Add JWT refresh mechanism | Low |
|
||||
| Audit logging | Log all auth events for compliance | Medium |
|
||||
| OIDC support | External identity provider integration | Low |
|
||||
|
||||
---
|
||||
|
||||
## 11. Version History
|
||||
|
||||
| Version | Date | Author | Changes |
|
||||
|---------|------|--------|---------|
|
||||
| 1.0 | January 2026 | Nightscout Team | Initial specification |
|
||||
|
||||
---
|
||||
|
||||
## 12. References
|
||||
|
||||
- [Security Audit](../security-audit.md) - Security analysis and recommendations
|
||||
- [API Layer Audit](../api-layer-audit.md) - API endpoint inventory
|
||||
- [Modernization Roadmap](../modernization-roadmap.md) - OIDC/OAuth2 plans
|
||||
- `lib/authorization/` - Implementation source code
|
||||
@@ -0,0 +1,484 @@
|
||||
# Authorization and Security Test Specification
|
||||
|
||||
**Document Version:** 1.0
|
||||
**Last Updated:** January 2026
|
||||
**Status:** Draft
|
||||
**Related Requirements:** [Authorization Security Spec](../requirements/authorization-security-spec.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
This document specifies the test cases for validating authentication and authorization behavior across Nightscout's API, WebSocket, and client-side layers. Each test case is linked to a formal requirement and mapped to actual test implementations.
|
||||
|
||||
---
|
||||
|
||||
## 2. Test Suite Overview
|
||||
|
||||
### 2.1 Test Files
|
||||
|
||||
| File | Purpose | Test Count |
|
||||
|------|---------|------------|
|
||||
| `tests/security.test.js` | API_SECRET validation and basic auth | 3 |
|
||||
| `tests/hashauth.test.js` | Client-side hash authentication | 4 |
|
||||
| `tests/verifyauth.test.js` | Auth verification endpoint and brute-force delay | 4 |
|
||||
| `tests/api.security.test.js` | JWT, Bearer tokens, role-based access | 10 |
|
||||
| **Total** | | **21** |
|
||||
|
||||
### 2.2 Test Execution
|
||||
|
||||
```bash
|
||||
# Run all security/auth tests
|
||||
npm test -- --grep "API_SECRET\|Security\|hashauth\|verifyauth"
|
||||
|
||||
# Run specific suite
|
||||
npm test -- --grep "API_SECRET"
|
||||
npm test -- --grep "Security of REST API V1"
|
||||
npm test -- --grep "hashauth"
|
||||
npm test -- --grep "verifyauth"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. API_SECRET Test Cases
|
||||
|
||||
### 3.1 Security Test Suite (`tests/security.test.js`)
|
||||
|
||||
| Test ID | Test Case | Requirement | Expected Result |
|
||||
|---------|-----------|-------------|-----------------|
|
||||
| SEC-001 | Should fail when unauthorized | REQ-ERR-001 | 401 Unauthorized |
|
||||
| SEC-002 | Should work fine set | REQ-AUTH-001a, REQ-AUTH-004 | 200 OK (valid hash grants admin) |
|
||||
| SEC-003 | Should not work short | REQ-AUTH-001b | API_SECRET null, error logged |
|
||||
|
||||
#### Test Case Details
|
||||
|
||||
**SEC-001: Should fail when unauthorized**
|
||||
```javascript
|
||||
it('should fail when unauthorized', function(done) {
|
||||
var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1';
|
||||
delete process.env.API_SECRET;
|
||||
process.env.API_SECRET = 'this is my long pass phrase';
|
||||
var env = require('../lib/server/env')();
|
||||
|
||||
env.enclave.isApiKey(known).should.equal(true);
|
||||
|
||||
setup_app(env, function(ctx) {
|
||||
ctx.app.enabled('api').should.equal(true);
|
||||
ctx.app.api_secret = '';
|
||||
ping_authorized_endpoint(ctx.app, 401, done);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**SEC-002: Should work fine set**
|
||||
```javascript
|
||||
it('should work fine set', function(done) {
|
||||
var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1';
|
||||
delete process.env.API_SECRET;
|
||||
process.env.API_SECRET = 'this is my long pass phrase';
|
||||
var env = require('../lib/server/env')();
|
||||
|
||||
env.enclave.isApiKey(known).should.equal(true);
|
||||
|
||||
setup_app(env, function(ctx) {
|
||||
ctx.app.enabled('api').should.equal(true);
|
||||
ctx.app.api_secret = known;
|
||||
ping_authorized_endpoint(ctx.app, 200, done);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**SEC-003: Should not work short**
|
||||
```javascript
|
||||
it('should not work short', function() {
|
||||
delete process.env.API_SECRET;
|
||||
process.env.API_SECRET = 'tooshort';
|
||||
var env = require('../lib/server/env')();
|
||||
|
||||
should.not.exist(env.api_secret);
|
||||
env.err[0].desc.should.startWith('API_SECRET should be at least');
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Client-Side Hash Authentication Test Cases
|
||||
|
||||
### 4.1 Hashauth Test Suite (`tests/hashauth.test.js`)
|
||||
|
||||
| Test ID | Test Case | Requirement | Expected Result |
|
||||
|---------|-----------|-------------|-----------------|
|
||||
| HASH-001 | Should make module unauthorized | N/A (UI state) | Status shows "Unauthorized" |
|
||||
| HASH-002 | Should make module authorized | N/A (UI state) | Status shows "Admin authorized" |
|
||||
| HASH-003 | Should store hash and remove authentication | REQ-AUTH-002a (client-side) | Hash matches expected SHA-1 |
|
||||
| HASH-004 | Should not store hash | REQ-AUTH-002a (client-side) | Hash computed but not persisted |
|
||||
| HASH-005 | Should report secret too short | REQ-AUTH-001b (client validation) | Alert shows "Too short API secret" |
|
||||
|
||||
**Note:** These tests validate client-side hash computation and UI state management, not server-side authentication. They verify that the client correctly hashes the API_SECRET before transmission.
|
||||
|
||||
#### Test Case Details
|
||||
|
||||
**HASH-003: Should store hash and then remove authentication**
|
||||
```javascript
|
||||
it('should store hash and the remove authentication', function () {
|
||||
var client = require('../lib/client');
|
||||
var hashauth = require('../lib/client/hashauth');
|
||||
var localStorage = require('./fixtures/localstorage');
|
||||
|
||||
localStorage.remove('apisecrethash');
|
||||
|
||||
hashauth.init(client,$);
|
||||
hashauth.verifyAuthentication = function mockVerifyAuthentication(next) {
|
||||
hashauth.authenticated = true;
|
||||
next(true);
|
||||
};
|
||||
hashauth.updateSocketAuth = function mockUpdateSocketAuth() {};
|
||||
|
||||
client.init();
|
||||
|
||||
hashauth.processSecret('this is my long pass phrase', true);
|
||||
|
||||
hashauth.hash().should.equal('b723e97aa97846eb92d5264f084b2823f57c4aa1');
|
||||
localStorage.get('apisecrethash').should.equal('b723e97aa97846eb92d5264f084b2823f57c4aa1');
|
||||
hashauth.isAuthenticated().should.equal(true);
|
||||
|
||||
hashauth.removeAuthentication();
|
||||
hashauth.isAuthenticated().should.equal(false);
|
||||
});
|
||||
```
|
||||
|
||||
#### Known Testing Quirks
|
||||
|
||||
**Browser Environment Simulation:**
|
||||
- Tests use `benv` package to simulate browser DOM
|
||||
- `headless.js` fixture provides secure jsdom harness
|
||||
- Tests mock `localStorage`, `window.alert`, and jQuery plugins
|
||||
|
||||
**Network Isolation:**
|
||||
- `mockAjax: true` prevents actual network requests
|
||||
- `verifyAuthentication` is mocked to control auth state
|
||||
|
||||
---
|
||||
|
||||
## 5. Verification Endpoint Test Cases
|
||||
|
||||
### 5.1 Verifyauth Test Suite (`tests/verifyauth.test.js`)
|
||||
|
||||
| Test ID | Test Case | Requirement | Expected Result |
|
||||
|---------|-----------|-------------|-----------------|
|
||||
| VERIFY-001 | Should return defaults when called without secret | REQ-AUTHZ-003 | 200 OK with default permissions |
|
||||
| VERIFY-002 | Should fail when calling with wrong secret | REQ-ERR-001 | Message: "UNAUTHORIZED" |
|
||||
| VERIFY-003 | Should fail unauthorized and delay subsequent attempts | REQ-BRUTE-002 | Progressive delay > 49ms |
|
||||
| VERIFY-004 | Should work fine authorized | REQ-AUTH-002a | 200 OK |
|
||||
|
||||
#### Test Case Details
|
||||
|
||||
**VERIFY-003: Should fail unauthorized and delay subsequent attempts**
|
||||
```javascript
|
||||
it('should fail unauthorized and delay subsequent attempts', function (done) {
|
||||
var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1';
|
||||
delete process.env.API_SECRET;
|
||||
process.env.API_SECRET = 'this is my long pass phrase';
|
||||
var env = require('../lib/server/env')();
|
||||
|
||||
env.enclave.isApiKey(known).should.equal(true);
|
||||
|
||||
setup_app(env, function (ctx) {
|
||||
ctx.app.enabled('api').should.equal(true);
|
||||
ctx.app.api_secret = 'wrong secret';
|
||||
const time = Date.now();
|
||||
|
||||
function checkTimer(res) {
|
||||
res.body.message.message.should.equal('UNAUTHORIZED');
|
||||
const delta = Date.now() - time;
|
||||
delta.should.be.greaterThan(49);
|
||||
done();
|
||||
}
|
||||
|
||||
function pingAgain (res) {
|
||||
res.body.message.message.should.equal('UNAUTHORIZED');
|
||||
ping_authorized_endpoint(ctx.app, 200, checkTimer, true);
|
||||
}
|
||||
|
||||
ping_authorized_endpoint(ctx.app, 200, pingAgain, true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### Brute-Force Protection Verification
|
||||
|
||||
This test validates the cumulative delay behavior:
|
||||
1. First failed attempt records the IP
|
||||
2. Second failed attempt experiences delay
|
||||
3. Total time between first and third request should exceed configured delay
|
||||
|
||||
**Configuration Note:** Tests use `settings.authFailDelay` which can be configured for faster test execution while maintaining realistic behavior in production.
|
||||
|
||||
---
|
||||
|
||||
## 6. REST API Security Test Cases
|
||||
|
||||
### 6.1 API Security Test Suite (`tests/api.security.test.js`)
|
||||
|
||||
| Test ID | Test Case | Requirement | Expected Result |
|
||||
|---------|-----------|-------------|-----------------|
|
||||
| APISEC-001 | Should fail on false token | REQ-ERR-001 | 401 Unauthorized |
|
||||
| APISEC-002 | Data load should fail unauthenticated | REQ-AUTHZ-003b | 401 Unauthorized |
|
||||
| APISEC-003 | Should return a JWT on token | REQ-AUTH-020a | Valid JWT with iat, exp |
|
||||
| APISEC-004 | Should return JWT with default roles on broken role token | REQ-AUTHZ-003c | JWT issued with default roles |
|
||||
| APISEC-005 | Data load should succeed with API SECRET | REQ-AUTH-002a | 200 OK |
|
||||
| APISEC-006 | Data load should succeed with GET token | REQ-AUTH-011b | 200 OK |
|
||||
| APISEC-007 | Data load should succeed with token in place of a secret | REQ-AUTH-011a | 200 OK |
|
||||
| APISEC-008 | Data load should succeed with a bearer token | REQ-AUTH-024a | 200 OK |
|
||||
| APISEC-009 | Data load fail with a false bearer token | REQ-AUTH-024c | 401 Unauthorized |
|
||||
| APISEC-010 | /verifyauth should return OK for Bearer tokens | REQ-AUTH-024a | message: "OK", isAdmin: true |
|
||||
|
||||
#### Test Case Details
|
||||
|
||||
**APISEC-003: Should return a JWT on token**
|
||||
```javascript
|
||||
it('Should return a JWT on token', function(done) {
|
||||
const now = Math.round(Date.now() / 1000) - 1;
|
||||
request(self.app)
|
||||
.get('/api/v2/authorization/request/' + self.token.read)
|
||||
.expect(200)
|
||||
.end(function(err, res) {
|
||||
const decodedToken = jwt.decode(res.body.token);
|
||||
decodedToken.accessToken.should.equal(self.token.read);
|
||||
decodedToken.iat.should.be.aboveOrEqual(now);
|
||||
decodedToken.exp.should.be.above(decodedToken.iat);
|
||||
done();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**APISEC-008: Data load should succeed with a bearer token**
|
||||
```javascript
|
||||
it('Data load should succeed with a bearer token', function(done) {
|
||||
request(self.app)
|
||||
.get('/api/v2/authorization/request/' + self.token.read)
|
||||
.expect(200)
|
||||
.end(function(err, res) {
|
||||
const token = res.body.token;
|
||||
request(self.app)
|
||||
.get('/api/v1/entries.json')
|
||||
.set('Authorization', 'Bearer ' + token)
|
||||
.expect(200)
|
||||
.end(function(err, res) {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### Test Setup Notes
|
||||
|
||||
**authSubject Fixture:**
|
||||
The `tests/fixtures/api3/authSubject.js` fixture creates test subjects with various permission levels:
|
||||
- `read` - Read-only access
|
||||
- `noneSubject` - No specific roles (tests default role behavior)
|
||||
- `adminAll` - Full admin access
|
||||
|
||||
**Environment Configuration:**
|
||||
```javascript
|
||||
self.env.settings.authDefaultRoles = 'denied';
|
||||
```
|
||||
Tests explicitly set `denied` as default to ensure unauthenticated access is blocked.
|
||||
|
||||
---
|
||||
|
||||
## 7. Test Environment Setup
|
||||
|
||||
### 7.1 Prerequisites
|
||||
|
||||
```javascript
|
||||
before(function(done) {
|
||||
var api = require('../lib/api/');
|
||||
delete process.env.API_SECRET;
|
||||
process.env.API_SECRET = 'this is my long pass phrase';
|
||||
self.env = require('../lib/server/env')();
|
||||
self.env.settings.authDefaultRoles = 'denied';
|
||||
|
||||
require('../lib/server/bootevent')(self.env, language).boot(async function booted (ctx) {
|
||||
self.app.use('/api/v1', api(self.env, ctx));
|
||||
self.app.use('/api/v2/authorization', ctx.authorization.endpoints);
|
||||
|
||||
let authResult = await authSubject(ctx.authorization.storage);
|
||||
self.subject = authResult.subject;
|
||||
self.token = authResult.accessToken;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 7.2 Common Test Patterns
|
||||
|
||||
**API_SECRET Hash:**
|
||||
```javascript
|
||||
var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1';
|
||||
// SHA-1 hash of 'this is my long pass phrase'
|
||||
```
|
||||
|
||||
**SHA-512 Hash (also accepted):**
|
||||
```javascript
|
||||
var known512 = '8c8743d38cbe00debe4b3ba8d0ffbb85e4716c982a61bb9e57bab203178e3718b2965831c1a5e42b9da16f082fdf8a6cecf993b49ed67e3a8b1cd475885d8070';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Coverage Matrix
|
||||
|
||||
| Requirement | Test ID(s) | Status |
|
||||
|-------------|------------|--------|
|
||||
| REQ-AUTH-001a | SEC-002 | Covered |
|
||||
| REQ-AUTH-001b | SEC-003, HASH-005 | Covered |
|
||||
| REQ-AUTH-002a | VERIFY-004, APISEC-005, HASH-003 (client) | Covered |
|
||||
| REQ-AUTH-003a | VERIFY-001 (implicit) | Covered |
|
||||
| REQ-AUTH-003b | VERIFY-001 | Covered |
|
||||
| REQ-AUTH-004 | SEC-002 | Covered |
|
||||
| REQ-AUTH-011a | APISEC-007 | Covered |
|
||||
| REQ-AUTH-011b | APISEC-006 | Covered |
|
||||
| REQ-AUTH-020a | APISEC-003 | Covered |
|
||||
| REQ-AUTH-024a | APISEC-008, APISEC-010 | Covered |
|
||||
| REQ-AUTH-024c | APISEC-009 | Covered |
|
||||
| REQ-AUTHZ-001 | N/A | Not Covered |
|
||||
| REQ-AUTHZ-003b | APISEC-002 | Covered |
|
||||
| REQ-AUTHZ-010 | N/A | Not Covered |
|
||||
| REQ-AUTHZ-011 | N/A | Not Covered |
|
||||
| REQ-BRUTE-002 | VERIFY-003 | Covered |
|
||||
| REQ-BRUTE-004 | Implicit in VERIFY-004 | Implicit |
|
||||
| REQ-ERR-001 | SEC-001, APISEC-001, APISEC-009 | Covered |
|
||||
| REQ-SOCK-001 | N/A | Not Covered |
|
||||
| REQ-SOCK-002 | N/A | Not Covered |
|
||||
| REQ-SUBJ-001 | N/A | Not Covered |
|
||||
| REQ-ROLE-001 | N/A | Not Covered |
|
||||
|
||||
**Note:** API v3 security tests (`tests/api3.security.test.js`) are out of scope for this document. They cover the distinct API v3 authentication model.
|
||||
|
||||
---
|
||||
|
||||
## 9. Coverage Gaps
|
||||
|
||||
### 9.1 High Priority Gaps
|
||||
|
||||
| Gap | Description | Recommended Test |
|
||||
|-----|-------------|------------------|
|
||||
| WebSocket Auth | No tests for `/storage` subscription authentication | Add socket.io-client tests for subscribe with/without token |
|
||||
| JWT Expiration | No test for expired JWT rejection | Create JWT with past exp, verify 401 |
|
||||
| Permission Wildcards | Shiro pattern matching not explicitly tested | Test `api:*:read` vs `api:entries:read` |
|
||||
| API v3 Security | API v3 has distinct security model (`lib/api3/security.js`) | Review `tests/api3.*.test.js` for security coverage, document separately |
|
||||
|
||||
### 9.2 Medium Priority Gaps
|
||||
|
||||
| Gap | Description | Recommended Test |
|
||||
|-----|-------------|------------------|
|
||||
| Subject CRUD | No tests for subject creation/update/delete | Add API tests for admin endpoints |
|
||||
| Role Management | Custom role creation not tested | Test role creation and permission assignment |
|
||||
| Default Roles | Built-in roles not verified | Test each default role's permission set |
|
||||
|
||||
### 9.3 Low Priority Gaps
|
||||
|
||||
| Gap | Description | Recommended Test |
|
||||
|-----|-------------|------------------|
|
||||
| Audit Events | Failed auth notification not verified | Mock bus, verify admin-notify event |
|
||||
| Delay Cleanup | Automatic delay list cleanup not tested | Fast-forward time, verify cleanup |
|
||||
|
||||
---
|
||||
|
||||
## 10. Discovered Quirks and Barriers
|
||||
|
||||
### 10.1 Client-Side Testing Complexity
|
||||
|
||||
**Issue:** The `hashauth.test.js` tests require complex browser environment simulation using `benv`.
|
||||
|
||||
**Details:**
|
||||
- Tests rely on `headless.js` fixture for secure jsdom setup
|
||||
- Network isolation via `NoNetworkLoader` pattern prevents accidental external requests
|
||||
- `js-storage` module caches environment detection on first require, requiring cache clearing in `after()` hook
|
||||
|
||||
**Barrier:** Modernizing these tests requires maintaining the secure jsdom harness to prevent test network leakage.
|
||||
|
||||
### 10.2 Brute-Force Test Timing
|
||||
|
||||
**Issue:** Brute-force delay tests have timing sensitivity.
|
||||
|
||||
**Details:**
|
||||
- Default delay is 5000ms per failure
|
||||
- Tests use lower `authFailDelay` setting for faster execution
|
||||
- Test timeout must exceed cumulative delay
|
||||
|
||||
**Quirk:** The test verifies delay > 49ms which is a very loose bound. Production uses 5000ms default.
|
||||
|
||||
### 10.3 SHA-1 vs SHA-512 Acceptance
|
||||
|
||||
**Issue:** Both SHA-1 and SHA-512 hashes are accepted for API_SECRET.
|
||||
|
||||
**Details:**
|
||||
- SHA-1 produces 40 character hex string
|
||||
- SHA-512 produces 128 character hex string
|
||||
- Both are validated in `verifyauth.test.js`
|
||||
|
||||
**Note:** This dual-hash support provides migration path but may be confusing.
|
||||
|
||||
### 10.4 Delay List Cleanup Limitation
|
||||
|
||||
**Issue:** The brute-force delay list cleanup is a one-shot mechanism, not recurring.
|
||||
|
||||
**Details:**
|
||||
- `delaylist.js` uses a single `setTimeout(30000)` at module initialization
|
||||
- Entries created after the cleanup runs may persist until server restart
|
||||
- The 60-second `FAIL_AGE` is only checked during that single cleanup
|
||||
|
||||
**Impact:** Long-running servers may accumulate stale delay list entries. This is low-risk since successful authentication clears entries and entries naturally expire when their delay time passes.
|
||||
|
||||
### 10.5 API v3 Security Model Scope
|
||||
|
||||
**Issue:** API v3 has a distinct security implementation that is not covered by this specification.
|
||||
|
||||
**Details:**
|
||||
- `lib/api3/security.js` implements API v3-specific authentication
|
||||
- Tests in `tests/api3.*.test.js` cover API v3 behavior
|
||||
- This document focuses on the core `lib/authorization/` module used by API v1/v2
|
||||
|
||||
**Recommendation:** Create separate API v3 security specification if detailed documentation is needed.
|
||||
|
||||
---
|
||||
|
||||
## 11. Test Modernization Notes
|
||||
|
||||
### 11.1 Alignment with Testing Modernization Proposal
|
||||
|
||||
Per `docs/proposals/testing-modernization-proposal.md`:
|
||||
|
||||
**Track 1 (Testing Foundation):**
|
||||
- Security tests are identified as "keep" tests due to their critical nature
|
||||
- hashauth tests require the secure jsdom harness from Track 1
|
||||
|
||||
**Track 2 (Logic/DOM Separation):**
|
||||
- `hashauth.js` client module could be split into pure logic (hash computation) and DOM interaction
|
||||
- Pure logic portion could be tested without browser simulation
|
||||
|
||||
### 11.2 Recommended Test Improvements
|
||||
|
||||
1. **Add explicit JWT expiration test** - Create expired JWT, verify rejection
|
||||
2. **Add WebSocket auth tests** - Use socket.io-client in test environment
|
||||
3. **Parameterize delay tests** - Test with configurable `authFailDelay`
|
||||
4. **Add Shiro pattern tests** - Explicit wildcard matching verification
|
||||
|
||||
---
|
||||
|
||||
## 12. Version History
|
||||
|
||||
| Version | Date | Author | Changes |
|
||||
|---------|------|--------|---------|
|
||||
| 1.0 | January 2026 | Nightscout Team | Initial specification |
|
||||
|
||||
---
|
||||
|
||||
## 13. References
|
||||
|
||||
- [Authorization Security Spec](../requirements/authorization-security-spec.md)
|
||||
- [Security Audit](../security-audit.md)
|
||||
- [Testing Modernization Proposal](../proposals/testing-modernization-proposal.md)
|
||||
- Test files in `tests/` directory
|
||||
@@ -215,7 +215,9 @@ Located in `docs/requirements/` and `docs/test-specs/`:
|
||||
|----------|-------------|
|
||||
| `requirements/data-shape-requirements.md` | Formal requirements for single vs array input handling |
|
||||
| `requirements/api-v1-compatibility-spec.md` | Client compatibility requirements (AAPS, Loop, xDrip) |
|
||||
| `requirements/authorization-security-spec.md` | Auth requirements: API_SECRET, JWT, Shiro permissions, brute-force protection |
|
||||
| `test-specs/shape-handling-test-spec.md` | Test case specifications with requirement traceability |
|
||||
| `test-specs/authorization-test-spec.md` | Security test cases mapped to auth requirements, coverage gaps identified |
|
||||
|
||||
## Comprehensive System Audit Documentation
|
||||
|
||||
@@ -290,6 +292,7 @@ Located in `docs/`:
|
||||
- All changes preserve backward compatibility with single-object inputs
|
||||
|
||||
## Recent Changes
|
||||
- 2026-01-15: Added authorization-security-spec.md and authorization-test-spec.md with formal requirements and test traceability for auth subsystem
|
||||
- 2026-01-15: Fixed devicestatus.js race condition and WebSocket array handling for MongoDB 5.x compatibility
|
||||
- 2026-01-15: Added comprehensive shape-handling test suite (38 tests) for multi-document write validation
|
||||
- 2026-01-13: Updated audit docs with accurate rate limiting info (delaylist.js) and OIDC/gateway architecture direction
|
||||
|
||||
Reference in New Issue
Block a user