Summary
KeeperVault.resumeSession() never sends the username to start_login, so persistent login always fails with REQUIRES_USERNAME. The failure is reported to callers as the misleading message "clone code may be expired or persistent login not enabled".
Versions
@keeper-security/keeper-sdk-javascript@2.1.0
@keeper-security/keeperapi@18.2.0
- Node.js v24.9.0, Windows 11
- Account type: enterprise, Cloud SSO Connect, region
keepersecurity.com.au
Cause
KeeperVault.resumeSession() resolves the username correctly, then discards it — dist/vault/KeeperVault.js:245-249 calls loginV3 without passing it:
this.auth = await this.createAuth({ useSessionResumption: true });
await this.auth.loginV3({
loginType: keeperapi_1.Authentication.LoginType.NORMAL,
resumeSessionOnly: true,
});
Compare KeeperVault.login(), which does pass username.
In keeperapi, loginV3 resolves the username fine (dist/index.cjs.js):
this._username = username || this.options.sessionStorage?.lastUsername || '';
but the test that decides whether to put it in the request keys off the raw argument rather than the resolved this._username (dist/index.cjs.js:284505):
if (needUserName || !this.options.useSessionResumption || loginType === LoginType.ALTERNATE || username) {
startLoginRequest.username = this._username;
needUserName = false;
}
On the resume path all four clauses are false — needUserName is initially false, useSessionResumption is true, loginType is NORMAL, and username is '' — so username is omitted.
The server replies REQUIRES_USERNAME, which would normally set needUserName = true and retry, but resumeSessionOnly returns before the retry loop is reached:
if (resumeSessionOnly && loginResponse && loginResponse.loginState != Authentication.LoginState.LOGGED_IN) {
return { result: "notLoggedin" };
}
resumeSession() then throws PERSISTENT_LOGIN_FAILED, pointing at the clone code rather than the missing username.
Reproduction
- Complete an interactive login on a clean machine so
~/.keeper/config.json is written with device_token, private_key, user, server and clone_code (v2.1.0 persists all of these).
- In a new process:
const vault = new KeeperVault({ host, configDir })
await vault.resumeSession()
Observed request and response:
→ authentication/start_login StartLoginRequest: { cloneCode: '…' } // no username
← start_login LoginResponse: { loginState: 'REQUIRES_USERNAME' }
Thrown: Persistent login failed — clone code may be expired or persistent login not enabled. Perform a normal login.
Confirmation
Reimplementing resumeSession() identically except for forwarding the username changes the server's answer, which isolates this as the cause:
await vault.auth.loginV3({
username, // the only difference
loginType: Authentication.LoginType.NORMAL,
resumeSessionOnly: true,
})
→ authentication/start_login StartLoginRequest: { username: '…', cloneCode: '…' }
← start_login LoginResponse: { loginState: 'REDIRECT_CLOUD_SSO' }
(The subsequent REDIRECT_CLOUD_SSO is a separate matter on this SSO account and not part of this report.)
Suggested fix
Pass the username through in KeeperVault.resumeSession():
await this.auth.loginV3({
username,
loginType: Authentication.LoginType.NORMAL,
resumeSessionOnly: true,
});
Optionally also make the keeperapi test use the resolved value, so the request is correct regardless of caller:
if (needUserName || !this.options.useSessionResumption || loginType === LoginType.ALTERNATE || this._username) {
It would also help if resumeSession() surfaced the actual loginState — the current message sends you looking at the clone code, which is not the problem.
Summary
KeeperVault.resumeSession()never sends the username tostart_login, so persistent login always fails withREQUIRES_USERNAME. The failure is reported to callers as the misleading message "clone code may be expired or persistent login not enabled".Versions
@keeper-security/keeper-sdk-javascript@2.1.0@keeper-security/keeperapi@18.2.0keepersecurity.com.auCause
KeeperVault.resumeSession()resolves the username correctly, then discards it —dist/vault/KeeperVault.js:245-249callsloginV3without passing it:Compare
KeeperVault.login(), which does passusername.In keeperapi,
loginV3resolves the username fine (dist/index.cjs.js):but the test that decides whether to put it in the request keys off the raw argument rather than the resolved
this._username(dist/index.cjs.js:284505):On the resume path all four clauses are false —
needUserNameis initiallyfalse,useSessionResumptionistrue,loginTypeisNORMAL, andusernameis''— sousernameis omitted.The server replies
REQUIRES_USERNAME, which would normally setneedUserName = trueand retry, butresumeSessionOnlyreturns before the retry loop is reached:resumeSession()then throwsPERSISTENT_LOGIN_FAILED, pointing at the clone code rather than the missing username.Reproduction
~/.keeper/config.jsonis written withdevice_token,private_key,user,serverandclone_code(v2.1.0 persists all of these).Observed request and response:
Thrown:
Persistent login failed — clone code may be expired or persistent login not enabled. Perform a normal login.Confirmation
Reimplementing
resumeSession()identically except for forwarding the username changes the server's answer, which isolates this as the cause:(The subsequent
REDIRECT_CLOUD_SSOis a separate matter on this SSO account and not part of this report.)Suggested fix
Pass the username through in
KeeperVault.resumeSession():Optionally also make the keeperapi test use the resolved value, so the request is correct regardless of caller:
It would also help if
resumeSession()surfaced the actualloginState— the current message sends you looking at the clone code, which is not the problem.