Redirect URI policy
The five redirect platforms, address shape rules, matching semantics and the port exception.
The redirect URI is where the authorization code is delivered, and the easiest part of the flow to misconfigure. This page makes "which addresses can be registered" and "which addresses can match" explicit.
Five platforms
| Platform | Address shape | Secret required | Access token | Refresh token |
|---|---|---|---|---|
web_server | https://host/path; loopback may use http://localhost/path | ✅ Confidential | 1 hour | 30 days |
web_client | Same as above | ❌ Public | 30 minutes | 14 days |
desktop | Loopback address (with path) or a custom scheme such as myapp://callback | ❌ Public | 30 minutes | 14 days |
ios_macos | Only com.muoceon.<bundle-id>://auth | ❌ Public | 30 minutes | 14 days |
android | Only com.muoceon.<package>://auth/<43-char signature hash> | ❌ Public | 30 minutes | 14 days |
Token lifetimes depend on whether the client holds a secret, not on the platform itself: confidential clients have a server-side fallback and can use longer tokens; public clients keep tokens on the user's device, where a leak has no second line of defence, so only short lifetimes make sense.
Common validation (all platforms)
Re-run at both registration and matching time:
- No more than 2048 characters, containing no whitespace.
- Must parse as a valid URL.
- No query, fragment, username or password:
https://example.com/cb?x=1❌https://example.com/cb#frag❌https://user:pass@example.com/cb❌
- Scheme blocklist:
javascript,data,file,blob,about,vbscript. These schemes "execute content" rather than "send the user back"; allowing them hands the code to a script.
Shape rules per platform
web_server / web_client
- Must be
httpswith a non-root path:https://example.com/cb✅,https://example.com❌. - Loopback may use
http:http://localhost:3000/callback✅.
desktop
One of two shapes:
- A loopback address with a path:
http://127.0.0.1:51789/callback✅. - A custom scheme (not
http/https):myapp://callback✅.
ios_macos / android
Only reverse-domain schemes generated by convention are accepted; free-form values are rejected:
com.muoceon.com.yourcompany.yourapp://auth
com.muoceon.com.yourcompany.yourapp://auth/<signature hash>The Android last segment is a 43-character signature hash (A-Z, a-z, 0-9, -, _). iOS and Android use independent schemes so that multiple apps declaring the same scheme do not force the OS to show a chooser.
Matching semantics
The redirect_uri in the authorization request is compared against usable registered rows:
- Exact match first: the whole normalised string must be equal (scheme, host, port, path and trailing slash included).
- Loopback port relaxation: when both the registered and requested addresses are loopback hosts (
localhost/127.0.0.1/[::1]), only the port is ignored; the path must still match. This applies only toweb_server,web_clientanddesktop. - Missing
redirect_uri: falls back only when the client has exactly one usable registered row; with multiple rows the request is rejected — multiple rows mean the platform cannot be inferred, and falling back to any of them delivers the code to a guessed address.
Requests must carry redirect_uri
The fallback is only a safety net. In production, send the same redirect_uri explicitly in both the
authorization request and the token request; otherwise adding a second callback address later will
suddenly break a flow that used to work.
Platform and secret must agree
Only web_server presumes "the client holds a secret"; the other four platforms run on user devices. Therefore:
- A
web_serverredirect row cannot sit on a client without a secret (the row claims "I have a server" while the client cannot present a secret). - A device-shape redirect row cannot sit on a confidential client (confidential clients must present a secret everywhere, so shipping it in a distributed binary or a browser publishes it).
All usable redirect rows of one client must belong to the same tier. If you need both a server-side and a device-side shape, split into two clients.
Common rejection causes
| Symptom | Cause |
|---|---|
| Authorization request rejected with an invalid redirect URI | Address not registered; or differs by a trailing slash / port (non-loopback) / host case |
invalid_grant at token exchange | The redirect_uri sent at exchange differs from the one sent in the authorization request |
| Works locally, fails in production | Local loopback http happened to match; the production address is not https or lacks a path |
| Mobile callback does not open the app | Scheme does not follow the com.muoceon.<bundle-id>://auth convention |