package management import ( "errors" "net/http" "net/url" "strings" "github.com/gin-gonic/gin" ) type oauthCallbackRequest struct { Provider string `json:"provider"` RedirectURL string `json:"redirect_url"` Code string `json:"code"` State string `json:"state"` Error string `json:"error"` } func (h *Handler) PostOAuthCallback(c *gin.Context) { if h == nil || h.cfg == nil { c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": "handler not initialized"}) return } var req oauthCallbackRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "invalid body"}) return } canonicalProvider, err := NormalizeOAuthProvider(req.Provider) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "unsupported provider"}) return } state := strings.TrimSpace(req.State) code := strings.TrimSpace(req.Code) errMsg := strings.TrimSpace(req.Error) if rawRedirect := strings.TrimSpace(req.RedirectURL); rawRedirect != "" { u, errParse := url.Parse(rawRedirect) if errParse != nil { c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "invalid redirect_url"}) return } q := u.Query() if state == "" { state = strings.TrimSpace(q.Get("state")) } if code == "" { code = strings.TrimSpace(q.Get("code")) } if errMsg == "" { errMsg = strings.TrimSpace(q.Get("error")) if errMsg == "" { errMsg = strings.TrimSpace(q.Get("error_description")) } } } if state == "" { c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "state is required"}) return } if err := ValidateOAuthState(state); err != nil { c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "invalid state"}) return } if code == "" && errMsg == "" { c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "code or error is required"}) return } sessionProvider, sessionStatus, ok := GetOAuthSession(state) if !ok { c.JSON(http.StatusNotFound, gin.H{"status": "error", "error": "unknown or expired state"}) return } if sessionStatus != "" { c.JSON(http.StatusConflict, gin.H{"status": "error", "error": "oauth flow is not pending"}) return } if !strings.EqualFold(sessionProvider, canonicalProvider) { c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "provider does not match state"}) return } if _, errWrite := WriteOAuthCallbackFileForPendingSession(h.cfg.AuthDir, canonicalProvider, state, code, errMsg); errWrite != nil { if errors.Is(errWrite, errOAuthSessionNotPending) { c.JSON(http.StatusConflict, gin.H{"status": "error", "error": "oauth flow is not pending"}) return } c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": "failed to persist oauth callback"}) return } c.JSON(http.StatusOK, gin.H{"status": "ok"}) }