package web import ( "bytes" "crypto/rand" "encoding/binary" "log/slog" "net/http" "net/url" "strings" "time" transporthttp "tug/internal/transport/http" "tug/internal/transport/web/text" ) const ( ContinuePath = "/login/continue" contCookie = "__Host-authsvc_cont" contTTL = 60 * time.Second contNonceLength = 16 ) func (k *Kit) continuationTarget(r *http.Request, target string) bool { u, err := url.Parse(target) if err != nil || u.Host == "" || k.origins.allows(target) != nil { return false } return !strings.EqualFold(u.Hostname(), transporthttp.CanonicalHost(r.Host)) } func (k *Kit) Continue(w http.ResponseWriter, r *http.Request, target string) { if !k.continuationTarget(r, target) { slog.Error("continuation refused: target is not an allowed cross-origin destination") k.renderError(w, r, http.StatusInternalServerError, text.ErrorInternalTitle, text.ErrorInternal) return } nonce := make([]byte, contNonceLength) rand.Read(nonce) payload := append(nonce, binary.BigEndian.AppendUint64(nil, uint64(k.now().Add(contTTL).Unix()))...) payload = append(payload, target...) setCookie(w, contCookie, seal(k.keys.cont, payload), int(contTTL/time.Second)) http.Redirect(w, r, ContinuePath, http.StatusSeeOther) } type continueData struct { Target string } func (k *Kit) consume(w http.ResponseWriter, r *http.Request) { c, err := r.Cookie(contCookie) if err != nil { k.renderError(w, r, http.StatusBadRequest, text.ErrorBadRequestTitle, text.ContinuationInvalid) return } setCookie(w, contCookie, "", -1) payload, err := unseal(k.keys.cont, c.Value) now := k.now() if err != nil || len(payload) < contNonceLength+8 { k.renderError(w, r, http.StatusBadRequest, text.ErrorBadRequestTitle, text.ContinuationInvalid) return } nonce := payload[:contNonceLength] expiry := time.Unix(int64(binary.BigEndian.Uint64(payload[contNonceLength:contNonceLength+8])), 0) target := string(bytes.Clone(payload[contNonceLength+8:])) if now.After(expiry) || !k.continuationTarget(r, target) || !k.spent.Spend(string(nonce), expiry, now) { k.renderError(w, r, http.StatusBadRequest, text.ErrorBadRequestTitle, text.ContinuationInvalid) return } k.render(w, r, http.StatusOK, "continue", k.newPage(w, r, continueData{Target: target})) } func (k *Kit) ContinueRoute() Route { return Route{Method: http.MethodGet, Path: ContinuePath, Access: Public, Handler: http.HandlerFunc(k.consume)} }