sasjs auth login is interactive-only — support programmatic (non-interactive) auth for CI / agents
Summary
The sasjs auth login command (the password-grant flow, added in #1460) is interactive-only: it always prompts for username and password via prompts/getString, with no flag, env var, or stdin path to supply them programmatically. This blocks sasjs auth login from being used in CI pipelines, headless agents, and any non-TTY context, which is precisely where automated deploys run.
The workaround (minting the token directly with curl against /SASLogon/oauth/token and writing it to .env.<target> by hand) works but reimplements getTokensWithPasswordGrant outside the CLI, bypasses the CLI's token-verification (/identities/users/@currentUser) and persistence (saveTokens), and is exactly the kind of hand-rolled SAS-auth the CLI exists to eliminate.
Environment
@sasjs/cli v4.19.0 (released; includes auth login)
- Node v24.15.0, Linux (headless agent / CI runner, no TTY)
- SAS Viya 4, password grant enabled for the
sas.cli client
Current behaviour
src/commands/auth/login.ts always does:
const user = await getString(
'Please enter your SAS username',
(v) => !!v || 'Username is required.'
)
const { pass } = await prompts(
{ type: 'password', name: 'pass', message: 'Please enter your SAS password' },
{ onCancel: () => { throw new Error('Input cancelled.') } }
)
const { access_token, refresh_token } = await getTokensWithPasswordGrant(target, user, pass)
There is no way to pass user/pass without an interactive TTY:
- No
--username / --password flags (and the password is intentionally a prompts('password') masked input — there's deliberately no plain-arg path for it).
- No env var (
SAS_USERNAME/SAS_PASSWORD or similar) — process.env is never consulted for credentials.
- Piping stdin (
printf 'user\npass\n' | sasjs auth login -t viya) does not work: the prompts library renders a TTY UI that echoes the input back to the terminal (the password appears in the log as the prompt's echo buffer), and the masked input doesn't consume the piped bytes cleanly.
Expected behaviour
At least one (ideally several) of:
- Env vars: read
SAS_USERNAME (and SAS_PASSWORD) when set, skipping the corresponding prompt entirely. This is the lowest-friction CI pattern — secrets injected via the runner's secret store, never on the command line, never in logs.
- Flags:
--username / --password (with the usual warning that --password leaks via shell history / process list; prefer env vars). A --password-stdin flag (read the password from stdin, like docker login) is the safest flag form.
- Stdin: if stdin is not a TTY and a password is piped, consume it without rendering the masked prompt. (Likely falls out of (1)/(2) naturally.)
In all cases the credentials should flow into the existing getTokensWithPasswordGrant → saveTokens path (no change to token minting, verification, or persistence), and the password must not be logged or persisted (same guarantee as the interactive flow today).
Suggested implementation
In src/commands/auth/login.ts, resolve credentials with a precedence like:
const user =
process.env.SAS_USERNAME ||
(await getString('Please enter your SAS username', (v) => !!v || 'Username is required.'))
const pass =
process.env.SAS_PASSWORD ||
(await prompts(
{ type: 'password', name: 'pass', message: 'Please enter your SAS password' },
{ onCancel: () => { throw new Error('Input cancelled.') } }
)).pass
i.e. env var short-circuits the prompt; if neither env var nor TTY is available, fail with a clear message pointing at the env vars. Document the env vars in docs/auth.md and the --help output.
Workaround (today)
Mint the token directly with curl (password grant against the secret-less sas.cli client) and write it to .env.<target> in the format the CLI expects — then every subsequent sasjs command picks it up (and silently refreshes via the stored refresh token):
curl -s -u "sas.cli:" <server>/SASLogon/oauth/token \
-d "grant_type=password" \
--data-urlencode "username=$VIYA_USER" \
--data-urlencode "password=$VIYA_PASS" \
| jq -r '"ACCESS_TOKEN=\(.access_token)\nREFRESH_TOKEN=\(.refresh_token)\nCLIENT=sas.cli\nSECRET="' \
> .env.viya
This is functional but reimplements getTokensWithPasswordGrant by hand, skips the CLI's /identities/users/@currentUser verification, and means the "always use the adapter/CLI to talk to SAS" principle has a carve-out just for minting the token. First-class env-var support would remove it.
Why this matters
- CI/CD: deploy pipelines can't run
auth login interactively. Today they must either use a registered client/secret (sasjs auth, the authorisation-code flow — also interactive, needs a browser) or the curl workaround above. Password-grant with env vars is the natural CI pattern for estates without a registered client.
- Agents / headless tooling: coding agents and other automation driving
sasjs have no TTY. The interactive prompt is the blocker.
- Consistency:
getTokensWithPasswordGrant is already a clean, exported utility — only the command layer is interactive. Surfacing env-var/flag input is a small, contained change that unlocks the whole non-interactive use case.
sasjs auth loginis interactive-only — support programmatic (non-interactive) auth for CI / agentsSummary
The
sasjs auth logincommand (the password-grant flow, added in #1460) is interactive-only: it always prompts for username and password viaprompts/getString, with no flag, env var, or stdin path to supply them programmatically. This blockssasjs auth loginfrom being used in CI pipelines, headless agents, and any non-TTY context, which is precisely where automated deploys run.The workaround (minting the token directly with
curlagainst/SASLogon/oauth/tokenand writing it to.env.<target>by hand) works but reimplementsgetTokensWithPasswordGrantoutside the CLI, bypasses the CLI's token-verification (/identities/users/@currentUser) and persistence (saveTokens), and is exactly the kind of hand-rolled SAS-auth the CLI exists to eliminate.Environment
@sasjs/cliv4.19.0 (released; includesauth login)sas.cliclientCurrent behaviour
src/commands/auth/login.tsalways does:There is no way to pass
user/passwithout an interactive TTY:--username/--passwordflags (and the password is intentionally aprompts('password')masked input — there's deliberately no plain-arg path for it).SAS_USERNAME/SAS_PASSWORDor similar) —process.envis never consulted for credentials.printf 'user\npass\n' | sasjs auth login -t viya) does not work: thepromptslibrary renders a TTY UI that echoes the input back to the terminal (the password appears in the log as the prompt's echo buffer), and the masked input doesn't consume the piped bytes cleanly.Expected behaviour
At least one (ideally several) of:
SAS_USERNAME(andSAS_PASSWORD) when set, skipping the corresponding prompt entirely. This is the lowest-friction CI pattern — secrets injected via the runner's secret store, never on the command line, never in logs.--username/--password(with the usual warning that--passwordleaks via shell history / process list; prefer env vars). A--password-stdinflag (read the password from stdin, likedocker login) is the safest flag form.In all cases the credentials should flow into the existing
getTokensWithPasswordGrant→saveTokenspath (no change to token minting, verification, or persistence), and the password must not be logged or persisted (same guarantee as the interactive flow today).Suggested implementation
In
src/commands/auth/login.ts, resolve credentials with a precedence like:i.e. env var short-circuits the prompt; if neither env var nor TTY is available, fail with a clear message pointing at the env vars. Document the env vars in
docs/auth.mdand the--helpoutput.Workaround (today)
Mint the token directly with
curl(password grant against the secret-lesssas.cliclient) and write it to.env.<target>in the format the CLI expects — then every subsequentsasjscommand picks it up (and silently refreshes via the stored refresh token):This is functional but reimplements
getTokensWithPasswordGrantby hand, skips the CLI's/identities/users/@currentUserverification, and means the "always use the adapter/CLI to talk to SAS" principle has a carve-out just for minting the token. First-class env-var support would remove it.Why this matters
auth logininteractively. Today they must either use a registered client/secret (sasjs auth, the authorisation-code flow — also interactive, needs a browser) or the curl workaround above. Password-grant with env vars is the natural CI pattern for estates without a registered client.sasjshave no TTY. The interactive prompt is the blocker.getTokensWithPasswordGrantis already a clean, exported utility — only the command layer is interactive. Surfacing env-var/flag input is a small, contained change that unlocks the whole non-interactive use case.