1
0
forked from wrenn/wrenn

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
This commit is contained in:
2026-04-21 11:03:12 +06:00
parent dbc6030c17
commit 6a6b489471
3 changed files with 178 additions and 20 deletions

View File

@ -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)
}