Why No client_secret in the Binary

Desktop applications are "public clients" under OAuth 2.0 — anything compiled into the installer can be extracted by anyone who downloads it. Embedding the client_secret in the app binary would allow it to be harvested and used to impersonate the application with Google's API.

To avoid this, What's Happening? uses PKCE (Proof Key for Code Exchange, RFC 7636) for the user-facing authorization step, combined with a minimal stateless proxy that holds the client_secret server-side and injects it only at the token exchange step. The app binary contains no secret.

How PKCE Works

Before starting the sign-in flow, the app generates two values locally on your device:

Google stores the challenge server-side when the user authenticates. When the app later presents the code_verifier to exchange for tokens, Google recomputes the SHA-256 hash and checks that it matches the stored challenge. Only the device that started the authorization flow — the one that generated the original code_verifier — can complete it, even if the authorization code is intercepted in transit.

State Parameter & CSRF Protection

Alongside the PKCE values, the app generates a separate random state token (32 bytes, base64url-encoded — same method as the code_verifier) and includes it in the authorization URL sent to Google.

Google echoes the state value back in the redirect to the app's local callback server. The app verifies the returned state matches the one it generated before accepting the authorization code. A mismatch causes the flow to abort with a 400 error. This guard is specified in RFC 6749 §10.12 as protection against cross-site request forgery.

The Happening Proxy never sees the state parameter — it is exchanged only between the app, the system browser, and Google's authorization endpoint.

The Happening Proxy

Google's token endpoint requires a client_secret even when PKCE is used. The proxy bridges this gap without requiring the secret to live in the app binary.

The proxy is a minimal Dart service deployed as a Docker container. It exposes a single endpoint — POST /token — and does exactly one thing: it reads the client_secret from a server-side environment variable, appends it to the incoming token request, and forwards the request to https://oauth2.googleapis.com/token. Google's response is returned to the app unchanged.

The proxy is fully stateless:

The client_secret is the only piece of information the proxy holds, and it is read from the deployment environment — not from any database or storage layer. Tokens returned by Google pass through the proxy in a single response body and are immediately written to your device's OS keychain via flutter_secure_storage. The proxy does not retain a copy.

The same proxy endpoint is used for automatic token refresh. When your access token expires, the googleapis_auth library silently obtains a new one via the stored refresh token — routed through the proxy in the same way. Your calendar data is never involved in this exchange.

Authorization Flow Diagram

The sequence below shows every step of the OAuth handshake, including which party holds which value at each stage.

PKCE OAuth Handshake PKCE OAuth Handshake 2. Launch auth URL GET /o/oauth2/auth ?code_challenge=...&state=... &scope=calendar.readonly 302 Redirect localhost:{port}?code=AUTH_CODE& state=CSRF_TOKEN 3. GET localhost:{port} ?code=AUTH_CODE& state=CSRF_TOKEN 5. POST /token code=AUTH_CODE, code_verifier [no client_secret in request] POST oauth2.googleapis.com/token + client_secret (injected by proxy) 6. HTTP 200 { access_token, refresh_token } Tokens forwarded unchanged App (local) System Browser Google OAuth Happening Proxy App (local) System Browser Google OAuth Happening Proxy 1. Generate locally: code_verifier = (32 random bytes) code_challenge = SHA256(cv) state = (CSRF token) redirect_uri = localhost:{port} User authenticates 4. Verify returned state == step 1. (abort with 400 on mismatch) Reads client_secret from K3 Secret. Injects it into request. Verifies SHA256(code_verifier) == code_challenge; validates code + secret. Stateless — stores nothing. No calendar data access. 7. flutter_secure_storage -> OS keychain (Keychain / CredMgr / libsecret)

Source Code

The proxy and app authentication code are open source and auditable: