feat(hololake): add mobile server account and capability proxy
This commit is contained in:
parent
758eef79ab
commit
19bd38c6e3
11 changed files with 1436 additions and 1 deletions
211
server-tools/lake-lamp-authz/hololake-session.test.js
Normal file
211
server-tools/lake-lamp-authz/hololake-session.test.js
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
"use strict";
|
||||
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const {
|
||||
HoloLakeSessionManager,
|
||||
} = require("./hololake-session");
|
||||
|
||||
function fixture(overrides = {}) {
|
||||
let now = 1_800_000_000_000;
|
||||
const mail = [];
|
||||
const manager = new HoloLakeSessionManager({
|
||||
registeredEmails: ["owner@example.invalid"],
|
||||
pepper: "test-only-pepper-with-enough-entropy",
|
||||
stateFile: "",
|
||||
now: () => now,
|
||||
sendEmail: async message => {
|
||||
mail.push(message);
|
||||
return true;
|
||||
},
|
||||
...overrides,
|
||||
});
|
||||
return {
|
||||
manager,
|
||||
mail,
|
||||
advance(milliseconds) {
|
||||
now += milliseconds;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("OTP request is non-enumerating and stores no plaintext code", async () => {
|
||||
const known = fixture();
|
||||
const requested = await known.manager.requestOtp({
|
||||
email: "Owner@Example.Invalid",
|
||||
deviceId: "ios-device-001",
|
||||
});
|
||||
assert.equal(requested.accepted, true);
|
||||
assert.match(requested.request_id, /^[0-9a-f-]{36}$/);
|
||||
assert.equal(known.mail.length, 1);
|
||||
assert.match(known.mail[0].text, /\b\d{6}\b/);
|
||||
const otp = known.mail[0].text.match(/\b\d{6}\b/)[0];
|
||||
assert.doesNotMatch(JSON.stringify(known.manager.inspectState()), new RegExp(otp));
|
||||
|
||||
const unknown = fixture();
|
||||
const decoy = await unknown.manager.requestOtp({
|
||||
email: "nobody@example.invalid",
|
||||
deviceId: "ios-device-001",
|
||||
});
|
||||
assert.equal(decoy.accepted, requested.accepted);
|
||||
assert.match(decoy.request_id, /^[0-9a-f-]{36}$/);
|
||||
assert.equal(unknown.mail.length, 0);
|
||||
});
|
||||
|
||||
test("OTP verification is device-bound, attempt-limited, and returns a one-time session token", async () => {
|
||||
const state = fixture();
|
||||
const requested = await state.manager.requestOtp({
|
||||
email: "owner@example.invalid",
|
||||
deviceId: "ios-device-001",
|
||||
});
|
||||
const otp = state.mail[0].text.match(/\b\d{6}\b/)[0];
|
||||
|
||||
const wrongDevice = state.manager.verifyOtp({
|
||||
requestId: requested.request_id,
|
||||
code: otp,
|
||||
deviceId: "ios-device-002",
|
||||
});
|
||||
assert.equal(wrongDevice.ok, false);
|
||||
assert.equal(wrongDevice.error, "invalid_or_expired_code");
|
||||
|
||||
const verified = state.manager.verifyOtp({
|
||||
requestId: requested.request_id,
|
||||
code: otp,
|
||||
deviceId: "ios-device-001",
|
||||
});
|
||||
assert.equal(verified.ok, true);
|
||||
assert.match(verified.session_token, /^[A-Za-z0-9_-]{40,}$/);
|
||||
assert.doesNotMatch(
|
||||
JSON.stringify(state.manager.inspectState()),
|
||||
new RegExp(verified.session_token),
|
||||
);
|
||||
|
||||
const reused = state.manager.verifyOtp({
|
||||
requestId: requested.request_id,
|
||||
code: otp,
|
||||
deviceId: "ios-device-001",
|
||||
});
|
||||
assert.equal(reused.ok, false);
|
||||
assert.equal(reused.error, "invalid_or_expired_code");
|
||||
});
|
||||
|
||||
test("session authentication, expiry, and revocation never return the stored token", async () => {
|
||||
const state = fixture({ sessionTtlSeconds: 60 });
|
||||
const requested = await state.manager.requestOtp({
|
||||
email: "owner@example.invalid",
|
||||
deviceId: "ios-device-001",
|
||||
});
|
||||
const otp = state.mail[0].text.match(/\b\d{6}\b/)[0];
|
||||
const verified = state.manager.verifyOtp({
|
||||
requestId: requested.request_id,
|
||||
code: otp,
|
||||
deviceId: "ios-device-001",
|
||||
});
|
||||
|
||||
const active = state.manager.authenticate(
|
||||
verified.session_token,
|
||||
"ios-device-001",
|
||||
);
|
||||
assert.equal(active.ok, true);
|
||||
assert.equal(active.session.device_id, "ios-device-001");
|
||||
assert.equal(Object.hasOwn(active.session, "token"), false);
|
||||
|
||||
assert.equal(
|
||||
state.manager.authenticate(verified.session_token, "ios-device-002").error,
|
||||
"session_device_mismatch",
|
||||
);
|
||||
assert.equal(
|
||||
state.manager.revoke(verified.session_token, "ios-device-002").error,
|
||||
"session_device_mismatch",
|
||||
);
|
||||
assert.equal(
|
||||
state.manager.revoke(verified.session_token, "ios-device-001").ok,
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
state.manager.authenticate(verified.session_token, "ios-device-001").error,
|
||||
"session_invalid",
|
||||
);
|
||||
|
||||
const second = await state.manager.requestOtp({
|
||||
email: "owner@example.invalid",
|
||||
deviceId: "ios-device-001",
|
||||
});
|
||||
const secondOtp = state.mail[1].text.match(/\b\d{6}\b/)[0];
|
||||
const secondSession = state.manager.verifyOtp({
|
||||
requestId: second.request_id,
|
||||
code: secondOtp,
|
||||
deviceId: "ios-device-001",
|
||||
});
|
||||
state.advance(61_000);
|
||||
assert.equal(
|
||||
state.manager.authenticate(
|
||||
secondSession.session_token,
|
||||
"ios-device-001",
|
||||
).error,
|
||||
"session_expired",
|
||||
);
|
||||
});
|
||||
|
||||
test("OTP expires, locks after five failed attempts, and request rate is bounded", async () => {
|
||||
const expired = fixture({ otpTtlSeconds: 30 });
|
||||
const requested = await expired.manager.requestOtp({
|
||||
email: "owner@example.invalid",
|
||||
deviceId: "ios-device-001",
|
||||
});
|
||||
const otp = expired.mail[0].text.match(/\b\d{6}\b/)[0];
|
||||
expired.advance(31_000);
|
||||
assert.equal(
|
||||
expired.manager.verifyOtp({
|
||||
requestId: requested.request_id,
|
||||
code: otp,
|
||||
deviceId: "ios-device-001",
|
||||
}).error,
|
||||
"invalid_or_expired_code",
|
||||
);
|
||||
|
||||
const locked = fixture({ maxOtpAttempts: 5 });
|
||||
const lockRequest = await locked.manager.requestOtp({
|
||||
email: "owner@example.invalid",
|
||||
deviceId: "ios-device-001",
|
||||
});
|
||||
const realOtp = locked.mail[0].text.match(/\b\d{6}\b/)[0];
|
||||
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||
assert.equal(
|
||||
locked.manager.verifyOtp({
|
||||
requestId: lockRequest.request_id,
|
||||
code: "000000",
|
||||
deviceId: "ios-device-001",
|
||||
}).ok,
|
||||
false,
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
locked.manager.verifyOtp({
|
||||
requestId: lockRequest.request_id,
|
||||
code: realOtp,
|
||||
deviceId: "ios-device-001",
|
||||
}).error,
|
||||
"invalid_or_expired_code",
|
||||
);
|
||||
|
||||
const limited = fixture({ requestLimit: 2 });
|
||||
await limited.manager.requestOtp({
|
||||
email: "owner@example.invalid",
|
||||
deviceId: "ios-device-001",
|
||||
networkKey: "198.51.100.7",
|
||||
});
|
||||
await limited.manager.requestOtp({
|
||||
email: "owner@example.invalid",
|
||||
deviceId: "ios-device-001",
|
||||
networkKey: "198.51.100.7",
|
||||
});
|
||||
const denied = await limited.manager.requestOtp({
|
||||
email: "owner@example.invalid",
|
||||
deviceId: "ios-device-001",
|
||||
networkKey: "198.51.100.7",
|
||||
});
|
||||
assert.equal(denied.accepted, false);
|
||||
assert.equal(denied.error, "rate_limited");
|
||||
assert.equal(limited.mail.length, 2);
|
||||
});
|
||||
Loading…
Reference in a new issue