- 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.
88 lines
2.6 KiB
Go
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)
|
|
}
|