OAuth Redirect URIs on Localhost — What Actually Works
The redirect URI problem
You configured Google, GitHub, or Auth0 with:
http://localhost:3000/auth/callback
Some providers accept localhost. Many require HTTPS in production-like settings. Mobile OAuth flows and certain enterprise IdPs reject plain HTTP entirely.
The fix is the same as webhooks: give your local server a public HTTPS URL that forwards to localhost.
Quick setup
npm run dev
npx localtolink
Register the tunnel URL as your redirect URI:
https://YOUR-SUB.localtolink.com/auth/callback
The path must match your app's callback route exactly — including trailing slashes.
Common OAuth errors
| Error | Cause | Fix |
|---|---|---|
redirect_uri_mismatch | URI does not match dashboard config | Copy-paste exact tunnel URL + path |
| Callback hits wrong port | Tunnel points elsewhere | Check --port matches your app |
| State cookie lost | Secure cookie on wrong domain | Set cookie domain/path correctly |
| Works locally, fails on phone | Phone cannot reach localhost | Use tunnel URL in mobile OAuth test |
Provider notes
Google OAuth — Add both localhost and tunnel URLs during development, or use tunnel URL only. Authorized redirect URIs must match exactly.
GitHub OAuth App — Authorization callback URL is a single field. Update when tunnel subdomain changes.
Auth0 — Add tunnel URL to Allowed Callback URLs. Wildcards do not help if path differs.
PKCE and SPA flows through a tunnel
Single-page apps using PKCE (Proof Key for Code Exchange) generate a code verifier locally and exchange it at the token endpoint after redirect. The tunnel does not participate in PKCE math — it only forwards the redirect carrying the authorization code.
Ensure your OAuth library builds the redirect URI from the current browser origin. When testing through a tunnel, open the app at https://YOUR-SUB.localtolink.com, not http://localhost:3000, so the redirect URI your app sends matches what you registered in the provider dashboard.
Mismatch errors (redirect_uri_mismatch) are almost always copy-paste or trailing-slash differences, not tunnel bugs.
Multi-provider development setup
Apps with Google + GitHub + Auth0 need each provider updated when the subdomain changes:
| Provider | Dashboard field | Example |
|---|---|---|
| Authorized redirect URIs | https://YOUR-SUB.localtolink.com/auth/google/callback | |
| GitHub | Authorization callback URL | https://YOUR-SUB.localtolink.com/auth/github/callback |
| Auth0 | Allowed Callback URLs | Same pattern per connection |
Start the tunnel before opening provider consoles so you copy the URL once. A spreadsheet of "current tunnel URL" during active OAuth work saves repeated dashboard hunting.
Cookies, sessions, and the tunnel domain
After OAuth callback, your session cookie must work when the user continues browsing on the tunnel domain. Common fixes:
- Set
SameSite=Laxfor OAuth redirects (default in most frameworks) - Do not hard-code cookie
Domain=localhostwhen users access via tunnel - If using secure cookies, the tunnel URL is already HTTPS —
Secureflag is fine
Test the full login → protected page → logout cycle through the tunnel URL, not just the callback hop.
Keep redirect URIs stable during a session
Tunnel subdomains change on restart. During an OAuth dev session:
- Start tunnel before opening the provider dashboard
- Register the URL once
- Complete your flow testing within the 2-hour session
- Update provider config when you restart the tunnel
For longer OAuth work, some teams use a fixed staging callback that proxies to localhost — heavier setup, stable URL.
Security notes
- Use development OAuth credentials, not production client IDs
- Do not commit client secrets; use environment variables
- Stop the tunnel when not actively testing — the callback URL is publicly reachable
- Validate
stateparameter to prevent CSRF
Honest limits
LocalToLink gives random subdomains on free tier. You will update provider dashboards when URLs change. Custom domains are not available on free tier yet.
FAQ
Can I use localhost without a tunnel?
Sometimes, for providers that allow http://localhost. HTTPS tunnel URLs work more broadly.
Does the tunnel break PKCE?
No. PKCE runs in your app; the tunnel only forwards HTTP.
What about cookie SameSite issues?
Ensure your session cookies work when the browser address bar shows the tunnel domain during callback.
Does Microsoft Entra / Azure AD work?
Yes — register the tunnel redirect URI in app registration the same way as other providers. Enterprise tenants may require HTTPS callbacks; tunnel URLs satisfy that requirement.
Can I test mobile OAuth through the tunnel?
Yes. Open the app on your phone via the tunnel URL or QR code. The redirect URI must match what the mobile browser sends — usually the same HTTPS tunnel URL you registered.
n8n and Shopify OAuth patterns
Automation tools use fixed callback paths. n8n expects:
https://YOUR-SUB.localtolink.com/rest/oauth2-credential/callback
Shopify apps typically use:
https://YOUR-SUB.localtolink.com/auth/callback
Register the exact path your framework documents. OAuth libraries rarely let you change callback paths without code changes — the tunnel must match the app, not the other way around.
Next steps
- OAuth callback on localhost — provider-specific steps
- Why webhooks fail on localhost — same networking problem
- CLI reference — tunnel flags and limits
Try it now
Start a tunnel from your project directory — no account required.
$ npx localtolink