Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol diffdown.com

Redirect to original URL after ATProto login

Store the ?next= param through the OAuth round-trip via the session
so collaborators land on their invite URL after signing in.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

+21 -5
+19 -5
internal/handler/atproto.go
··· 47 47 48 48 // ATProtoLoginPage renders the handle-entry form. 49 49 func (h *Handler) ATProtoLoginPage(w http.ResponseWriter, r *http.Request) { 50 - h.render(w, "atproto_login.html", PageData{Title: "Sign in with Bluesky"}) 50 + next := r.URL.Query().Get("next") 51 + h.render(w, "atproto_login.html", PageData{Title: "Sign in with Bluesky", Next: next}) 51 52 } 52 53 53 54 // ATProtoLoginSubmit starts the ATProto OAuth flow. 54 55 func (h *Handler) ATProtoLoginSubmit(w http.ResponseWriter, r *http.Request) { 55 56 handle := strings.TrimSpace(r.FormValue("handle")) 57 + next := r.FormValue("next") 56 58 if handle == "" { 57 - h.render(w, "atproto_login.html", PageData{Title: "Sign in with Bluesky", Error: "Handle is required"}) 59 + h.render(w, "atproto_login.html", PageData{Title: "Sign in with Bluesky", Error: "Handle is required", Next: next}) 58 60 return 59 61 } 60 62 ··· 62 64 did, err := atproto.ResolveHandle(handle) 63 65 if err != nil { 64 66 log.Printf("ATProto: resolve handle %s: %v", handle, err) 65 - h.render(w, "atproto_login.html", PageData{Title: "Sign in with Bluesky", Error: "Could not resolve handle — check it and try again"}) 67 + h.render(w, "atproto_login.html", PageData{Title: "Sign in with Bluesky", Error: "Could not resolve handle — check it and try again", Next: next}) 66 68 return 67 69 } 68 70 69 71 pds, err := atproto.ResolvePDS(did) 70 72 if err != nil { 71 73 log.Printf("ATProto: resolve PDS for %s: %v", did, err) 72 - h.render(w, "atproto_login.html", PageData{Title: "Sign in with Bluesky", Error: "Could not reach your PDS"}) 74 + h.render(w, "atproto_login.html", PageData{Title: "Sign in with Bluesky", Error: "Could not reach your PDS", Next: next}) 73 75 return 74 76 } 75 77 76 78 meta, err := atproto.FetchAuthServerMeta(pds) 77 79 if err != nil { 78 80 log.Printf("ATProto: fetch auth meta from %s: %v", pds, err) 79 - h.render(w, "atproto_login.html", PageData{Title: "Sign in with Bluesky", Error: "PDS does not support ATProto OAuth"}) 81 + h.render(w, "atproto_login.html", PageData{Title: "Sign in with Bluesky", Error: "PDS does not support ATProto OAuth", Next: next}) 80 82 return 81 83 } 82 84 ··· 179 181 sess.Values["atproto_did"] = did 180 182 sess.Values["atproto_handle"] = handle 181 183 sess.Values["atproto_pds_url"] = pds 184 + if next != "" { 185 + sess.Values["atproto_next"] = next 186 + } 182 187 sess.Save(r, w) 183 188 184 189 authURL := meta.AuthorizationEndpoint + "?client_id=" + url.QueryEscape(clientID) + "&request_uri=" + url.QueryEscape(parResp.RequestURI) ··· 213 218 tokenEndpoint, _ := sess.Values["atproto_token_endpoint"].(string) 214 219 expectedDID, _ := sess.Values["atproto_did"].(string) 215 220 pdsURL, _ := sess.Values["atproto_pds_url"].(string) 221 + next, _ := sess.Values["atproto_next"].(string) 216 222 217 223 // Clean up session state 218 224 delete(sess.Values, "atproto_state") ··· 222 228 delete(sess.Values, "atproto_token_endpoint") 223 229 delete(sess.Values, "atproto_did") 224 230 delete(sess.Values, "atproto_pds_url") 231 + delete(sess.Values, "atproto_next") 225 232 226 233 // 2. Exchange code for tokens 227 234 kp, err := dpop.UnmarshalPrivate([]byte(keyJSON)) ··· 355 362 356 363 auth.SetUserID(w, r, user.ID) 357 364 sess.Save(r, w) 365 + 366 + // Redirect to the originally requested URL, or home. 367 + // Only allow local paths to prevent open redirect. 368 + if next != "" && strings.HasPrefix(next, "/") { 369 + http.Redirect(w, r, next, http.StatusSeeOther) 370 + return 371 + } 358 372 http.Redirect(w, r, "/", http.StatusSeeOther) 359 373 }
+1
internal/handler/handler.go
··· 47 47 Error string 48 48 Description string 49 49 OGImage string 50 + Next string 50 51 } 51 52 52 53 // DocumentEditData is passed to document_edit.html.
+1
templates/atproto_login.html
··· 6 6 <div class="alert alert-error">{{.Error}}</div> 7 7 {{end}} 8 8 <form method="post" action="/auth/atproto" class="auth-form"> 9 + {{if .Next}}<input type="hidden" name="next" value="{{.Next}}">{{end}} 9 10 <label> 10 11 <input type="text" name="handle" placeholder="Bluesky username or custom domain name" required autofocus 11 12 autocomplete="username" spellcheck="false" autocorrect="off" autocapitalize="off">