Skip to content

Repository files navigation

my.gladysassistant.com

The shortcut to your own Gladys Assistant instance.

This is a static site, deployed on Cloudflare Pages. It has no backend, no database and no analytics. Its only job is to redirect a browser to the Gladys instance running on the visitor's own network.

Why it exists

OAuth2 providers now require the redirect URI of an application to be HTTPS. Spotify, for instance, enforces it since April 2025: only https://... is accepted, plus the loopback literals http://127.0.0.1:PORT and http://[::1]:PORTlocalhost is refused, and so is any LAN address such as http://192.168.1.50:1443.

That is exactly how most people run Gladys at home: a local IP, over plain HTTP, with no certificate. There is no way to satisfy the provider from there.

So https://my.gladysassistant.com/redirect/oauth is registered as the redirect URI instead. It is served over HTTPS, the provider accepts it, and it bounces the browser back to the instance. This is the same approach Home Assistant takes with my.home-assistant.io.

The flow

Gladys (http://192.168.1.50:1443)
  │  1. the user clicks "Connect" on an integration
  │     the frontend wraps the integration's state with its own origin
  ▼
Provider (Spotify…)
  │  2. the user grants access
  │     redirect_uri = https://my.gladysassistant.com/redirect/oauth
  ▼
my.gladysassistant.com/redirect/oauth
  │  3. unwraps the state, checks the target, shows it, waits for a click
  ▼
Gladys (http://192.168.1.50:1443/dashboard/…/oauth-callback?code=…&state=…)
     4. the instance verifies the state and exchanges the code itself

Step 4 never leaves the local network: the token exchange is made by the Gladys instance, with its own client secret. No token or credential ever transits through this page, and nothing is stored here — everything happens in the visitor's browser.

To be precise rather than reassuring: the provider redirect does carry the authorization code in the query string of a request to this host, so the static hosting in front of it sees that URL. No code runs server-side, nothing is logged on purpose, the query string is stripped from the address bar once read, and the code is useless on its own — the exchange needs the client secret, which never leaves the instance. Do not claim that no data ever touches Gladys-operated infrastructure; users who want none at all can point their instance at its own HTTPS callback instead.

The wrapped state

The provider hands the state parameter back untouched, so it is the only way to carry the instance address across the round trip. Before starting the flow, the Gladys frontend builds:

state = base64url(
  JSON.stringify({
    v: 1,
    origin: 'http://192.168.1.50:1443',
    path: '/dashboard/integration/device/external/ext-dev-spotify/oauth-callback',
    state: '<the anti-CSRF state generated by the integration>',
  }),
);
Field Description
v Format version, currently 1.
origin Origin of the instance, as the user reaches it. No path, no query, no credentials.
path Absolute path of the callback route inside the instance.
state The state generated by the integration. Only the instance can verify it.

This page then redirects to origin + path, forwarding every query parameter of the provider (code, but also error, scope… which vary between providers) and putting the inner state back in place. The instance sees exactly what it would have seen from a direct redirect.

assets/js/oauth-state.js is the reference implementation of the format, and exports encodeWrappedState() for clients.

Security model

The page is deliberately narrow:

  • Redirect targets are restricted. Any https:// origin is accepted (the user set up TLS themselves), but a plain http:// origin is only accepted when it can exist solely on the visitor's own network: RFC 1918 ranges, loopback, link-local, the CGNAT range Tailscale uses, IPv6 ULA/link-local, localhost, .local/.lan/.home/.internal/.home.arpa, and single-label hostnames. Anything else is refused, so this cannot become an open redirect that leaks an authorization code to a public host over plain HTTP.
  • The redirect is never automatic. The visitor sees the target address and clicks. Nothing is forwarded silently.
  • Paths cannot escape the origin. No //, no backslash, no query, no fragment, and the rebuilt URL is checked against the declared origin.
  • The query string is stripped from the address bar once read, so the authorization code does not linger in history or in a screenshot.
  • Nothing third-party runs on this origin. _headers sets a strict CSP (default-src 'none', script-src 'self', connect-src 'none', frame-ancestors 'none'), Referrer-Policy: no-referrer and Cache-Control: no-store on /redirect/*. Keep it that way: no CDN font, no analytics, no tag manager. A script on this origin could read the authorization code.
  • No cookie may be set on the apex domain gladysassistant.com, or it would be sent to this subdomain too.

Development

No build step, no dependency. Serve the repository root with any static server:

python3 -m http.server 8000
# then open http://127.0.0.1:8000

Run the tests (Node.js 18+):

npm test

To exercise the redirect page by hand, generate a state:

node -e "import('./assets/js/oauth-state.js').then(({ encodeWrappedState }) => \
  console.log(encodeWrappedState({ origin: 'http://192.168.1.50:1443', \
    path: '/dashboard/integration/device/external/ext-dev-spotify/oauth-callback', \
    state: 'demo' })))"

then open /redirect/oauth?code=demo-code&state=<the output>.

Add ?lang=fr or ?lang=en to any page to force a language.

Deployment

Cloudflare Pages, from the main branch:

Setting Value
Framework preset None
Build command (empty)
Build output / (repository root)

_headers is applied by Cloudflare Pages automatically. 404.html is served for unknown paths.

Adding new redirects later

/redirect/oauth is the first entry of a namespace meant to grow: future pages such as /redirect/integration/<selector> can open a given page of the visitor's own instance from the documentation, the blog or the community forum. Any new redirect target must reuse the origin checks in assets/js/oauth-state.js.

License

Apache-2.0, like Gladys Assistant.

About

Static website for URL redirection

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages