Repository navigation
feat(auth): bootstrap a login token from credentials and 2FA (#138) - #148
Merged
Merged
Conversation
Copying a personal login token out of the ChurchTools web UI is the first
thing a new user has to do and the first thing they get wrong. `ct auth
login` without `--token` now offers three ways to authenticate:
1. Username and password — POST /api/login, complete POST /api/login/totp
on the SAME session cookie when the instance answers `status: "totp"`,
then GET /api/persons/{personId}/logintoken for the persistent token.
2. Existing login token — the previous behaviour, asked hidden.
3. Skip — nothing collected, with the command to run later.
Security properties, each covered by a test:
- Username, password and TOTP code are locals for the duration of the two
requests that consume them. Only {host, token} reaches the credential
store. There is deliberately NO password flag: a password on the command
line lands in shell history and in `ps`.
- Nothing secret is printed. Messages derived from a ChurchTools response
pass through `redactSecrets`, and an authentication error carries the HTTP
status and CT's own message — never the request body that was sent.
- Password/TOTP/token prompts do not echo (`askHidden` mutes readline).
- Platforms with no credential store (`isSecureStorageAvailable`, the same
macOS check `storeCredentials` already made) are detected BEFORE anything
is asked for, so a password is never collected only to be discarded; they
keep the CT_HOST / CT_LOGINTOKEN guidance.
Non-interactive `ct auth login --host … --token …` is unchanged, and a
non-TTY never prompts.
The flow lives in `bootstrapLoginToken(host, deps)` so `ct init` (#131) can
call the same prompts from its own sequence.
Closes #138.
Claude-Session: https://claude.ai/code/session_018JShVZYNLaRb4hF5KbHCXG
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The flow
ct auth loginwithout--token(and on a TTY) now asks:hidden.
POST /api/login; when ChurchTools answersstatus: "totp", asks for thesix-digit code (also hidden) and
POST /api/login/totpwithcode+personIdon thesame session cookie; then
GET /api/persons/{personId}/logintokenfor thepersistent personal login token. Only
{host, token}is then verified and storedthrough the existing
storeCredentialspath.with
CtClient.authenticate, stored.The host is prompted for too when neither
--hostnorCT_HOSTis set.Fetching the login token belongs to the person who just authenticated — that is
authentication, not people management. No other person surface is added.
Scoping — this lands on
auth login, notinitThe issue frames this as part of
ct init(#131). #131's PR #139 is still a draft andconflicting with
main, so this PR is based onmainand lands the flow onct auth logininstead. It does not touchct initand is independently mergeable.The seam for #139: the whole interactive flow is one exported function,
ct initcan call it with the host the user just chose and drop the returned token intothe same
storeCredentialscallct auth loginmakes.depscarries injectableprompts/isTTY/secureStorage/fetchImpl, soinitcan reuse its own promptsurface. It stores nothing itself, so the two callers cannot disagree about where
credentials live.
Security properties, and how each is tested
loginWithPassword; only{host, token}is passed to the store.ct auth loginstill callsstoreCredentials({host, token})and nothing else.bootstrapLoginTokenreturns{kind:"token", token}and nothing else — asserted by equality, so an added field fails the test.redactSecrets(text, secrets); errors areLoginError(message, status)and never carry a request body.optionsand asserts no flag matches/password/i, `/--totpaskHiddenmutes readline's_writeToOutputimmediately after the prompt label is written./api/loginbody, the code only in/api/login/totp, and neither in any URL.isSecureStorageAvailable()(exported fromtokenStore.ts— the sameplatform() === "darwin"checkstoreCredentialsalready made) is consulted before the first prompt; the flow returnsunsupportedwith theCT_HOST/CT_LOGINTOKENguidance.secureStorage: falseand prompts that throw if called, and asserts the env-var hint comes back. A second test pinsisSecureStorageAvailable()to the platform.Also covered: password login without 2FA, TOTP challenge detected and completed on the
same cookie, no code asked when there is no challenge, both ChurchTools envelope shapes
(top-level and nested
data), a non-six-digit code refused before it is sent anywhere, achallenge with no interactive way to answer it, an instance returning no token, the skip
choice, an unknown choice, and "never prompts on a non-TTY".
Notes
fetchstub, and the fixtures use a.invalidhost so a missed stub fails loudly rather than reaching an instance.src/api/ctClient.ts,src/api/session.tsandauthenticate()are untouched, tokeep the merge with the parallel Session is not reused across invocations — every ct call re-runs the login handshake and trips CT's 429 rate limit #145 session-caching work cheap. The login→TOTP session
cookie is handled by a small local jar in
src/auth/login.ts; the resulting token isstill verified through
CtClient.authenticateunchanged.ct initchanges (see scoping), and no live/opt-in test — there is nothinghere to probe live without typing a real password at a prod instance.
Closes #138.