Skip to main content

Dashboard Sign-in Setup

The dashboard signs users in itself and keeps the session in an httpOnly cookie. Browser JavaScript never sees a token.

  • Sign in with GitHub: OAuth web flow with PKCE (S256). Only GitHub logins listed in ALLOWED_GITHUB_USERS get a session.
  • Username and password (fallback): the dashboard server forwards the form to the Observatory's POST /auth/login (CHAT_USERNAME / CHAT_PASSWORD).
  • Connect OpenRouter (optional, per user): OpenRouter's PKCE flow; the Observatory exchanges the code and stores the user's key.

Both sign-in methods end in the same session: an HS256 JWT signed with JWT_SECRET, with sub (username), iss and aud (github-backup), iat and exp. The Observatory accepts it when sub is CHAT_USERNAME or one of ALLOWED_GITHUB_USERS, so both services need the same JWT_SECRET and the same allowlist.


1. Pick the Public URL and Callback URLs​

Set APP_URL on the dashboard to the origin users open, for example https://github.mishrashardendu22.is-a.dev. It is required in production. The dashboard builds its callback URLs from it, and whether its cookies are Secure. When it is unset, both come from the request's Host, X-Forwarded-Host and X-Forwarded-Proto headers. That breaks behind proxies that do not send them, and behind a proxy that passes client-supplied values the client's headers decide them. Leave it unset only for local development.

ProviderCallback URLWhere it is registered
GitHub<APP_URL>/api/auth/github/callbackIn the GitHub App or OAuth App (next section)
OpenRouter<APP_URL>/api/auth/openrouter/callbackNowhere: the dashboard sends it with each authorization request

For local development the GitHub callback is http://localhost:3000/api/auth/github/callback.

Cookies are marked Secure on every origin except plain-HTTP loopback (http://localhost, http://127.0.0.1, http://[::1]). Serving the dashboard over plain HTTP on any other host (for example http://192.168.1.20:3000) therefore cannot keep a session; use HTTPS or localhost.


2. Create the GitHub OAuth Credentials​

Either a GitHub App or a classic OAuth App works; the dashboard only needs a client ID and a client secret, requests no scopes and throws the GitHub access token away after reading the login.

Option A: GitHub App​

  1. GitHub, Settings > Developer settings > GitHub Apps > New GitHub App.
  2. Homepage URL: APP_URL.
  3. Callback URL: <APP_URL>/api/auth/github/callback. A GitHub App can list several callback URLs, so the production and the localhost callback can share one app.
  4. Leave Request user authorization (OAuth) during installation and Enable Device Flow off, and untick Webhook > Active.
  5. Permissions: none. Reading the signed-in user's login needs no permission.
  6. Where can this GitHub App be installed?: Only on this account. The app does not have to be installed for sign-in.
  7. Create the app, copy the Client ID and generate a client secret.

Option B: OAuth App​

  1. GitHub, Settings > Developer settings > OAuth Apps > New OAuth App.
  2. Homepage URL: APP_URL.
  3. Authorization callback URL: <APP_URL>/api/auth/github/callback. An OAuth App has a single callback URL, so use a second OAuth App for localhost.
  4. Register the app, copy the Client ID and generate a client secret.

3. Set the Environment Variables​

All dashboard variables are read by the dashboard server at request time and never sent to the browser.

Dashboard (src/apps/github-backup-dashboard)​

VariableRequiredPurpose
GITHUB_OAUTH_CLIENT_IDFor GitHub sign-inClient ID from step 2. When set, the sign-in panels show Sign in with GitHub.
GITHUB_OAUTH_CLIENT_SECRETFor GitHub sign-inClient secret from step 2.
JWT_SECRETFor GitHub sign-in; recommended alwaysSame value as the Observatory's. Signs GitHub sessions and verifies every session locally. Without it, each session check (and each gated request) asks the Observatory's GET /auth/me.
ALLOWED_GITHUB_USERSFor GitHub sign-inComma-separated GitHub logins, compared case-insensitively. Empty refuses every GitHub login.
APP_URLRequired in productionPublic origin for callback URLs and the cookies' Secure flag, for example https://dash.example.com. Unset (local development only) means the request's origin.
JWT_EXPIRES_MINUTESNo (default 60)Lifetime of sessions created by GitHub sign-in; the cookie expires with the JWT. Password sessions last as long as the Observatory's token.
DASHBOARD_REQUIRE_LOGINNo (default false)true requires a session for every page and /api/proxy/* (see section 5). Unrecognised values count as true.
AGENT_API_URLAlready requiredThe Observatory; the password login, session checks and the OpenRouter exchange call it.
INTERNAL_SECRETAlready requiredSame value as the API's and the Observatory's. The password login sends it with the browser's address (X-Client-Address), so the Observatory limits failed logins per browser instead of per dashboard.
GITHUB_OAUTH_BASE_URL / GITHUB_API_BASE_URLNoDefault https://github.com / https://api.github.com. Only for GitHub Enterprise Server or a local test stub.

Generate a secret with openssl rand -base64 48 and use the same value on both services.

Observatory (src/services/github-backup-observatory)​

VariablePurpose
JWT_SECRETSame value as the dashboard's.
ALLOWED_GITHUB_USERSSame list as the dashboard's; the Observatory accepts a JWT whose sub is CHAT_USERNAME or one of these logins.
CHAT_USERNAME / CHAT_PASSWORDCredentials of the password fallback.
JWT_EXPIRES_MINUTESLifetime of password sessions.

Settings the Observatory needs for storing per-user OpenRouter keys are described in its own README.

Local Development​

Put the dashboard values in a gitignored src/apps/github-backup-dashboard/.env.local:

AGENT_API_URL=http://localhost:8000
BACKUP_API_URL=http://localhost:8080
APP_URL=http://localhost:3000
GITHUB_OAUTH_CLIENT_ID=<client id>
GITHUB_OAUTH_CLIENT_SECRET=<client secret>
JWT_SECRET=<same value as the Observatory>
ALLOWED_GITHUB_USERS=<your-github-login>

4. Routes and Cookies​

RouteMethodPurpose
/api/auth/github/start?returnTo=/pathGETStarts GitHub sign-in; returnTo must be a relative path on the dashboard (default /ai).
/api/auth/github/callbackGETGitHub redirects here; sets the session cookie and returns to returnTo.
/api/auth/passwordPOSTForm fields username, password; the Observatory login, done by the server.
/api/auth/sessionGET{authenticated, username, method, expiresAt, providers: {github}}.
/api/auth/logoutPOSTDeletes the session cookie (same-origin requests only).
/api/auth/openrouter/startGETStarts "Connect OpenRouter" (requires a session).
/api/auth/openrouter/callbackGETOpenRouter redirects here; the Observatory exchanges the code; lands on /settings.
/api/healthGETLiveness check, public even with the gate on.
CookiePathLifetimeContent
ghb_session/The JWT's lifetimeSession JWT
ghb_github_oauth/api/auth/github10 minutesOAuth state, PKCE verifier, return path
ghb_openrouter_pkce/api/auth/openrouter10 minutesPKCE verifier

All three are HttpOnly and SameSite=Lax, and Secure except on plain-HTTP loopback origins.

The Observatory proxy (/api/proxy/agent/*) sends the session JWT as Authorization: Bearer <jwt>, ignores any Authorization header from the browser, and refuses POST/PATCH/DELETE requests that do not come from the dashboard's own pages (Sec-Fetch-Site / Origin check). The Connections page (/settings) shows the signed-in account and the OpenRouter status, and connects or disconnects the key.


5. Optional Login Gate​

By default the dashboard stays viewable without signing in; only the AI features need a session. With DASHBOARD_REQUIRE_LOGIN=true:

  • Pages redirect signed-out visitors to /signin?returnTo=<page>.
  • /api/proxy/* answers 401 JSON (AUTH_REQUIRED), or 503 when the Observatory cannot confirm a session.
  • The landing page /, /api/auth/*, /api/health, /signin and static assets (/_next/* and files with an extension under public/) stay public. The overview at /dashboard and every other page are gated.

Set JWT_SECRET on the dashboard when the gate is on; otherwise every gated request costs one call to the Observatory. Point container health checks at /api/health rather than a page. After sign-in with no valid returnTo, users land on /dashboard.


6. Troubleshooting​

SymptomCause
"GitHub sign-in is not configured" (503)GITHUB_OAUTH_CLIENT_ID, GITHUB_OAUTH_CLIENT_SECRET or JWT_SECRET is missing on the dashboard; the page names the missing ones.
"GitHub sign-in is disabled" (403)ALLOWED_GITHUB_USERS is empty on the dashboard.
"This GitHub account is not allowed" (403)The login is not in ALLOWED_GITHUB_USERS.
GitHub reports that the redirect_uri is not associated with the applicationThe registered callback URL differs from <APP_URL>/api/auth/github/callback, or APP_URL is unset behind a proxy.
"Sign-in could not be verified" (400)The sign-in took longer than 10 minutes, was started in another browser, or cookies are blocked.
"The Observatory issued a session this dashboard cannot verify"JWT_SECRET differs between the services, or the Observatory issues tokens without iss/aud.
Signed in, but the first AI request ends the session ("Your session expired or was not accepted by the Observatory")The Observatory rejects the JWT: different JWT_SECRET, the GitHub login missing from the Observatory's ALLOWED_GITHUB_USERS, or an Observatory build without GitHub-user support.
Sign-in works on localhost but not on a LAN addressSession cookies are Secure; use HTTPS.

7. Migrating From the Shared-Password Login​

  • Earlier builds kept the JWT in localStorage (agent_token, agent_username). The dashboard deletes those keys on the first page load, so every existing user is signed out once and signs in again.
  • /api/proxy/agent/auth/login and /api/proxy/agent/auth/me are no longer forwarded; sign-in goes through /api/auth/*.
  • Deploy the Observatory with the matching JWT_SECRET and ALLOWED_GITHUB_USERS before enabling GitHub sign-in, or GitHub sessions are rejected by the Observatory.