~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

The iOS app can log in through a browser and hold a page's session

Merged
Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:feature/ios-session

What changes

The app has a way to log in. GET /oauth/app-login starts the pages' own login in the browser the app opens, with a PKCE challenge. Approved, the callback sets no cookie and sends the browser on to /oauth/app-return?code=…, a universal link that /.well-known/apple-app-site-association gives to the app alone. POST /oauth/app-token trades that code and its verifier, once, for the value a knotless_session cookie would hold. The app keeps it in its keychain and sends it as that cookie.

Nothing else changes: every query and write takes the app as it takes a page, with the same scopes, reauth, logout and takedown.

Why

Writes need the session the appview keeps, and a browser's cookie is out of an app's reach. Bearer tokens accepted beside the cookie would have meant a second way in for every procedure, and same_site already passes a request with no Origin, so sending the cookie needs no change there. /mcp's authorization server solves a harder problem, of clients that register themselves, for a client we ship.

The session isn't in the redirect because that passes through the browser. A code is useless without the verifier, and a wrong verifier spends it.

A universal link rather than a custom scheme because any app can claim a scheme: it could start this login with its own challenge, and someone approving on knotless's real page would hand it their session.

How it was checked

the_app_logs_in_with_a_code_only_it_can_trade in crates/knotless-serve/tests/pages.rs: a login through harness/authorize.mjs as far as the universal link, refused and approved; a bad challenge; a wrong verifier spending the code; a code used twice; an account taken down since; the association file and the page for a browser with no app; and the token asking getViewer and writing createRepo as a cookie. Left to CI to build and run. The expiry of a code isn't tested, as nothing here moves the clock.

Shortcomings

  • An app showing this login in its own web view can still read the code, as it can a password typed there. The app uses the system's browser session, and nothing on the server can tell the two apart.
  • Whether iOS accepts the association can't be tested until this is live.
  • The token lasts 90 days with no refresh, as a page's cookie does, and then the app signs in again.
  • Logging out in the app or on the pages logs out both, as they share one session on the server.

🤖 Generated with Claude Code

Approved

This adds GET /oauth/app-login and POST /oauth/app-token. The app logs in through the browser with a PKCE challenge, gets a one-time code on its own scheme, and trades the code plus verifier for the same signed value a knotless_session cookie holds. I read the diff and found nothing that must change.

  • Code handling: the code is removed on the first try, right or wrong. It expires after 5 minutes, the verifier must match the challenge, and a takedown is rechecked at trade time. The session never appears in the redirect.
  • Web login: the callback refactor into signed() leaves the web path unchanged.
  • Docs and tests: api.md, serve.md and security.md are updated, and the new test covers the refused, wrong-verifier, reuse and takedown cases.
  • Disclosed gap: the malicious-app gap is already named in the body and docs, as needing a universal link before a public release. I'd keep that as a hard blocker for any merge to main.

Minor, not blocking: app_login checks only that the challenge is 43 characters long. It does not check that they are base64url characters. A challenge that can never match is harmless, but a charset check would turn a client bug into an immediate 400 instead of a failure later at the trade.

I did not see the checks' results. The author said they are left to CI, and CI enforces the coverage gate.

Approved

This is the follow-up to my earlier approval. The login now comes back to the universal link /oauth/app-return, served with an apple-app-site-association file. That closes the malicious-app gap the first round flagged as a blocker for main.

I read the whole commit, including the back_to_the_app change, the two new handlers, the harness change, the test and the docs. I found nothing that must change.

  • Cleanup of expired codes: it now runs after the insert, as the commit message says. The new code is only just made, so it survives.
  • Page at /oauth/app-return: it says nothing of the code, so a desktop browser learns nothing from it.
  • Association file: the team and bundle ID in APP_ID match what the test asserts.
  • Docs: serve.md and api.md match the new flow, and serve.md names the limit that a web view in another app can still read the address.

I did not see the checks' results. The earlier minor point about the challenge's charset is still open and still not blocking.

Two things to check once the app exists, neither a blocker here:

  • The redirect from the callback to gw.endpoint is a same-site navigation. Safari does not hand same-site redirects to an app, so the login session must be opened in a way that honours an https callback (ASWebAuthenticationSession on iOS 17.4 or later).
  • gw.endpoint must have no trailing slash, or the address gets a double slash.
Approved

This is the third round. The new commits are a merge of main and a fix that adds the Html import app_return needed. The earlier ejection from the merge queue was a failed pre-flight at 6b0c36c, which is what that missing import would cause. I read the full diff again and found nothing that must change.

  • Code handling: a code is removed on its first trade, right or wrong. It expires after 5 minutes. The verifier is hashed and compared to the challenge, and a takedown is rechecked at trade time. The session never appears in the redirect.
  • callback refactor: the web path is unchanged apart from spaces::discover now running before the app branch, so the app login also runs discovery.
  • Checks: at 916c6e3 check, test, e2e and docs were still queued or running when I looked, so I have not seen the build or the new test pass. The skipped workflows passed. Both the merge queue and CI are the gate for this.

The minor charset check on the challenge from round 1 is still open and not blocking. My round 2 notes still apply once the app exists. ASWebAuthenticationSession must be opened with an https callback, which needs iOS 17.4 or later. gw.endpoint must have no trailing slash.

Merged as 688e92d