Document Version: 1.1
Last Updated: January 2026
Status: Active
Related Requirements: Authorization Security Requirements
| Metric | Value |
|---|---|
| Total Tests | 21 |
| Coverage Status | Core paths covered, gaps in WebSocket/API v3 |
| Last Test Run | January 2026 |
| Known Regressions | None |
| Date | Discovery | Impact | Source |
|---|---|---|---|
| 2026-01-15 | JWT uses dedicated signing key, not API_SECRET | Corrected security model understanding | lib/server/enclave.js |
| 2026-01-15 | Brute-force cleanup is one-shot setTimeout | Potential long-running server issue | lib/authorization/delaylist.js |
| 2026-01-15 | Both SHA-1 and SHA-512 accepted for API_SECRET | Migration path but potential confusion | lib/hashauth.js |
| 2026-01-15 | Access token = SHA-1(apiKeySHA1 + subject._id) | Not direct API_SECRET derivative | lib/server/enclave.js:getSubjectHash() |
| Gap | Priority | Status |
|---|---|---|
WebSocket Auth (/storage subscription) |
High | Not Covered |
| JWT Expiration rejection | High | Not Covered |
| Permission Wildcards (Shiro patterns) | High | Not Covered |
| API v3 Security model | High | Separate spec needed |
| Subject CRUD operations | Medium | Not Covered |
| Role Management | Medium | Not Covered |
| Audit Events | Low | Not Covered |
npm test -- --grep "API_SECRET\|Security\|hashauth\|verifyauth"
npm test -- --grep "Security of REST API V1"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.
| 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 |
# 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"| 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 |
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');
});| 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.
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);
});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
| 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 |
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);
});
});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.
| 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 |
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();
});
});
});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.
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();
});
});API_SECRET Hash:
var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1';
// SHA-1 hash of 'this is my long pass phrase'SHA-512 Hash (also accepted):
var known512 = '8c8743d38cbe00debe4b3ba8d0ffbb85e4716c982a61bb9e57bab203178e3718b2965831c1a5e42b9da16f082fdf8a6cecf993b49ed67e3a8b1cd475885d8070';| 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.
| 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 |
| 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 |
| 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 |
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.
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.
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.
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.
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.
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
- 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
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | January 2026 | Nightscout Team | Initial specification |
- Authorization Security Requirements
- Security Audit
- Testing Modernization Proposal
- Test files in
tests/directory