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}