From 573e66c99ae3499d93f95a0c5c5e74c9315c4243 Mon Sep 17 00:00:00 2001 From: mabrur-h Date: Tue, 28 Apr 2026 16:47:35 +0500 Subject: [PATCH] feat: support loopback redirect for desktop OAuth + auto-allow users --- server/jprq.go | 2 +- website/main.go | 86 ++++++++++++++++++++++++++++++++++++------------- 2 files changed, 64 insertions(+), 24 deletions(-) diff --git a/server/jprq.go b/server/jprq.go index 9eda252..1067da3 100644 --- a/server/jprq.go +++ b/server/jprq.go @@ -59,7 +59,7 @@ func (j *Jprq) Start() { go func() { // periodically load allowed users j.loadAllowedUsers() - for range time.Tick(time.Minute) { + for range time.Tick(5 * time.Second) { j.loadAllowedUsers() } }() diff --git a/website/main.go b/website/main.go index 970b63a..4a4c5ce 100644 --- a/website/main.go +++ b/website/main.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "net/http" + "net/url" "os" "github.com/azimjohn/jprq/server/github" @@ -50,19 +51,27 @@ func contentHandler(content []byte, contentType string) func(w http.ResponseWrit } func authHandler(w http.ResponseWriter, r *http.Request) { - // Pass the app parameter to the OAuth state if present app := r.URL.Query().Get("app") + callback := r.URL.Query().Get("callback") oauthURL := oauth.OAuthUrl() - // If app parameter is present, add it to the state if app != "" { - // Store app parameter in session or pass it through state - // For now, we'll use a cookie to preserve it http.SetCookie(w, &http.Cookie{ Name: "jprq_app", Value: app, Path: "/", - MaxAge: 300, // 5 minutes + MaxAge: 300, + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + }) + } + + if callback != "" { + http.SetCookie(w, &http.Cookie{ + Name: "jprq_callback", + Value: callback, + Path: "/", + MaxAge: 300, HttpOnly: true, SameSite: http.SameSiteLaxMode, }) @@ -83,35 +92,46 @@ func oauthCallback(w http.ResponseWriter, r *http.Request) { return } + // Auto-add user to allowed list (non-blocking) + go autoAllowUser(token) + // Check if this is an app-based authentication appCookie, err := r.Cookie("jprq_app") + callbackCookie, _ := r.Cookie("jprq_callback") + if err == nil && appCookie.Value != "" { - // Clear the cookie + // Clear cookies http.SetCookie(w, &http.Cookie{ - Name: "jprq_app", - Value: "", - Path: "/", - MaxAge: -1, - HttpOnly: true, + Name: "jprq_app", Value: "", Path: "/", MaxAge: -1, HttpOnly: true, + }) + http.SetCookie(w, &http.Cookie{ + Name: "jprq_callback", Value: "", Path: "/", MaxAge: -1, HttpOnly: true, }) - // Redirect to the app URL with the token - var appURL string + // If callback URL provided, redirect there instead of deep link. + // Parse the URL so we preserve any pre-existing query params (e.g. state) + // and append `token` correctly using `&` instead of a second `?`. + if callbackCookie != nil && callbackCookie.Value != "" { + parsed, perr := url.Parse(callbackCookie.Value) + if perr == nil && parsed.Scheme != "" && parsed.Host != "" { + q := parsed.Query() + q.Set("token", token) + parsed.RawQuery = q.Encode() + http.Redirect(w, r, parsed.String(), http.StatusFound) + return + } + fmt.Printf("invalid callback URL %q: %v\n", callbackCookie.Value, perr) + } + + // Fall back to deep link switch appCookie.Value { - case "mac": - appURL = fmt.Sprintf("jprq://auth/callback?token=%s", token) - case "windows": - appURL = fmt.Sprintf("jprq://auth/callback?token=%s", token) - case "linux": - appURL = fmt.Sprintf("jprq://auth/callback?token=%s", token) + case "mac", "windows", "linux": + appURL := fmt.Sprintf("jprq://auth/callback?token=%s", token) + http.Redirect(w, r, appURL, http.StatusFound) default: - // Unknown app type, fall back to web display w.Header().Set("Content-Type", "text/html") w.Write([]byte(fmt.Sprintf(tokenHtml, token))) - return } - - http.Redirect(w, r, appURL, http.StatusFound) return } @@ -119,3 +139,23 @@ func oauthCallback(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") w.Write([]byte(fmt.Sprintf(tokenHtml, token))) } + +const allowedUsersFile = "/etc/jprq/allowed-users.csv" + +func autoAllowUser(token string) { + user, err := oauth.Authenticate(token) + if err != nil { + fmt.Printf("auto-allow: failed to authenticate user: %s\n", err) + return + } + + file, err := os.OpenFile(allowedUsersFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + fmt.Printf("auto-allow: failed to open file: %s\n", err) + return + } + defer file.Close() + + fmt.Fprintf(file, "%s,desktop\n", user.Login) + fmt.Printf("auto-allowed user: %s\n", user.Login) +}