Manually testing the OAuth login flow #
This guide walks through a real browser-based OAuth login for GhostOff.
1. Make sure there is no app password configured #
GhostOff defaults to OAuth whenever ATP_APP_PASSWORD (or --atproto-app-password) is absent. The easiest way to test that path is to unset it just for this run:
env ATP_APP_PASSWORD= npm run dev
Or temporarily comment out ATP_APP_PASSWORD in your .env.
If you want to keep this test isolated from your normal OAuth session files, pass different file names:
env ATP_APP_PASSWORD= npm run dev -- \
--oauth-session-file ghostoff-oauth-session-test.json \
--oauth-state-file ghostoff-oauth-state-test.json
ATP_SERVICE can be left set or removed; OAuth discovers the PDS from the handle.
2. Answer the prompt #
You should see:
No app password found in .env or --atproto-app-password. Would you like to authenticate via OAuth [Y/n]?
Type Y and press Enter.
If ATP_IDENTIFIER is not configured, the CLI will then prompt:
Enter your atproto handle (e.g. handle.example.com):
3. Authenticate in the browser #
The CLI starts a temporary HTTP server on 127.0.0.1:<ephemeral-port> and prints an authorization URL:
Open this URL in your browser to authenticate GhostOff:
https://selfhosted.social/oauth/...?request_uri=...&client_id=http%3A%2F%2Flocalhost%2F%3F...
The CLI also tries to open the URL automatically. If it doesn't, copy and paste it into your browser.
Log in to your PDS and approve GhostOff. Because this is a local-development OAuth client, the client_id defaults to http://localhost/?redirect_uri=... and the callback lands on http://127.0.0.1:<port>/callback.
4. Wait for the redirect #
After you approve, the PDS redirects to the local callback server. You should see:
Authentication successful. You can close this tab and return to the terminal.
In the terminal you should see something like:
authenticated as did:plc:...
session ready: your-handle.example.com (did:plc:...)
The migration then proceeds normally.
5. Verify it uploaded #
Once the run finishes, check your Offprint publication to confirm the posts appeared.
If the PDS rejects the default client_id #
The atproto local-development exception expects the client_id origin to be http://localhost and the callback to be 127.0.0.1. If your PDS insists on http://127.0.0.1 as the client_id origin, override it:
env ATP_APP_PASSWORD= npm run dev -- \
--oauth-client-id "http://127.0.0.1/?redirect_uri=http%3A%2F%2F127.0.0.1%2Fcallback&scope=atproto%20transition%3Ageneric" \
--oauth-session-file ghostoff-oauth-session-test.json \
--oauth-state-file ghostoff-oauth-state-test.json
Re-testing the full flow #
To force a fresh login, delete the test session file and run again:
rm ghostoff-oauth-session-test.json
env ATP_APP_PASSWORD= npm run dev -- \
--oauth-session-file ghostoff-oauth-session-test.json \
--oauth-state-file ghostoff-oauth-state-test.json
To test session restore instead, leave the session file in place and run the same command a second time. It should skip the browser step and use the cached OAuth session.
Troubleshooting #
- Browser doesn't open: copy the printed URL manually.
- "OAuth state mismatch" or callback errors: make sure you didn't start a second GhostOff process while the first is still waiting for the callback; the loopback server only handles one callback.
- Callback never reaches the terminal: ensure your browser is running on the same machine as the CLI, because the callback server binds to
127.0.0.1only. - Migration prompts for a handle instead of using
ATP_IDENTIFIER:ATP_IDENTIFIERwas empty; the prompt is the intended fallback.