Repository navigation
feat: recovery sessions and account notice delivery, with parity hardening - #12
Merged
Merged
Conversation
The console proxy now forwards X-Frame-Options, Content-Security-Policy and X-Content-Type-Options from the auth API, so the console served on the app origin cannot be framed. Closes #10
…ening Recovery sessions: RequireAuth refuses an access token carrying recovery: true with 403 enrollment_required, and Authenticate returns ErrEnrollmentRequired, unless AllowRecoverySessions is set. User.Recovery marks one. After passkey registration or TOTP enrollment from a recovery session, cookie transport refreshes so the new access cookie drops the claim. Account notices: with DeliverAccountNotices, routes the manifest marks notices ask for external delivery with x-seamless-auth-delivery-kinds, hand every item in deliveries to Deliver and strip it from the response. If no notice is delivered, the adapter cancels the recovery request with the token from the cancel link and answers 503 recovery_notice_failed. Without the option the API sends the notices itself. Delivery gains Purpose, Notice, Subject, Text and CancelURL, prints without its credentials, and an unknown kind is logged and skipped. The bundled manifest is synced with API 0.19.4 (recovery, recovery code and policy routes). Parity with the Python and Rust adapters: the upstream client refuses redirects, a supplied client is copied rather than changed, upstream errors are logged without the URL, the JWKS cache expires after 10 minutes with a 5 second failure backoff and a 256 KiB cap, JWTs must carry exp, logout clears the cookies when the credential is rejected, a cookie transport pick never carries tokens, cookie lifetimes are capped at 400 days, and AuthServerURL must be http or https. Placeholder secrets: New refuses the example cookie and service secrets unless InsecureCookies is set. Closes #11 fix: refuse upstream redirects and keep upstream URLs out of logs fix: expire the JWKS cache, back off failed fetches and cap the key set size fix: require exp, clear cookies on a rejected logout and never pick tokens into a body fix: cap cookie lifetimes at 400 days and require an http or https AuthServerURL fix: refuse placeholder cookie and service secrets outside insecure local mode
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 v0.20 adapter work for Go: parity with the Python and Rust adapters, the console framing headers, placeholder secrets, recovery sessions and account recovery notice delivery.
#11 Parity with Python and Rust
CheckRedirectreturnshttp.ErrUseLastResponse). A caller-suppliedHTTPClientis copied with thatCheckRedirect, so the caller's own client is not changed. It also gets the 15 second timeout if it has none, as in Python.*url.ErrorURL, so no path tokens or query strings end up in logs. This covers manifest routes, logout, the manifest fetch and JWKS.checkTimesnow requiresexp, for both HS256 cookies and RS256 API tokens.pickno longer letstokenorrefreshTokenthrough.httporhttpsURL with a host.#10 Console headers
ConsoleHandlernow also forwardsX-Frame-Options,Content-Security-PolicyandX-Content-Type-Options.Placeholder secrets (fells-code/seamless-auth-server#231)
Newrefusesdev-only-cookie-signing-key-replace-me,GRAB_FROM_SEAMLESS_AUTH_PORTAL_DEVand any value containingreplace-me(case-insensitive) forCookieSecretandServiceSecret. WithInsecureCookies(the adapter's local development mode, as in Python and Rust) they are allowed, with a warning.Recovery sessions (fells-code/seamless-auth-server#232)
RequireAuthanswers 403{ "error": "enrollment_required" }for an access token withrecovery: true, by cookie or bearer, andAuthenticatereturns the newErrEnrollmentRequired.Options.AllowRecoverySessionsadmits such a token, andUser.Recoverymarks it.POST /webauthn/register/finishorPOST /totp/enroll/verifyfrom a recovery session refreshes the session, so the new access cookie drops the claim. This also works when a silent refresh happened earlier in the same request: the refresh uses the token that refresh just issued. It goes through the shared refresher, so parallel tabs do not trip reuse detection. If the refresh fails, the enrollment response is still returned. Bearer clients hold their own refresh token, so the README tells them to callPOST /auth/refreshafter enrolling.Account recovery delivery (fells-code/seamless-auth-api#500)
ManifestRoute.Noticesreads the manifest's new optionalnoticesfield. The bundled manifest is synced from the API's committedadapter-manifest.json(0.19.4), which adds the recovery, recovery code and policy routes.Options.DeliverAccountNotices, false by default. A singleDelivercallback cannot show whether it handles the new kinds. Only with this option (andDeliver) does anotices: trueroute sendx-seamless-auth-delivery-mode: external, the delivery service token andx-seamless-auth-delivery-kinds: account_notice_email,account_notice_sms. Without it, external mode is not sent on that route, and the API sends the notices itself.deliveriesgoes toDeliver.deliveries(anddelivery) are stripped from the response in both transports. If at least one notice is delivered, the response is the API's. If none is, the adapter cancels the request: it sendsPOST /recovery/cancelwith{"cancelToken": <fragment of cancelUrl>}and the proxy service token, then answers 503recovery_notice_failed. The cancel URL and token are never logged.DeliverygainsPurpose,Notice,Subject,TextandCancelURL, plusDelivery*kind constants andPurposeAccountRecovery. ItsString/GoStringmethods leave out the code and links. An unknown kind, or a notice kind without the opt-in, is logged and skipped, not passed toDeliver./recovery/*,/recovery-codes*,GET /recovery/requests,DELETE /recovery/requests/{id}, every/system-config/policy*route,503 recovery_notice_failedand a403 step_up_requiredbody on an admin write.Checks
Go 1.22 test binaries cannot run on the local macOS (a dyld
LC_UUIDloader error), so the 1.22 test run is left to CI. The conformance suite was not run locally.Closes #10
Closes #11