package idp import ( "context" "encoding/base64" "errors" "log/slog" "net/http" "net/url" dexstorage "github.com/dexidp/dex/storage" transporthttp "tug/internal/transport/http" "tug/internal/transport/web" ) const ( LoginRequiredPath = "/login-required" promptNone = "none" errLoginRequired = "login_required" loginRequiredDescription = "User not authenticated" ) var errNotSilent = errors.New("idp: the auth request is not prompt=none") func (v *Verifier) LoginRequired(ctx context.Context, state, proof string) (string, error) { target, err := v.loginRequired(ctx, state, proof, false) if errors.Is(err, errNotSilent) { return "", requestFailure(errDeadRequest) } return target, err } func (v *Verifier) LoginRequiredUnclaimed(ctx context.Context, state string) (string, bool, error) { target, err := v.loginRequired(ctx, state, "", true) if errors.Is(err, errNotSilent) { return "", false, nil } return target, err == nil, err } func (v *Verifier) loginRequired(ctx context.Context, state, proof string, unclaimed bool) (string, error) { var value string err := v.updateBound(ctx, state, proof, unclaimed, func(a dexstorage.AuthRequest, p pending) (pending, error) { if a.Prompt != promptNone { return p, errNotSilent } value, p.RequiredDigest, p.RequiredExpiry = v.newValue(a.Expiry) return p, nil }) if err != nil { return "", err } q := url.Values{"state": {state}, ValueParam: {value}} return v.deps.issuer + LoginRequiredPath + "?" + q.Encode(), nil } type silent struct { next http.Handler v *Verifier page transporthttp.ErrorPage } func (s silent) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet || r.URL.Path != LoginRequiredPath { s.next.ServeHTTP(w, r) return } w.Header().Set("Cache-Control", "no-store") ctx := r.Context() value, err := base64.RawURLEncoding.Strict().DecodeString(r.URL.Query().Get(ValueParam)) if err != nil || len(value) != valueLength { s.page(w, r, http.StatusBadRequest) return } a, _, err := s.v.waiting(ctx, r.URL.Query().Get("state")) switch { case errors.Is(err, web.ErrLoginBusy): s.busy(w, r) return case err != nil || a.Prompt != promptNone: s.page(w, r, http.StatusBadRequest) return } u, err := url.Parse(a.RedirectURI) if err != nil { s.page(w, r, http.StatusInternalServerError) return } err = s.v.deps.dex.UpdateAuthRequest(ctx, a.ID, func(a dexstorage.AuthRequest) (dexstorage.AuthRequest, error) { if !s.v.live(a) || a.Prompt != promptNone { return a, errDeadRequest } p, err := record(a.ConnectorData) if err != nil { return a, err } if err := checkValue(value, p.RequiredDigest, p.RequiredExpiry, s.v.deps.now()); err != nil { return a, err } a.ConnectorData = []byte(errLoginRequired) return a, nil }) switch { case errors.Is(err, ErrValueMismatch), errors.Is(err, ErrValueExpired), errors.Is(err, errDeadRequest), errors.Is(err, dexstorage.ErrNotFound): s.page(w, r, http.StatusBadRequest) return case err != nil: s.busy(w, r) return } if err := s.v.deps.dex.DeleteAuthRequest(ctx, a.ID); err != nil && !errors.Is(err, dexstorage.ErrNotFound) { slog.WarnContext(ctx, "silent sign-in cleanup failed", "err", err) } q := u.Query() q.Add("state", a.State) q.Add("error", errLoginRequired) q.Add("error_description", loginRequiredDescription) u.RawQuery = q.Encode() http.Redirect(w, r, u.String(), http.StatusSeeOther) } func (s silent) busy(w http.ResponseWriter, r *http.Request) { w.Header().Set("Retry-After", "1") s.page(w, transporthttp.MarkBusy(r), http.StatusServiceUnavailable) }