Skip to content

Connect intervals.icu by signing in (OAuth), keeping API keys - #15

Open
stretch0 wants to merge 2 commits into
mainfrom
intervals-oauth
Open

stretch0 wants to merge 2 commits into
mainfrom
intervals-oauth

Conversation

@stretch0

@stretch0 stretch0 commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

Members can connect intervals.icu with a Connect with intervals.icu button instead of pasting an API key, on servers that have registered an intervals.icu OAuth app. Pasting a key still works everywhere, and it's the only option on servers without an app.

Setting it up on a server

  1. Apply at https://intervals.icu/oauth/apply with the redirect URI <ORIGIN>/api/integrations/intervals/oauth/callback.
  2. Once intervals.icu approves it, set INTERVALS_CLIENT_ID and INTERVALS_CLIENT_SECRET and restart.

Each server needs its own app, because intervals.icu ties an app to its redirect URIs. This is in the README (new Watch sync section) and .env.example.

How it works

  • integration.auth_method (api_key or oauth, migration 0009_intervals_oauth, defaults to api_key) says how the member connected. The key or token is stored encrypted in credentials either way, and the client sends Basic or Bearer to match.
  • Settings is a plain GET form to /api/integrations/intervals/oauth/start. That route keeps the state, PKCE verifier, member and start date in a signed, HttpOnly cookie for 10 minutes, then redirects to intervals.icu asking for ACTIVITY:READ only.
  • The callback:
    • checks that the state and member match;
    • swaps the code for a token;
    • saves the connection, with the same one-account-per-person rule as keys;
    • starts the first sync in the background;
    • sends the member to /settings?intervals=connected|denied|expired|taken|failed, and Settings shows a message for each.
  • Disconnecting an OAuth connection revokes the token (DELETE /api/v1/disconnect-app). So does losing the "already connected to someone else" race.
  • When OAuth is available, a member whose key stops working can paste a new one or switch to signing in.
  • Settings polls while a sync runs, so the background sync after the redirect shows up without a reload.
  • The PWA's offline cache now skips navigations under /api, so the sign-in redirects always reach the server.
  • PRIVACY.md and the in-app privacy page now describe both kinds of access: a sign-in can only read activities, while a key has full access.

Testing

  • API tests for the OAuth flow:
    • only offered with an app;
    • connects and syncs with a Bearer token;
    • denied, forged state, missing cookie, another member's browser, and a bad code;
    • one account per person, with the spare token revoked;
    • switching a key connection to OAuth keeps the start date and runs.
  • The e2e fake intervals.icu now approves sign-ins straight away. The sync e2e test connects through the sign-in, disconnects, then connects with an API key.
  • Checked the Settings card in the production build in light and dark mode.

Not tested against the real intervals.icu, because that needs a registered and approved app. The requests follow the OAuth docs.

Demo mode: OAuth is off on the demo, where everyone shares one login, and the demo's limits also block the GET that starts the sign-in.

🤖 Generated with Claude Code

Andrew McCallum and others added 2 commits October 30, 2026 14:18
Servers that register an intervals.icu OAuth app and set
INTERVALS_CLIENT_ID and INTERVALS_CLIENT_SECRET offer a "Connect with
intervals.icu" button that asks only to read activities. Pasting an API
key still works everywhere, and is all there is on servers without an
app. Existing connections keep working (auth_method defaults to api_key).

The sign-in uses PKCE and a signed, short-lived cookie for the state.
Disconnecting revokes the token. Settings refreshes while a sync runs, so
the first sync after the redirect shows up, and the PWA no longer caches
navigations under /api.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The OAuth migration is regenerated as 0009_intervals_oauth, after main's
0008_club_logo. Demo mode turns OAuth off (everyone shares one login),
and its limits also block the GET that starts the intervals.icu sign-in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant