# Test OAuth callbacks on localhost

Many OAuth providers only accept redirect URIs that use HTTPS, and some reject `localhost` altogether. Expose gives your local app a public HTTPS URL that you can register as the redirect URI.

## 1. Share your app with a stable URL

The redirect URI you register with the provider has to match exactly, so use a URL that doesn't change. With [Expose Pro](https://expose.dev/#pricing):

```bash
expose share http://localhost:3000 --subdomain=my-app
```

Your app is now at a URL such as `https://my-app.eu-1.sharedwithexpose.com`. [Reserve the subdomain](https://expose.dev/docs/expose-network/reserved-subdomains) so nobody else can use it, or use your [own domain](https://expose.dev/docs/expose-network/custom-domains).

On the free plan the URL is random on each connection, so you would need to update the provider's settings every time.

## 2. Register the redirect URI

In the provider's developer console, add your callback URL to the allowed redirect URIs, for example:

```
https://my-app.eu-1.sharedwithexpose.com/auth/callback
```

## 3. Tell your app its public URL

Your app builds the redirect URI it sends to the provider, and it must be the same one you registered. Set the base URL your framework or auth library uses, for example `APP_URL` in Laravel or `AUTH_URL` in Auth.js, to the Expose URL while you test.

Then start the login from the public URL, not from `localhost`. Session and state cookies are set for the host you start on, and the callback has to arrive on the same host.

## 4. Watch the flow

The Expose dashboard at `http://127.0.0.1:4040` shows the callback request with its `code` and `state` parameters and your app's response, which makes it easy to see where a login fails.

## Troubleshooting

- **redirect_uri_mismatch.** The URI your app sends differs from the registered one. Compare scheme, host, path and trailing slash in the dashboard.
- **Invalid state.** You started the login on `localhost` and returned on the Expose URL. Start on the Expose URL.
- **Links go back to http.** Your app doesn't know it is behind HTTPS. Trust the forwarded headers in your framework's proxy settings.
