Skip to content

Commit 1a17984

Browse files
fix(auth): harden OAuth callback flow and add per-app redirect URI config (#64)
1 parent eeeb501 commit 1a17984

13 files changed

Lines changed: 798 additions & 121 deletions

File tree

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Changelog
2+
3+
All user-visible bugs and enhancements should be recorded here.
4+
5+
## Unreleased
6+
7+
Last updated: 2026-04-19 23:48:20 CEST
8+
9+
### Fixed
10+
11+
- [2026-04-19 23:08:51 CEST] OAuth2 callback listeners now bind to the host and port derived from the effective redirect URI instead of always listening on `127.0.0.1:8080`. For `localhost`, `xurl` now listens on both `127.0.0.1` and `::1`, which fixes browser-dependent loopback resolution failures while still supporting non-default callback paths.
12+
- [2026-04-19 23:08:51 CEST] The OAuth2 listener now starts listening before the browser opens, which removes a race where the browser could reach the callback URL before the local server was ready.
13+
- [2026-04-19 23:08:51 CEST] OAuth2 token refresh no longer depends on `/2/users/me` succeeding. If username discovery fails, `xurl` keeps the refreshed token instead of failing the request.
14+
- [2026-04-19 23:08:51 CEST] Shortcut commands that need the current user ID now fall back to `--username` lookups when `/2/users/me` is unavailable.
15+
- [2026-04-19 23:08:51 CEST] `GetOAuth2Header` now consistently returns a `Bearer` header even when it has to trigger a fresh OAuth2 flow.
16+
17+
### Enhanced
18+
19+
- [2026-04-19 23:08:51 CEST] OAuth2 tokens can now be retained without a discovered username label when X’s `/2/users/me` lookup is unavailable. Status output makes that state visible as `(unknown user)` instead of silently dropping the token.
20+
- [2026-04-19 23:08:51 CEST] Repo documentation now describes the effective redirect URI as the source of callback host, port, and path, calls out explicit username authentication as the safer fallback when username discovery is unreliable, and documents the new stored `redirect_uri` behavior.
21+
- [2026-04-19 23:08:51 CEST] Apps can now store a per-app `redirect_uri` in `~/.xurl`, `REDIRECT_URI` from the environment still takes precedence, and `xurl auth apps redirect-uri get/set` plus `auth apps update --redirect-uri` make that configuration visible and editable from the CLI.
22+
- [2026-04-19 23:48:20 CEST] Documentation now records the confirmed X platform enrollment requirement behind `client-forbidden` / `client-not-enrolled` read failures: moving the app to the `Pay-per-use` package and the `Production` environment fixed live `/2/*` reads after OAuth had already succeeded.

README.md

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,19 +51,28 @@ Register your X API app credentials so they're stored in `~/.xurl` (no env vars
5151
xurl auth apps add my-app --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET
5252
```
5353

54+
If you want the app to keep its own callback configuration in `~/.xurl`, you can store the redirect URI there too:
55+
56+
```bash
57+
xurl auth apps add my-app --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET --redirect-uri http://localhost:8080/callback
58+
```
59+
5460
You can register multiple apps:
5561
```bash
5662
xurl auth apps add prod-app --client-id PROD_ID --client-secret PROD_SECRET
5763
xurl auth apps add dev-app --client-id DEV_ID --client-secret DEV_SECRET
5864
```
5965

6066
> **Legacy / env-var flow:** You can also set `CLIENT_ID` and `CLIENT_SECRET` as environment variables. They'll be auto-saved into the active app on first use.
67+
>
68+
> `REDIRECT_URI` now resolves in this order: `REDIRECT_URI` environment variable, then the app's stored `redirect_uri` in `~/.xurl`, then the built-in default `http://localhost:8080/callback`.
6169
6270
#### OAuth 2.0 User-Context
6371
**Note:** For OAuth 2.0 authentication, you must specify the redirect URI in the [X API developer portal](https://developer.x.com/en/portal/dashboard).
6472

6573
1. Create an app at the [X API developer portal](https://developer.x.com/en/portal/dashboard).
66-
2. Go to authentication settings and set the redirect URI to `http://localhost:8080/callback`.
74+
2. Go to authentication settings and set the redirect URI to the same value that `xurl` will use through `REDIRECT_URI`.
75+
The default is `http://localhost:8080/callback`, and `xurl` derives the callback host, port, and path from the effective redirect URI. The effective value is resolved from `REDIRECT_URI`, then the app's stored `redirect_uri`, then the built-in default. When you use `localhost`, `xurl` listens on both `127.0.0.1` and `::1` so browser loopback resolution does not break the callback.
6776
![Setup](./assets/setup.png)
6877
![Redirect URI](./assets/callback.png)
6978
3. Register the app (if you haven't already):
@@ -75,6 +84,24 @@ xurl auth apps add my-app --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT
7584
xurl auth oauth2
7685
```
7786

87+
If X returns a `client-forbidden` / `client-not-enrolled` error even though auth completed successfully, check the app’s package and environment in the X developer console. On current X platform setup, the working fix was:
88+
89+
1. Go to `Apps` -> `Manage apps`
90+
2. Open the app
91+
3. Use `Move to package`
92+
4. Choose `Pay-per-use`
93+
5. Move the app to the `Production` environment
94+
95+
Without that enrollment step, `xurl whoami` and other `/2/*` reads can fail even when the OAuth callback and tokens are valid.
96+
97+
If X does not return your username reliably through `/2/users/me`, authenticate with an explicit handle instead:
98+
99+
```bash
100+
xurl auth oauth2 YOUR_USERNAME
101+
```
102+
103+
That keeps the OAuth2 token associated with the expected username and also gives shortcut commands a fallback when `/2/users/me` is unavailable.
104+
78105
#### App authentication (bearer token):
79106
```bash
80107
xurl auth app --bearer-token BEARER_TOKEN
@@ -95,6 +122,19 @@ xurl auth apps list
95122
Update credentials on an existing app:
96123
```bash
97124
xurl auth apps update my-app --client-id NEW_ID --client-secret NEW_SECRET
125+
xurl auth apps update my-app --redirect-uri http://localhost:8080/callback
126+
```
127+
128+
`REDIRECT_URI` from the environment still overrides the stored app value at runtime, so `auth apps update --redirect-uri` is best for your default per-app callback while env vars remain the temporary override path.
129+
130+
View the effective and stored redirect URI for an app:
131+
```bash
132+
xurl auth apps redirect-uri get my-app
133+
```
134+
135+
Set the stored redirect URI for an app:
136+
```bash
137+
xurl auth apps redirect-uri set my-app http://localhost:8080/callback
98138
```
99139

100140
Remove an app:
@@ -124,20 +164,28 @@ View authentication status across all apps:
124164
xurl auth status
125165
```
126166

167+
This output shows the effective redirect URI for each app and, when `REDIRECT_URI` is set in the environment, also shows the stored app value separately so precedence is visible.
168+
127169
Example output:
128170
```
129171
▸ my-app [client_id: VUttdG9P…]
172+
redirect_uri: http://localhost:8080/callback [app config]
130173
▸ oauth2: alice
131174
oauth2: bob
132175
oauth1: ✓
133176
bearer: ✓
134177
135178
dev-app [client_id: OTHER789…]
179+
redirect_uri: http://localhost:8080/callback [built-in default]
136180
oauth2: (none)
137181
oauth1: –
138182
bearer: –
139183
```
140184

185+
### X Platform Enrollment Troubleshooting
186+
187+
If OAuth succeeds but reads like `xurl whoami` fail with an error body containing `client-forbidden` or `client-not-enrolled`, the current X platform fix is to move the app into the `Pay-per-use` package and use the `Production` environment in the developer console. This is an X platform enrollment issue, not a local callback-listener issue in `xurl`.
188+
141189
`` on the left = default app. `` next to a user = default user.
142190

143191
### Clear Authentication
@@ -283,6 +331,7 @@ apps:
283331
my-app:
284332
client_id: abc123
285333
client_secret: secret456
334+
redirect_uri: http://localhost:8080/callback
286335
default_user: alice
287336
oauth2_tokens:
288337
alice:

SKILL.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,15 @@ For multiple pre-configured apps, switch between them:
4040
xurl auth default prod-app # set default app
4141
xurl auth default prod-app alice # set default app + user
4242
xurl --app dev-app /2/users/me # one-off override
43+
xurl auth apps redirect-uri get prod-app
44+
xurl auth apps redirect-uri set prod-app http://localhost:8080/callback
4345
```
4446

4547
### Other auth methods
4648

4749
Examples with inline secret flags are intentionally omitted. If OAuth1 or app-only auth is needed, the user must run those commands manually outside agent/LLM context.
4850

49-
Tokens are persisted to `~/.xurl` in YAML format. Each app has its own isolated tokens. Do not read this file through the agent/LLM. Once authenticated, every command below will auto‑attach the right `Authorization` header.
51+
Tokens are persisted to `~/.xurl` in YAML format. Each app has its own isolated tokens and may also store a `redirect_uri`. `REDIRECT_URI` in the environment still takes precedence over the stored app value. Do not read this file through the agent/LLM. Once authenticated, every command below will auto‑attach the right `Authorization` header.
5052

5153
---
5254

@@ -87,7 +89,9 @@ Tokens are persisted to `~/.xurl` in YAML format. Each app has its own isolated
8789
| **App Management** | |
8890
| Register app | Manual, outside agent (do not pass secrets via agent) |
8991
| List apps | `xurl auth apps list` |
90-
| Update app creds | Manual, outside agent (do not pass secrets via agent) |
92+
| Update app config | Manual, outside agent (do not pass secrets via agent) |
93+
| View app redirect URI | `xurl auth apps redirect-uri get [NAME]` |
94+
| Set app redirect URI | `xurl auth apps redirect-uri set NAME URI` |
9195
| Remove app | `xurl auth apps remove NAME` |
9296
| Set default (interactive) | `xurl auth default` |
9397
| Set default (command) | `xurl auth default APP_NAME [USERNAME]` |
@@ -382,7 +386,8 @@ xurl --app staging /2/users/me # one-off request against staging
382386
- Non‑zero exit code on any error.
383387
- API errors are printed as JSON to stdout (so you can still parse them).
384388
- Auth errors suggest re‑running `xurl auth oauth2` or checking your tokens.
385-
- If a command requires your user ID (like, repost, bookmark, follow, etc.), xurl will automatically fetch it via `/2/users/me`. If that fails, you'll see an auth error.
389+
- If a command requires your user ID (like, repost, bookmark, follow, etc.), xurl will automatically fetch it via `/2/users/me`. When that endpoint is unreliable, use `--username USERNAME` or authenticate with `xurl auth oauth2 USERNAME` so xurl can fall back to username lookup.
390+
- If X returns `client-forbidden` / `client-not-enrolled` after successful auth, check the app’s X developer-console package and environment. In current testing, moving the app to `Pay-per-use` and `Production` fixed `/2/*` read failures without changing local `xurl` auth data.
386391

387392
---
388393

@@ -391,7 +396,10 @@ xurl --app staging /2/users/me # one-off request against staging
391396
- **Rate limits:** The X API enforces rate limits per endpoint. If you get a 429 error, wait and retry. Write endpoints (post, reply, like, repost) have stricter limits than read endpoints.
392397
- **Scopes:** OAuth 2.0 tokens are requested with broad scopes. If you get a 403 on a specific action, your token may lack the required scope — re‑run `xurl auth oauth2` to get a fresh token.
393398
- **Token refresh:** OAuth 2.0 tokens auto‑refresh when expired. No manual intervention needed.
394-
- **Multiple apps:** Each app has its own isolated credentials and tokens. Configure credentials manually outside agent/LLM context, then switch with `xurl auth default` or `--app`.
399+
- **Multiple apps:** Each app has its own isolated credentials, tokens, and optional stored `redirect_uri`. Configure credentials manually outside agent/LLM context, then switch with `xurl auth default` or `--app`.
400+
- **Redirect URI precedence:** The effective redirect URI resolves from `REDIRECT_URI` in the environment first, then the app's stored `redirect_uri` in `~/.xurl`, then the built-in default.
401+
- **Redirect URI management:** Use `xurl auth apps redirect-uri get [NAME]`, `xurl auth apps redirect-uri set NAME URI`, or `xurl auth apps update NAME --redirect-uri URI` to inspect and manage the stored per-app callback value.
402+
- **X platform enrollment:** A successful OAuth callback does not guarantee `/2/*` reads will work. If you see `client-not-enrolled`, verify the app is in the correct X package/environment. Current confirmed fix: `Apps` -> `Manage apps` -> `Move to package` -> choose `Pay-per-use`, then move the app to `Production`.
395403
- **Multiple accounts:** You can authenticate multiple OAuth 2.0 accounts per app and switch between them with `--username` / `-u` or set a default with `xurl auth default APP USER`.
396404
- **Default user:** When no `-u` flag is given, xurl uses the default user for the active app (set via `xurl auth default`). If no default user is set, it uses the first available token.
397405
- **Token storage:** `~/.xurl` is YAML. Each app stores its own credentials and tokens. Never read or send this file to LLM context.

0 commit comments

Comments
 (0)