OAuth Callback on Localhost (Step-by-Step)
The OAuth localhost problem
OAuth providers (Google, GitHub, Auth0, Microsoft, etc.) redirect users back to your application after authentication. The redirect URI must match exactly what you registered — scheme, host, path, and trailing slash.
Many providers accept http://localhost:3000/callback in development mode. Others require HTTPS and a publicly reachable URL, especially when testing production-like flows, mobile OAuth, or provider apps that reject localhost entirely.
A development tunnel gives you a public HTTPS URL that forwards to your local OAuth callback handler.
Solution: tunnel + redirect URI
1. Start your app with the OAuth callback route implemented:
npm run dev
2. Start LocalToLink:
npx localtolink --port 3000
3. Register the redirect URI in your OAuth provider's console:
https://k7m2x9p4q1.localtolink.com/auth/callback
Use the exact subdomain LocalToLink prints. It changes on each new session.
4. Initiate the OAuth flow by visiting your app through the tunnel URL (not localhost) so redirect URIs match.
Redirect URI rules
OAuth providers enforce strict matching:
| Rule | Example |
|---|---|
| Scheme must match | https:// not http:// when registered as HTTPS |
| Host must match exactly | k7m2x9p4q1.localtolink.com ≠ localhost |
| Path must match | /auth/callback ≠ /auth/callback/ (trailing slash matters for some providers) |
| No wildcards (usually) | Cannot register *.localtolink.com — add each subdomain individually |
Practical tip: Register both localhost and tunnel URLs during development if your provider allows multiple redirect URIs. Switch between them depending on how you access the app.
Step-by-step OAuth flow through a tunnel
- User opens
https://SUBDOMAIN.localtolink.com/login - Your app redirects to the provider's authorization URL
- User authenticates with the provider
- Provider redirects to
https://SUBDOMAIN.localtolink.com/auth/callback?code=... - Your handler exchanges the code for tokens server-side
- User lands in your authenticated app
The callback request is a browser navigation — you may see the safety prompt on first visit (see below).
Browser interstitial warning
When the visitor interstitial policy is enabled, OAuth browser redirects may show LocalToLink's Opening a shared link page before your callback route runs. Continue once per subdomain, or use localtolink-skip-browser-warning in automated browser tests.
Server-side token exchange and provider webhook POSTs use non-browser clients and normally skip the interstitial. Only human navigations in a browser need the continue flow.
Provider-specific configuration
Google OAuth
- Open Google Cloud Console → APIs & Services → Credentials
- Edit your OAuth 2.0 Client ID
- Add under Authorized redirect URIs:
https://SUBDOMAIN.localtolink.com/auth/callback - Save and wait a few minutes for propagation
Google may also require the tunnel URL under Authorized JavaScript origins if your frontend makes direct API calls.
GitHub OAuth App
- GitHub → Settings → Developer settings → OAuth Apps
- Set Authorization callback URL to your tunnel URL
- Update when subdomain changes
For GitHub Apps (not OAuth Apps), configure the callback URL in the app settings similarly.
Auth0
- Auth0 Dashboard → Applications → Your App → Settings
- Add tunnel URL to Allowed Callback URLs
- Also add to Allowed Logout URLs and Allowed Web Origins if needed
Microsoft / Azure AD
Register the redirect URI under App registrations → Authentication → Redirect URIs. Microsoft requires exact HTTPS matches.
Combining OAuth with webhooks
If your app uses both OAuth and webhooks (common with Shopify, Slack apps), register the same tunnel subdomain for both:
| Setting | Example URL |
|---|---|
| Redirect URI | https://SUBDOMAIN.localtolink.com/auth/callback |
| Webhook URL | https://SUBDOMAIN.localtolink.com/webhooks/provider |
See Test Shopify Webhooks in Development for a combined OAuth + webhook example.
Limitations on the free tier
| Limitation | Impact |
|---|---|
| Random subdomain per session | Update provider settings when you restart the tunnel |
| 2-hour session limit | Restart tunnel and update redirect URI for longer sessions |
| No persistent URL | New session after reconnect grace = new subdomain |
| Custom subdomains | Not available on the free tier yet |
The 10-minute reconnect grace keeps the same URL after brief network drops. After that window, you get a new subdomain.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
redirect_uri_mismatch | URI not registered or typo | Copy exact URL from terminal; check trailing slash |
| OAuth works on localhost but not tunnel | Registered localhost only | Add tunnel URL to provider settings |
| Infinite redirect loop | Callback handler misconfigured | Verify callback route exchanges code correctly |
| Interstitial blocks OAuth | First visit to subdomain | Click through warning page once |
| Token exchange fails | Using wrong client secret | Confirm OAuth app credentials match environment |
FAQ
Can I use localhost and tunnel URLs interchangeably?
Only if both are registered and you initiate the flow from the matching origin.
Do mobile OAuth flows work?
Yes. Scan the terminal QR code to open the tunnel URL on your phone. See Test Localhost on Your Phone.
What about PKCE?
PKCE works through the tunnel — it is a client-side flow modification, not affected by the tunnel layer.
For n8n OAuth specifically, see n8n OAuth and Webhooks on Localhost.
Try it now
Start a tunnel from your project directory — no account required.
$ npx --yes localtolink@latest