Raw
  1package idp
  2
  3import (
  4	"context"
  5	"encoding/base64"
  6	"errors"
  7	"log/slog"
  8	"net/http"
  9	"net/url"
 10
 11	dexstorage "github.com/dexidp/dex/storage"
 12
 13	transporthttp "tug/internal/transport/http"
 14	"tug/internal/transport/web"
 15)
 16
 17const (
 18	LoginRequiredPath        = "/login-required"
 19	promptNone               = "none"
 20	errLoginRequired         = "login_required"
 21	loginRequiredDescription = "User not authenticated"
 22)
 23
 24var errNotSilent = errors.New("idp: the auth request is not prompt=none")
 25
 26func (v *Verifier) LoginRequired(ctx context.Context, state, proof string) (string, error) {
 27	target, err := v.loginRequired(ctx, state, proof, false)
 28	if errors.Is(err, errNotSilent) {
 29		return "", requestFailure(errDeadRequest)
 30	}
 31	return target, err
 32}
 33
 34func (v *Verifier) LoginRequiredUnclaimed(ctx context.Context, state string) (string, bool, error) {
 35	target, err := v.loginRequired(ctx, state, "", true)
 36	if errors.Is(err, errNotSilent) {
 37		return "", false, nil
 38	}
 39	return target, err == nil, err
 40}
 41
 42func (v *Verifier) loginRequired(ctx context.Context, state, proof string, unclaimed bool) (string, error) {
 43	var value string
 44	err := v.updateBound(ctx, state, proof, unclaimed, func(a dexstorage.AuthRequest, p pending) (pending, error) {
 45		if a.Prompt != promptNone {
 46			return p, errNotSilent
 47		}
 48		value, p.RequiredDigest, p.RequiredExpiry = v.newValue(a.Expiry)
 49		return p, nil
 50	})
 51	if err != nil {
 52		return "", err
 53	}
 54	q := url.Values{"state": {state}, ValueParam: {value}}
 55	return v.deps.issuer + LoginRequiredPath + "?" + q.Encode(), nil
 56}
 57
 58type silent struct {
 59	next http.Handler
 60	v    *Verifier
 61	page transporthttp.ErrorPage
 62}
 63
 64func (s silent) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 65	if r.Method != http.MethodGet || r.URL.Path != LoginRequiredPath {
 66		s.next.ServeHTTP(w, r)
 67		return
 68	}
 69	w.Header().Set("Cache-Control", "no-store")
 70	ctx := r.Context()
 71	value, err := base64.RawURLEncoding.Strict().DecodeString(r.URL.Query().Get(ValueParam))
 72	if err != nil || len(value) != valueLength {
 73		s.page(w, r, http.StatusBadRequest)
 74		return
 75	}
 76	a, _, err := s.v.waiting(ctx, r.URL.Query().Get("state"))
 77	switch {
 78	case errors.Is(err, web.ErrLoginBusy):
 79		s.busy(w, r)
 80		return
 81	case err != nil || a.Prompt != promptNone:
 82		s.page(w, r, http.StatusBadRequest)
 83		return
 84	}
 85	u, err := url.Parse(a.RedirectURI)
 86	if err != nil {
 87		s.page(w, r, http.StatusInternalServerError)
 88		return
 89	}
 90	err = s.v.deps.dex.UpdateAuthRequest(ctx, a.ID, func(a dexstorage.AuthRequest) (dexstorage.AuthRequest, error) {
 91		if !s.v.live(a) || a.Prompt != promptNone {
 92			return a, errDeadRequest
 93		}
 94		p, err := record(a.ConnectorData)
 95		if err != nil {
 96			return a, err
 97		}
 98		if err := checkValue(value, p.RequiredDigest, p.RequiredExpiry, s.v.deps.now()); err != nil {
 99			return a, err
100		}
101		a.ConnectorData = []byte(errLoginRequired)
102		return a, nil
103	})
104	switch {
105	case errors.Is(err, ErrValueMismatch), errors.Is(err, ErrValueExpired), errors.Is(err, errDeadRequest), errors.Is(err, dexstorage.ErrNotFound):
106		s.page(w, r, http.StatusBadRequest)
107		return
108	case err != nil:
109		s.busy(w, r)
110		return
111	}
112	if err := s.v.deps.dex.DeleteAuthRequest(ctx, a.ID); err != nil && !errors.Is(err, dexstorage.ErrNotFound) {
113		slog.WarnContext(ctx, "silent sign-in cleanup failed", "err", err)
114	}
115	q := u.Query()
116	q.Add("state", a.State)
117	q.Add("error", errLoginRequired)
118	q.Add("error_description", loginRequiredDescription)
119	u.RawQuery = q.Encode()
120	http.Redirect(w, r, u.String(), http.StatusSeeOther)
121}
122
123func (s silent) busy(w http.ResponseWriter, r *http.Request) {
124	w.Header().Set("Retry-After", "1")
125	s.page(w, transporthttp.MarkBusy(r), http.StatusServiceUnavailable)
126}