From 6a6b489471ec6070e5028f71e909d23105e04cc4 Mon Sep 17 00:00:00 2001 From: pptx704 Date: Tue, 21 Apr 2026 11:03:12 +0600 Subject: [PATCH] feat: separate GitHub OAuth login/signup flows with name confirmation Block auto-account creation when signing in via GitHub from login mode. Signup via GitHub now shows a name confirmation dialog before redirecting to dashboard, letting users verify/edit their display name pulled from GitHub. - Add intent query param to OAuth redirect, persisted in HMAC-signed state cookie - Block registration in callback when intent=login, return no_account error - Set wrenn_oauth_new_signup cookie on new account creation - Frontend callback shows name confirmation dialog for new signups - Add no_account error message to login page --- .../routes/auth/github/callback/+page.svelte | 158 ++++++++++++++++-- frontend/src/routes/login/+page.svelte | 3 +- internal/api/handlers_oauth.go | 37 +++- 3 files changed, 178 insertions(+), 20 deletions(-) diff --git a/frontend/src/routes/auth/github/callback/+page.svelte b/frontend/src/routes/auth/github/callback/+page.svelte index 6ede837..17409b3 100644 --- a/frontend/src/routes/auth/github/callback/+page.svelte +++ b/frontend/src/routes/auth/github/callback/+page.svelte @@ -1,12 +1,18 @@ -
-

Signing you in...

-
+{#if showConfirmDialog} +
+
+
+

Almost there

+

+ We pulled your details from GitHub. Change your display name if you'd like. +

+ +
+ +
+ +
+
+ +
+ +
+
+ + +
+ +
+
+ +
+ +
+
+
+ + {#if nameError} +

{nameError}

+ {/if} + + +
+ +
+
+
+
+{:else} +
+

Signing you in...

+
+{/if} diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte index 18c55fa..afc00ab 100644 --- a/frontend/src/routes/login/+page.svelte +++ b/frontend/src/routes/login/+page.svelte @@ -29,6 +29,7 @@ access_denied: 'Access was denied by the provider', email_taken: 'An account with this email already exists', exchange_failed: 'Authentication failed — please try again', + no_account: 'No GitHub account connected — sign up instead', }; // Read OAuth error forwarded from /auth/github/callback @@ -259,7 +260,7 @@ diff --git a/internal/api/handlers_oauth.go b/internal/api/handlers_oauth.go index 9c9a14a..bc1ae73 100644 --- a/internal/api/handlers_oauth.go +++ b/internal/api/handlers_oauth.go @@ -55,8 +55,14 @@ func (h *oauthHandler) Redirect(w http.ResponseWriter, r *http.Request) { return } - mac := computeHMAC(h.jwtSecret, state) - cookieVal := state + ":" + mac + // Persist intent (login|signup) in the state cookie so the callback can enforce it. + intent := r.URL.Query().Get("intent") + if intent != "signup" { + intent = "login" + } + + mac := computeHMAC(h.jwtSecret, state+":"+intent) + cookieVal := state + ":" + mac + ":" + intent http.SetCookie(w, &http.Cookie{ Name: "oauth_state", @@ -105,13 +111,17 @@ func (h *oauthHandler) Callback(w http.ResponseWriter, r *http.Request) { Secure: isSecure(r), }) - parts := strings.SplitN(stateCookie.Value, ":", 2) - if len(parts) != 2 { + parts := strings.SplitN(stateCookie.Value, ":", 3) + if len(parts) < 2 { redirectWithError(w, r, redirectBase, "invalid_state") return } nonce, expectedMAC := parts[0], parts[1] - if !hmac.Equal([]byte(computeHMAC(h.jwtSecret, nonce)), []byte(expectedMAC)) { + intent := "login" + if len(parts) == 3 && parts[2] == "signup" { + intent = "signup" + } + if !hmac.Equal([]byte(computeHMAC(h.jwtSecret, nonce+":"+intent)), []byte(expectedMAC)) { redirectWithError(w, r, redirectBase, "invalid_state") return } @@ -249,6 +259,12 @@ func (h *oauthHandler) Callback(w http.ResponseWriter, r *http.Request) { return } + // Block auto-registration when intent is login-only. + if intent == "login" { + redirectWithError(w, r, redirectBase, "no_account") + return + } + // New OAuth identity — check for email collision. existingUser, err := h.db.GetUserByEmail(ctx, email) if err == nil { @@ -365,6 +381,17 @@ func (h *oauthHandler) Callback(w http.ResponseWriter, r *http.Request) { return } + // Signal frontend that this is a new signup so it can show the name confirmation dialog. + http.SetCookie(w, &http.Cookie{ + Name: "wrenn_oauth_new_signup", + Value: "1", + Path: "/auth/", + MaxAge: 60, + HttpOnly: false, + SameSite: http.SameSiteLaxMode, + Secure: isSecure(r), + }) + redirectWithToken(w, r, redirectBase, token, id.FormatUserID(userID), id.FormatTeamID(teamID), email, profile.Name) }