Accounts Phase 2: Google sign-in (OAuth 2.0 / OIDC)

- oauth_google.py (stdlib): PKCE, auth URL, code exchange, ID-token claim
  validation (iss/aud/exp/email_verified — token comes straight from Google's
  token endpoint over TLS, so no signature re-verify / JWKS needed).
- API: GET /api/auth/google/start (302 to Google, PKCE + signed state cookie
  binding the flow to the browser) and /callback (CSRF-checked state, exchange,
  find-or-create by verified email → links to an existing magic-link account,
  session cookie, redirect home). Errors land on /auth/verify?error=google.
- SignIn modal: "Continue with Google" + an "or email link" divider.
- 112 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-03 01:31:52 +00:00
parent 28dc79d0b7
commit b635d8f574
5 changed files with 280 additions and 5 deletions
+48 -3
View File
@@ -35,10 +35,21 @@
{:else}
<h2>Sign in to Upbeat Bytes</h2>
<p class="sub">
Save articles and keep your history across devices. We'll email you a one-time
link — no password to remember.
Save articles and keep your history across devices.
</p>
<a class="google" href="/api/auth/google/start">
<svg width="18" height="18" viewBox="0 0 18 18" aria-hidden="true">
<path fill="#4285F4" d="M17.64 9.2c0-.64-.06-1.25-.16-1.84H9v3.48h4.84a4.14 4.14 0 0 1-1.8 2.72v2.26h2.92c1.71-1.57 2.68-3.89 2.68-6.62z"/>
<path fill="#34A853" d="M9 18c2.43 0 4.47-.8 5.96-2.18l-2.92-2.26c-.81.54-1.84.86-3.04.86-2.34 0-4.32-1.58-5.03-3.7H.96v2.33A9 9 0 0 0 9 18z"/>
<path fill="#FBBC05" d="M3.97 10.72a5.4 5.4 0 0 1 0-3.44V4.95H.96a9 9 0 0 0 0 8.1l3.01-2.33z"/>
<path fill="#EA4335" d="M9 3.58c1.32 0 2.5.45 3.44 1.35l2.58-2.58C13.46.9 11.43 0 9 0A9 9 0 0 0 .96 4.95l3.01 2.33C4.68 5.16 6.66 3.58 9 3.58z"/>
</svg>
Continue with Google
</a>
<div class="or"><span>or email me a one-time link — no password</span></div>
<form onsubmit={submit}>
<input
type="email"
@@ -53,7 +64,6 @@
</form>
{#if status === 'error'}<p class="err">{error}</p>{/if}
<!-- Phase 2: "Continue with Google" button slots in here -->
<p class="fine">No account needed to browse — signing in just saves your stuff.</p>
{/if}
</div>
@@ -135,6 +145,41 @@
opacity: 0.6;
cursor: default;
}
.google {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
width: 100%;
padding: 11px 16px;
border: 1px solid var(--line);
border-radius: 999px;
background: #fff;
color: var(--ink);
font-weight: 600;
font-size: 0.95rem;
cursor: pointer;
transition: background 0.14s ease, border-color 0.14s ease;
}
.google:hover {
background: #f7f7f5;
border-color: #d9d3c6;
}
.or {
display: flex;
align-items: center;
gap: 12px;
margin: 16px 0;
color: var(--muted);
font-size: 0.78rem;
}
.or::before,
.or::after {
content: '';
flex: 1;
height: 1px;
background: var(--line);
}
.err {
margin: 10px 0 0;
color: #9a3b3b;