ultisuite-backend/internal/api/auth/complete.go
R3D347HR4Y 8bbc539d77 feat(auth): implement flow completion and rate limiting for authentication flows
- Added a new handler for completing authentication flows, including session validation and cookie management.
- Implemented flow rate limiting to restrict the number of flow start requests per client IP.
- Enhanced flow session management with Redis support for persistent session storage.
- Updated existing handlers to integrate the new flow completion logic and error handling for various session states.
- Introduced unit tests for the new flow completion and rate limiting functionalities to ensure reliability.
2026-06-20 01:09:42 +02:00

88 lines
2.6 KiB
Go

package authapi
import (
"encoding/json"
"errors"
"net/http"
"net/url"
"strings"
"github.com/ultisuite/ulti-backend/internal/api/apiresponse"
"github.com/ultisuite/ulti-backend/internal/authentik"
)
type flowCompleteRequest struct {
ReturnTo string `json:"returnTo"`
}
type flowCompleteResponse struct {
RedirectURL string `json:"redirectUrl"`
}
func (h *Handler) CompleteFlow(w http.ResponseWriter, r *http.Request) {
sessionID := readFlowSessionCookie(r)
if sessionID == "" {
apiresponse.WriteError(w, r, http.StatusUnauthorized, "flow_session_missing", "flow session cookie required", nil)
return
}
var req flowCompleteRequest
if r.Body != nil {
_ = json.NewDecoder(r.Body).Decode(&req)
}
returnTo := strings.TrimSpace(req.ReturnTo)
if returnTo == "" {
returnTo = "/mail/inbox"
}
if !strings.HasPrefix(returnTo, "/") || strings.HasPrefix(returnTo, "//") {
apiresponse.WriteError(w, r, http.StatusBadRequest, "invalid_return_to", "returnTo must be a relative path", nil)
return
}
slug := FlowAuthentication
cookies, err := h.flows.CompleteSession(r.Context(), sessionID, slug)
if err != nil {
if errors.Is(err, authentik.ErrFlowSessionNotFound) {
clearFlowSessionCookie(w)
apiresponse.WriteError(w, r, http.StatusGone, "flow_session_expired", "flow session expired; restart the flow", nil)
return
}
if errors.Is(err, authentik.ErrFlowSessionNotCompleted) {
apiresponse.WriteError(w, r, http.StatusConflict, "flow_not_completed", "authentication flow is not finished yet", nil)
return
}
if errors.Is(err, authentik.ErrFlowSessionSlugMismatch) {
apiresponse.WriteError(w, r, http.StatusConflict, "flow_session_mismatch", "flow slug does not match active session", nil)
return
}
apiresponse.WriteError(w, r, http.StatusBadGateway, "flow_complete_failed", err.Error(), nil)
return
}
clearFlowSessionCookie(w)
setBrowserAuthentikCookies(w, cookies)
loginURL := buildLoginRedirectURL(h.appURL, returnTo)
apiresponse.WriteJSON(w, http.StatusOK, flowCompleteResponse{RedirectURL: loginURL})
}
func buildLoginRedirectURL(appURL, returnTo string) string {
base := strings.TrimRight(strings.TrimSpace(appURL), "/")
if base == "" {
base = "http://localhost:3004"
}
params := url.Values{}
params.Set("returnTo", returnTo)
return base + "/api/auth/login?" + params.Encode()
}
func setBrowserAuthentikCookies(w http.ResponseWriter, stored []authentik.SerializedCookie) {
for _, c := range authentik.BrowserAuthentikCookies(stored) {
http.SetCookie(w, c)
}
}
func forwardFlowCookies(w http.ResponseWriter, stored []authentik.SerializedCookie) {
setBrowserAuthentikCookies(w, stored)
}