Feat/tier1 auth methods - #3
Merged
Merged
Conversation
Tier 1 needs the TOTP, WebAuthn, recovery-code, login-attempt and API-key tables. Same reasoning as the existing 001/002 copies: this repo keeps its own copy so local dev and CI stay self-contained. Filenames continue this repo's own sequence; the header line keeps cryden's original migration number so the source stays traceable.
Tier 1's second-factor surface, all of it a thin translation of an engine call that already exists in cryden — no auth logic lives here. - TOTP: enroll/confirm/disable (auth required) plus the public /v1/login/totp completion of a paused login. - Passkeys: register begin/finish, list, delete-by-credential-id (all auth required), plus the public login begin/finish pair. Ceremony options travel as raw JSON so the client gets an object, not a JSON-encoded string. - Magic link: request (always the same 200 either way — this does not create accounts, and anything else would enumerate emails) and complete. Needs a new notify.MagicLinkSender implementation, which cryden deliberately keeps separate from EmailSender. - Recovery codes: generate (raw codes shown exactly once, and the response says so) and the public login completion. A correct password is no longer always enough to finish a login, so /v1/login (and the OAuth callback, next commit) now surfaces a paused login as 200/second_factor_required with the pending token and the enrolled methods instead of letting *auth.ErrSecondFactorRequired fall through to a 500. TOTP and WebAuthn are optional per deployment, gated on ENCRYPTION_KEY in main.go — cryden refuses to build an engine with either store set and no encryption key. An unconfigured method answers 404 from mapError, the same shape oauth_provider_not_configured already uses, rather than the server refusing to start.
The router and handler shape already took a {provider} path param, so
each of these is a case in the provider switch plus its env vars —
same authorize/token/userinfo flow Google and GitHub already use.
Also moves the code-for-token exchange onto a POST form body instead
of query parameters. RFC 6749 puts those parameters in the body, and
Microsoft and Discord require it; Google and GitHub accept the body
form too, so this replaces a per-provider branch rather than adding
one. (GitHub's form-encoded response is still avoided by the existing
Accept: application/json header.)
Apple is deliberately not in this commit — its client "secret" is a
short-lived JWT you sign yourself, the email comes back inside a
signed id_token rather than from a userinfo call, and the name is
only sent on first authorization. That is its own piece of work, not
a fourth mechanical case.
httpapi had no test files before this; these four are the first, and they only cover what is verifiable without a database: that each new engine error resolves to its documented (status, code) pair through mapError (including when wrapped), and that a paused login writes the 200/second_factor_required shape with an empty array rather than null while leaving ordinary errors for writeErr.
README gains the new endpoints, the second-factor env vars, the paused-login response shape and the supported provider list; the env example gains the optional OAuth and second-factor settings. NEXT.md marks Tier 1 built-and-verified-as-far-as-this-sandbox-allows with Apple still open; CURRENT-STATE.md gains the Tier 1 section; PROGRESS.md gains this session's entry, including the decisions that were not in the spec (paused login as a 200, the token exchange moving to a form body) and two things noticed but deliberately not fixed (unmapped password-policy errors, the .env.example/.env.exampl name mismatch).
Neither auth.ErrPasswordPolicyViolation nor auth.ErrPasswordBreached had a case in mapError, so a signup or password change that broke the configured policy answered 500 internal_error and logged itself as a server fault. Both are the caller's mistake to fix, and both now answer 400 with a code a client can branch on. The policy error is deliberately kept as a struct case, like ErrOAuthEmailConflict: it carries every broken rule at once as stable codes (min_length, require_digit, ...) precisely so the client can show them together rather than parsing prose. writeErr reads them off the error into an optional `details` array, which is the only addition to the error envelope and appears on no other error — the tests pin both halves of that.
README has always told you to `cp .env.example .env`; the tracked file was missing the final `e`, so that copy failed on a fresh clone. Renamed to match the instruction rather than changing the instruction, since `.env.example` is the conventional name and `.gitignore` only ignores `.env` itself.
Tier 1's last sub-item, and deliberately not a fifth mechanical provider case — all three of Apple's differences are handled rather than papered over: - The client "secret" is an ES256 JWT this repo signs itself with the .p8 key from Apple's developer console (apple.go's appleClientSecret), not a static string. APPLE_CLIENT_ID, APPLE_TEAM_ID, APPLE_KEY_ID and APPLE_PRIVATE_KEY are all required; a partially configured Apple reads as unavailable, like any other unconfigured provider. - There is no userinfo endpoint. The identity comes from the `id_token` in the token response, so it is parsed against Apple's published JWKS with issuer, audience, expiry and RS256 enforced — an unverified decode would let anyone who can reach the callback mint an account. The key set is cached with a TTL and refetched on an unknown kid. - Authorization uses response_mode=query so the existing GET callback route works unchanged. The name/email Apple only sends on a first authorization arrives in the `user` form field under form_post, which this repo does not need: it stores the id_token's email. exchangeCode now takes the client secret as an argument and returns both the access token and the id_token, which is what lets the one Apple path reuse the existing POST plumbing instead of duplicating it. Verified offline by unit tests (apple_test.go): the generated secret parses as a valid ES256 JWT with the console's kid and the right iss/aud/sub; the id_token is accepted when signed by the served key and rejected for a wrong audience, wrong issuer, expiry, a different signing key, an unknown kid, a missing subject, and an `alg: none` token. Real Apple credentials and a live round trip were not available in this sandbox — that is stated in PROGRESS.md and the docs.
README now lists apple among the providers, documents its four env values and the two real differences (self-signed ES256 secret, identity from a verified id_token); the env example gains the APPLE_* block. NEXT.md drops the "except Apple" caveat and records the two Apple decisions (response_mode=query, no nonce) plus what is still owed: a DB-backed smoke-test run, a live Apple round trip, and the WebAuthn ceremonies. CURRENT-STATE.md gains the Apple/what-is-tested section and the two bug fixes, and its verification-gap paragraph now names exactly which paths this sandbox could not exercise. PROGRESS.md gains the continuation entry.
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.
No description provided.