Skip to content

resumeSession() omits username from start_login, so persistent login always fails #249

Description

@jj-dt

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

  1. 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).
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions