Skip to content

Electron App Deep Dive

Electron apps are Chromium + Node.js packaged into a desktop shell. The JS bundle is readable, the storage is standard Chromium formats, and the auth tokens are often sitting in a JSON file. Once you know where to look, Electron is one of the easiest desktop targets.

Part of Layer 6: Desktop Apps. See also 3-auth for general auth patterns.


Terminal window
ls /Applications/SomeApp.app/Contents/Resources/
# Look for: app.asar ← bundled JS/HTML/CSS
# app/ ← unpacked (less common)
file /Applications/SomeApp.app/Contents/MacOS/SomeApp
# Should reference Electron framework

Terminal window
# One-shot: extract app.asar to /tmp/app
npx @electron/asar extract /Applications/SomeApp.app/Contents/Resources/app.asar /tmp/app
ls /tmp/app
# Typical: dist-electron/ dist-app/ node_modules/ package.json

The bundle is minified but readable. Variable names are mangled; string literals (URLs, endpoint paths, header names) are not minified. Use these to navigate.

Terminal window
grep -o "[a-zA-Z]*\.example\.com[^\"']*" /tmp/app/dist-electron/main/index.js | sort -u
Terminal window
grep -o "[a-z-]*\.example\.com" /tmp/app/dist-electron/main/index.js | sort -u
Terminal window
# Look for Authorization, X-Client-*, bearer
grep -o ".{0,150}Authorization.{0,150}" /tmp/app/dist-electron/main/index.js | head -10

All Electron app data lives in:

~/Library/Application Support/<AppName>/
File / DirWhat it contains
*.json filesAuth tokens, config, feature flags
CookiesSQLite — Chromium encrypted cookies (usually empty in Electron)
Local Storage/leveldb/LevelDB — localStorage, sometimes tokens
IndexedDB/file__0.indexeddb.leveldb/IndexedDB — app state, can contain tokens
PreferencesJSON — per-profile settings

Electron apps typically store auth in JSON files, not browser cookies, because the main process (Node.js) writes them directly without going through Chromium’s cookie jar.


Terminal window
for f in ~/Library/Application\ Support/AppName/*.json; do
echo "=== $f ===" && python3 -c "
import json, sys
with open('$f') as f: d = json.load(f)
def walk(obj, p=''):
if isinstance(obj, dict):
for k,v in obj.items(): walk(v, p+'.'+k)
elif isinstance(obj, str) and len(obj) > 20:
print(f' {p}: {obj[:60]}')
walk(d)
"
done
Terminal window
# JWTs start with eyJ (base64url of {"alg":...)
grep -r "eyJ" ~/Library/Application\ Support/AppName/ --include="*.json" -l
import base64, json
def decode_jwt(token):
parts = token.split('.')
def b64d(s):
s += '=' * (4 - len(s) % 4)
return json.loads(base64.urlsafe_b64decode(s))
return b64d(parts[0]), b64d(parts[1]) # header, payload
header, payload = decode_jwt(token)
print("iss:", payload.get("iss")) # who issued it
print("exp:", payload.get("exp")) # expiry
print("claims:", list(payload.keys()))

The iss field tells you the auth provider (WorkOS, Supabase, Auth0, Okta, etc.) and which client ID / tenant.


Most Electron APIs reject requests missing client identification headers. Find them by searching the bundle for the header-building function:

Terminal window
# Common patterns: X-Client-*, X-App-*, platform, device-id
grep -o ".{0,100}X-Client.{0,200}" /tmp/app/dist-app/assets/operationBuilder.js | head -5

Typical Electron API headers:

HeaderExampleNotes
X-Client-Version7.71.1App version from package.json
X-Client-Platform / X-Granola-PlatformdarwinOS platform
X-Workspace-IdUUIDMulti-tenant identifier
X-Device-IdUUIDPersisted device fingerprint

Without these, the server may return {"message":"Unsupported client"} even with a valid token.

Get the version:

Terminal window
cat /tmp/app/package.json | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['version'])"

Auth Migration Pattern (Supabase → WorkOS)

Section titled “Auth Migration Pattern (Supabase → WorkOS)”

Many Electron apps launched with Supabase Auth and later migrated to WorkOS (or Clerk, Auth0, etc.) for enterprise SSO. The telltale sign:

~/Library/Application Support/AppName/supabase.json ← filename from v1
→ contents: { "workos_tokens": "...", "user_info": ... } ← migration artifact

The filename is kept for backward compatibility, but the contents changed. The old Supabase user UUID is preserved as external_id in the new JWT so database foreign keys don’t break.

How to detect a migration:

import json
with open("supabase.json") as f:
d = json.load(f)
if "workos_tokens" in d:
print("Migrated to WorkOS — parse workos_tokens as JSON for the JWT")
elif "access_token" in d:
print("Still on Supabase — access_token is the JWT directly")
elif "session" in d:
print("Supabase session object — check session.access_token")

See workos.md for the full WorkOS token model.


CrossAppAuth — Desktop ↔ Web Session Handoff

Section titled “CrossAppAuth — Desktop ↔ Web Session Handoff”

Some Electron apps share a session between the desktop client and the web app without requiring a separate login. The pattern:

  1. User logs in on the web app (browser)
  2. Desktop app detects the session (via deep link, polling, or IPC)
  3. Desktop calls an auth-handoff-complete-style endpoint with the web session
  4. Server mints a new desktop token (different expiry, different claims)

You’ll see this as sign_in_method: "CrossAppAuth" in the JWT payload, or as an endpoint like /v1/auth-handoff-complete in the app bundle.

To find:

Terminal window
grep -o "[^\"]*auth.handoff[^\"]*\|[^\"]*cross.app[^\"]*" /tmp/app/dist-electron/main/index.js

Electron apps frequently gate features behind server-controlled flags stored in local-state.json or a similar config file:

import json
with open("local-state.json") as f:
d = json.load(f)
flags = d.get("featureFlags", {})
for k, v in flags.items():
print(f" {k}: {v}")

If an API endpoint returns 403 Forbidden or {"enabled": false} even with a valid token, check whether there’s a feature flag that needs to be true. Some flags are user-controlled (toggle in Settings), others are server-pushed and require a plan upgrade.


Electron apps can use Chromium cookies and localStorage, but most don’t — the Node.js main process writes tokens directly to JSON files instead.

If you do find a populated Cookies database, decrypt it the same way as Brave or Chrome:

Terminal window
# Check if there's a Keychain entry
security find-generic-password -s "AppName Safe Storage" -a "AppName" -w
# Cookies database
sqlite3 ~/Library/Application\ Support/AppName/Cookies \
"SELECT name, host_key FROM cookies LIMIT 20;"

See the skills/brave-browser/ skill for the full Chromium cookie decryption pipeline (PBKDF2 + AES-128-CBC).


□ Find app.asar and extract it
□ Grep for all subdomains and API endpoints
□ Find the header-building function → identify required custom headers
□ Scan ~/Library/Application Support/<App>/*.json for tokens
□ Decode any JWT → check iss, exp, claims
□ Detect auth migration (supabase.json but workos_tokens key?)
□ Test token against a known-working endpoint with correct headers
□ Check for feature flags gating the feature you need