Skip to content

docs(lore-0156): settle the self-service auth assumptions — membership + account age, one key, ADR 0010 - #187

Open
adamkoot wants to merge 8 commits into
developfrom
docs/0156_self-service-auth-assumptions
Open

docs(lore-0156): settle the self-service auth assumptions — membership + account age, one key, ADR 0010#187
adamkoot wants to merge 8 commits into
developfrom
docs/0156_self-service-auth-assumptions

Conversation

@adamkoot

Copy link
Copy Markdown
Collaborator

Closes the two items the Self-Service Onboarding epic flagged "confirm before build". Research is in lore/1-tasks/active/0156_.../, the decision is ADR 0010.

The finding that changed the design

The epic's abuse barrier exists but the flow could not see it.

Stellar's guild (897514728459468821) does have Membership Screening enabled, so the assumption was not baseless. But under the identify scope, Discord OAuth observes only that a Discord account existsverified requires the email scope, there is no phone field on the OAuth user object at all, and guild membership sits behind a separate scope and call. The barrier was invisible to the software that depended on it.

Supporting evidence that cuts the same way: SDF's own SCF Dashboard does not treat a Discord account as sufficient — its verified-member flow adds social verification and Stellar wallet auth on top.

Decisions

  • Scope grows to identify + guilds.members.read — never guilds, which returns every server a user belongs to and whose partial guild objects carry neither pending nor joined_at.
  • Membership of the Stellar guild is required, plus a 5-minute account age from the snowflake, matching Stellar's own verification_level: 2. Both checked once, at issuance.
  • One active key per account confirmed — and structurally required: AWS charges quota per (usage plan, API key) with no principal that aggregates keys, so multi-key would force the aggregation the rework cap exists to avoid.
  • Captcha, email confirmation and manual approval costed and declined. A fully-drained key is worth $0.38/month; 286 of them reach $100/month.

Guild ID is per-environment SSM config: a stellar_test guild for build and test, production flipped in 0170.

Two corrections to text already merged

Both checked directly against AWS docs:

  • nameQuery is not documented as a prefix match. AWS's entire description is "The name of queried API keys." — no matching semantics at all. This makes the client-side exact-match filter in 0158/0160 load-bearing rather than defence in depth.
  • The quota reset instant is undocumented. "1st of the month, 00:00 UTC" appeared in the epic and 0157/0158/0160 as if inherited from AWS. It is our own product rule; AWS's only statement is an example caption.

Verification

Every URL cited across the five research notes was re-fetched and its quoted text compared against the original. Live endpoints were re-run independently — the Discord invite API and the SCF OAuth redirect chain matched byte for byte, and a snapshot taken three days earlier agrees on guild ID, verification level and screening. 17 sources archived.

One contradiction inside Discord's own documentation is recorded rather than resolved: the API reference and the support article disagree on what the MEDIUM 5-minute clock measures.

Downstream

0157-0164 and the epic updated. 0164 needed rewriting rather than extending — "run from a genuinely fresh account" would now fail by design, and its Tranche 3 evidence must follow 0170's SSM flip, because a flow gated on our private test guild is not functional for any outside developer.

Two tasks spawned: 0170 (SDF contact, verify against the real guild, flip SSM) and 0171 (measure seven undocumented Discord/AWS behaviours the design depends on).

Renumbered from 0169/0170 after PR #184 took 0169 on develop mid-flight.

Five parallel research lines settle the two items the Self-Service
Onboarding epic flagged "confirm before build".

The barrier exists but the flow could not see it: Stellar's guild has
Membership Screening enabled, yet under the `identify` scope OAuth
observes only that a Discord account exists. `verified` requires the
`email` scope and no phone field exists on the OAuth user object at all.
SDF's own SCF Dashboard does not treat a Discord account as sufficient
either.

One active key per account is confirmed and structurally required: AWS
charges quota per (usage plan, API key) and has no principal that
aggregates keys, so multi-key would force the aggregation the rework cap
exists to avoid.

Every cited URL was re-fetched and its quotes checked against the
original; live endpoints were re-run independently. 17 sources archived.
0169 owns the external half: agreeing production guild integration with
SDF and proving the flow against the real guild. Development runs
against a `stellar_test` guild instead, so this blocks launch rather
than build. Its most important question is whether SDF will keep
Membership Screening on and warn us if that changes — with the account
age threshold at 5 minutes, screening is the entire abuse barrier.

0170 measures seven behaviours the design depends on that turn out to be
undocumented. Two are corrections to text already written as fact:
`nameQuery` is not documented as a prefix match, and the monthly quota
reset instant is not documented at all.
Scope grows by two gates: membership of the Stellar Discord via
`identify` + `guilds.members.read`, and a 5-minute account age from the
snowflake, matching Stellar's own verification_level. Both are checked
once at issuance, so the registry stores no membership data and the
0160 handlers make no Discord calls at all.

0164 needed rewriting rather than extending: "run from a genuinely fresh
account" would now fail by design, and its evidence run must follow
0169's SSM flip — a flow gated on our private test guild is not
functional for any outside developer, which is what the Tranche 3
criterion asks about. Epic ordering recorded there.

Also corrects two claims the sources do not support, in the epic and in
0157/0158/0160: `nameQuery` matching semantics are undocumented, and the
quota reset instant is our own product rule rather than inherited AWS
behaviour.
0169 was taken on develop by the deploy-board trigger bug (PR #184),
merged while this branch was in flight. The SDF integration task becomes
0170 and the undocumented-behaviour spike becomes 0171.

References updated across the epic, ADR 0010, 0157-0164 and both task
files. The verbatim source archives are untouched: one apparent hit
there was the substring 017000 inside an OAuth token timestamp.
ADR 0010 makes the guild ID and the account-age threshold configuration,
but no task declared where they come from. Unassigned, they would have
been hard-coded — the one outcome the ADR rules out. 0159 now owns
creating both under the existing /prices/{env}/* key contract, and must
prove the threshold is tunable without a redeploy.

Also restores numeric order of 0164's checks 11 and 12.
Five independent audits of this PR. The evidence base held: 60 URLs
re-fetched, every load-bearing quote verbatim, all 22 cost figures
reproduced. The decision documents did not.

Corrected in ADR 0010:
- HIGH's 10 minutes is server dwell time, not account age. Discord
  publishes no account-age threshold above five minutes.
- `customerId` has two AWS definitions; the omitted one ("or an external
  system, such as a developer portal") describes our case. The
  conclusion stands on a narrower warrant: no page says quota aggregates
  over it.
- Added the phone-supersedes caveat, the delete-then-create derivation
  the research asked the ADR to show, and lowering the quota as a
  costed alternative.
- Restated the barrier honestly as two clicks and a five-minute wait.
- Hedges restored where the research was more careful than the ADR.

Recorded as Open, blocking 0159/0160/0162: whether issuance requires
`pending === false`; how the eligibility verdict reaches the issue
endpoint, since "checked once at issuance" was written meaning "at
sign-in" and the tasks split on exactly that seam; and what happens when
a departed member's session expires.

Also: the new SSM parameters are operator-seeded and read at runtime,
not CDK-created — a CloudFormation-owned parameter would revert the
launch guild flip on the next deploy. Citation slips fixed in three
research notes; 0171 renumbered seven to nine; notes promoted to mature.
Adam's decisions on the three questions the audit opened.

Membership must hold at the moment a key is issued, not at sign-in. The
verdict cannot ride in the session cookie: OAuth only proves a Discord
account exists, so an ineligible user holds a valid session, and a signed
claim would date the check to sign-in and die long before a rework. So
the action carries the OAuth round-trip — `state` binds the intended
action, and the callback checks with a fresh token. No bot in Stellar's
guild, no persisted tokens.

An issued key never expires and is never deactivated. Reveal and usage
need no Discord check and work indefinitely, including for someone who
has left the guild. Rework re-checks membership; account age is not
re-checked, since an account old enough once stays old enough.

Reading `pending: true` as not-a-member, flagged in the ADR as reversible
— Discord restricts such members from acting in the guild, and it is the
only reading under which screening means anything. Gated on 0171 #2:
`pending`'s presence on the REST response is undocumented, and a naive
test would refuse everyone if it is absent.

Supersedes "checked once, at issuance" in ADR 0010 §3 and withdraws
0160's "these handlers make no Discord calls" — issue was one of them.
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.

2 participants