Skip to content

feat(sdks): typed MFA-required login error in all four SDKs + real-server contract tests for the 403/409 refusals - #60

Open
aibeshkov wants to merge 7 commits into
mainfrom
sdkmfaerr1/mfa-required-typed-error
Open

aibeshkov wants to merge 7 commits into
mainfrom
sdkmfaerr1/mfa-required-typed-error

Conversation

@aibeshkov

Copy link
Copy Markdown
Contributor

STACKED ON #59 — merge after it (this branch contains #59's commits; the diff on top of them is the last commit). It also needs keyorix #3175 on the server for the refusal contract tests (see "Server ref" below).

What

  1. Typed MFA-required login error in all four SDKs. A password login on an account with a second factor (the server default, ADR-112) answers 200 with mfa_required + mfa_challenge and no token. Every SDK used to report that as a generic "no token in login response". Now:
SDK login on an MFA account complete it with
Go *MFARequiredError {Challenge, TOTPAvailable, WebAuthnAvailable, Methods()} VerifyMFA(ctx, url, challenge, code)
Node MFARequiredError (extends AuthError) .challenge, .methods verifyMfa(url, challenge, code)
Python MFARequiredError (extends AuthError) .challenge, .methods verify_mfa(url, challenge, code)
Java MFARequiredException (extends AuthException) getChallenge(), getMethods() Keyorix.verifyMfa(url, challenge, code)
  • verifyMfa calls POST /auth/mfa/verify with {mfa_challenge, code} (TOTP or recovery code); a wrong or expired code/challenge is the SDK's auth error.
  • Node/Python/Java: the new type extends the auth error login already threw for this case, so existing catch AuthError handlers keep working. Go had no matchable type before.
  • Passkey (WebAuthn) needs a browser ceremony; the SDK reports webauthn in the methods but does not complete it (documented).
  • The challenge id is not put in the error message.
  1. Real-server contract tests, per SDK:
    • password login on an MFA account -> typed error with challenge and ["totp"]; wrong code -> auth error; fresh login + TOTP computed from the account's seed -> a session that authenticates.
    • the four refusals (SecretSuspended, SecretExpired, SecretMaxReadsExhausted = 409 -> secret-unavailable error; SecretAccessOutsideSchedule = 403 -> forbidden error), each checked for status, stable code, type, and that the value does not leak.
  2. Fixtures via the API, in setup. scripts/contract_bootstrap.py (still MFA ON, never require_mfa:false) now also creates: one MFA-enrolled account per SDK (contract-mfa-<lang>, seed exported masked as KEYORIX_CONTRACT_MFA_SEED_<LANG>; one each because the server burns a TOTP step per account), and one secret per refusal state (suspend via POST /secrets/{id}/suspend, a 2 s expiration, max_reads=1 with the one read spent, a schedule closed for today and tomorrow UTC).

Server ref (not .github)

Main answers these refusals 500 today; the codes come from keyorix #3175 (open). .keyorix-server-ref is set to errmap1/value-read-refusal-codes so the contract job builds that server. After #3175 merges, set .keyorix-server-ref back to main (one-line change, I will do it on request). Codes used are #3175's table: 403 SecretAccessOutsideSchedule; 409 SecretExpired, SecretSuspended, SecretMaxReadsExhausted.

.github

No .github change needed: the bootstrap step already runs once and exports through $GITHUB_ENV.

Verification (local, sandbox)

Server built from keyorix errmap1/value-read-refusal-codes (#3175), fresh SQLite DB, MFA on, then contract_bootstrap.py:

  • Go: go test -race ./... ok (unit + contract); gofmt, go vet clean.
  • Node: test.js 66/0; contract-test.js 31/0.
  • Python: unit 25 OK; contract 11 OK.
  • Java: compiled with javac and run with the JUnit launcher (Maven cannot write ~/.m2 in the sandbox): 58/59 pass; the one failure is KeyorixClientTest.testTlsPrivateCa, which cannot create its temp dir in the sandbox (Operation not permitted), unrelated. ContractTest (incl. the two new tests) and MfaLoginTest pass.
  • Not possible: a red proof of the MFA tests without turning MFA off (forbidden). The refusal tests are red against a server without #3175 (it answers 500).

Session: SDK-MFA-ERR-1

…MAP-1)

The server now answers a value read the secret's own state refuses with 409
SecretExpired / SecretSuspended / SecretMaxReadsExhausted, or 403
SecretAccessOutsideSchedule, instead of 500. Expose the stable code on the base
error and raise SecretUnavailable* for the three 409 codes; the schedule 403 stays
Forbidden* with an accurate message. Local message text, never the server body.

Session: ERRMAP-1
Signed-off-by: Andrei Beshkov <andrey.beshkov@gmail.com>
…ce on a ternary

Signed-off-by: Andrei Beshkov <andrey.beshkov@gmail.com>
…nstructor throws it)

Signed-off-by: Andrei Beshkov <andrey.beshkov@gmail.com>
keyorix main requires MFA by default (ADR-112), so the contract suites'
password login stopped at the MFA step. A bootstrap script now creates the
admin, enrols TOTP from the returned seed, completes the real MFA login and
mints a short-lived PAT that every SDK suite uses. Each suite also asserts a
password-only login still returns an MFA challenge. The role-less machine
token expectation accepts the current empty list as well as 403.

Signed-off-by: Andrei Beshkov <andrey.beshkov@gmail.com>
…al-server contract tests for the 403/409 refusals

login on an MFA account now returns a typed MFA-required error carrying the challenge id and methods (instead of 'no token in login response'), with verifyMfa to complete it. The bootstrap creates per-SDK MFA accounts and one secret per refusal state through the API; each suite exercises MFA login->TOTP->session and the four refusals against the real server.

Signed-off-by: Andrei Beshkov <andrey.beshkov@gmail.com>
…ation burns the current step; challenge logins hold rate-limit slots)

Signed-off-by: Andrei Beshkov <andrey.beshkov@gmail.com>
Signed-off-by: Andrei Beshkov <andrey.beshkov@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant