Skip to content

Let the API create a token that carries a vault - #839

Merged
blaipr merged 1 commit into
mainfrom
feat/the-api-can-create-a-token-that-carries-a-vault
Aug 23, 2026
Merged

blaipr merged 1 commit into
mainfrom
feat/the-api-can-create-a-token-that-carries-a-vault

Conversation

@blaipr

@blaipr blaipr commented Aug 23, 2026

Copy link
Copy Markdown
Member

Closes the gap recorded in CLAUDE.md since #834.

The gap

POST /api/v1/auth-tokens answered 500 "Error while retrieving master password from context"
for every action a token carries a vault for — the five SECURED_ACTIONS and the three
CAN_USE_SECURE_TOKEN_ACTIONS, so ACCOUNT_VIEW and ACCOUNT_CREATE among them — with or without
a password. The help documents actionId with no restriction and the web creates the same tokens
without difficulty, so this was an oversight, not a policy.

Sealing a vault needs the master password on the context. The API only ever loads that from the
calling token's own vault, and AUTHTOKEN_CREATE was on neither list, so it had none.

The decision

This was recorded rather than patched because fixing it is a decision, not a repair:
AUTHTOKEN_CREATE and AUTHTOKEN_EDIT are now themselves on CAN_USE_SECURE_TOKEN_ACTIONS, so a
token that can mint tokens also carries the master password.

That is the authority the web already grants — an administrator who can reach the tokens page has
unlocked the vault with their own password — but on the API it is a bearer credential living in
somebody's script, so it is worth as much as the vault. Two consequences, both enforced in
AuthTokenBase::prepareSecureToken() and both recorded in CLAUDE.md:

  • the password is required, not optional, for a token that carries a vault. Without it the vault
    is sealed with the empty string and nothing can ever open it, because tokenPass is a required
    parameter and required refuses the empty string. This is the same rule Ask for a token password whenever a vault will be built #833 gave the web form,
    asked at the other door;
  • creating one needs a calling token that already carries a vault. An AUTHTOKEN_CREATE token
    minted before this change has none and gets a 401 until it is re-issued; the web can always issue
    the first one.

Test

In AuthTokenRoundTripTest, which already round-trips a token rather than only minting one:

  • the round trip that was impossible: create an ACCOUNT_VIEW token through the API, then use
    the string it answers with to read an account with customFields — the path that needs the
    vault. Minting it is only half the claim;
  • every one of the eight vault-carrying actions mints, so this is not a fix for the one action that
    happened to be tried;
  • no password is refused with Password cannot be blank;
  • an action carrying no vault still needs no password — the control, so the rule did not become
    "every token needs one".

Mutation-checked: reverting src/ fails exactly those ten and leaves the four pre-existing tests
passing.

OK (4037 tests, 36967 assertions)   unit
OK (1005 tests, 3004 assertions)    integration

PHPStan level 6 and PHPCS clean.

AuthTokenHelp is unchanged on purpose: password stays required: false there because it is
conditional on the action, and ApiHelpMatchesControllersTest pins the declared flag against what
the controller actually reads.

POST /api/v1/auth-tokens answered 500 "Error while retrieving master password
from context" for every action a token carries a vault for — the five
SECURED_ACTIONS and the three CAN_USE_SECURE_TOKEN_ACTIONS, so ACCOUNT_VIEW and
ACCOUNT_CREATE among them — with or without a password, while the web created the
same tokens without difficulty. Sealing a vault needs the master password on the
context, and the API only loads that from the calling token's own vault, which
AUTHTOKEN_CREATE did not have.

Fixing it is a decision rather than a repair, which is why it was recorded first:
AUTHTOKEN_CREATE and AUTHTOKEN_EDIT are now themselves on
CAN_USE_SECURE_TOKEN_ACTIONS, so a token that can mint tokens also carries the
master password. That is the authority the web already grants, since an
administrator who can reach the tokens page has unlocked the vault with their own
password — but on the API it is a bearer credential in somebody's script, so it
is worth as much as the vault.

Two things follow, both enforced in AuthTokenBase::prepareSecureToken(). The
password on such a token is required rather than optional, because without it the
vault is sealed with the empty string and nothing can open it: tokenPass is a
required parameter and required refuses the empty string. And creating one needs
a calling token that already carries a vault, so an AUTHTOKEN_CREATE token minted
before this change gets a 401 until it is re-issued; the web can always issue the
first one.

The tests round-trip rather than only mint: an ACCOUNT_VIEW token created through
the API is then used to read an account with customFields, which is the path that
needs the vault. All eight vault-carrying actions are covered, a missing password
is refused, and an action carrying no vault still needs none.

AuthTokenHelp is unchanged: password stays optional there because it is
conditional on the action, and ApiHelpMatchesControllersTest pins the declared
flag against what the controller reads.
@blaipr
blaipr merged commit 0732b9f into main Aug 23, 2026
8 checks passed
@blaipr
blaipr deleted the feat/the-api-can-create-a-token-that-carries-a-vault branch August 23, 2026 00:47
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