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
17 KiB
Authorization and Security Test Specification
Document Version: 1.0
Last Updated: January 2026
Status: Draft
Related Requirements: Authorization Security Spec
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
# 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
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
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
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
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
benvpackage to simulate browser DOM headless.jsfixture provides secure jsdom harness- Tests mock
localStorage,window.alert, and jQuery plugins
Network Isolation:
mockAjax: trueprevents actual network requestsverifyAuthenticationis 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
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:
- First failed attempt records the IP
- Second failed attempt experiences delay
- 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
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
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 accessnoneSubject- No specific roles (tests default role behavior)adminAll- Full admin access
Environment Configuration:
self.env.settings.authDefaultRoles = 'denied';
Tests explicitly set denied as default to ensure unauthenticated access is blocked.
7. Test Environment Setup
7.1 Prerequisites
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:
var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1';
// SHA-1 hash of 'this is my long pass phrase'
SHA-512 Hash (also accepted):
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.jsfixture for secure jsdom setup - Network isolation via
NoNetworkLoaderpattern prevents accidental external requests js-storagemodule caches environment detection on first require, requiring cache clearing inafter()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
authFailDelaysetting 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.jsuses a singlesetTimeout(30000)at module initialization- Entries created after the cleanup runs may persist until server restart
- The 60-second
FAIL_AGEis 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.jsimplements API v3-specific authentication- Tests in
tests/api3.*.test.jscover 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.jsclient 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
- Add explicit JWT expiration test - Create expired JWT, verify rejection
- Add WebSocket auth tests - Use socket.io-client in test environment
- Parameterize delay tests - Test with configurable
authFailDelay - 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
- Security Audit
- Testing Modernization Proposal
- Test files in
tests/directory