Raw
  1package idp
  2
  3import (
  4	"context"
  5	"crypto/subtle"
  6	"encoding/base64"
  7	"encoding/binary"
  8	"encoding/json"
  9	"errors"
 10	"log/slog"
 11	"net/http"
 12	"net/url"
 13	"time"
 14	"unicode/utf8"
 15
 16	dexstorage "github.com/dexidp/dex/storage"
 17
 18	"tug/internal/storage"
 19)
 20
 21const (
 22	TokenBodyLimit   = 64 << 10
 23	reuseRevokeLimit = 5 * time.Second
 24)
 25
 26type refreshRevoker interface {
 27	RevokeRefreshTokenAudited(ctx context.Context, id string, audit storage.AppendAuditInput) (bool, error)
 28}
 29
 30type reuseGuard struct {
 31	next         http.Handler
 32	dex          dexstorage.Storage
 33	authenticate func(id, secret string) bool
 34	revoker      refreshRevoker
 35	logger       *slog.Logger
 36}
 37
 38func revokeOnReuse(next http.Handler, dex dexstorage.Storage, clients []dexstorage.Client, revoker refreshRevoker, logger *slog.Logger) http.Handler {
 39	return &reuseGuard{next: next, dex: dex, authenticate: clientAuthenticator(clients), revoker: revoker, logger: logger}
 40}
 41
 42func clientAuthenticator(clients []dexstorage.Client) func(id, secret string) bool {
 43	byID := make(map[string]dexstorage.Client, len(clients))
 44	for _, c := range clients {
 45		byID[c.ID] = c
 46	}
 47	return func(id, secret string) bool {
 48		c, ok := byID[id]
 49		if !ok {
 50			return false
 51		}
 52		if c.Public {
 53			return true
 54		}
 55		return c.Secret != "" && subtle.ConstantTimeCompare([]byte(c.Secret), []byte(secret)) == 1
 56	}
 57}
 58
 59func (g *reuseGuard) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 60	if r.Method != http.MethodPost || r.URL.Path != "/token" {
 61		g.next.ServeHTTP(w, r)
 62		return
 63	}
 64	r.Body = http.MaxBytesReader(w, r.Body, TokenBodyLimit)
 65	if err := r.ParseForm(); err != nil {
 66		var tooLarge *http.MaxBytesError
 67		if errors.As(err, &tooLarge) {
 68			refuseToken(w, http.StatusRequestEntityTooLarge, "The token request body is too large.")
 69			return
 70		}
 71		refuseToken(w, http.StatusBadRequest, "The token request could not be parsed.")
 72		return
 73	}
 74	rec := &statusRecorder{ResponseWriter: w}
 75	g.next.ServeHTTP(rec, r)
 76	if rec.status == http.StatusOK || r.PostForm.Get("grant_type") != "refresh_token" {
 77		return
 78	}
 79	client, ok := g.authenticatedClient(r)
 80	if !ok {
 81		return
 82	}
 83	id, secret, ok := decodeRefreshToken(r.PostForm.Get("refresh_token"))
 84	if !ok {
 85		return
 86	}
 87	ctx, cancel := context.WithTimeout(context.WithoutCancel(r.Context()), reuseRevokeLimit)
 88	defer cancel()
 89	g.revoke(ctx, id, secret, client)
 90}
 91
 92func (g *reuseGuard) revoke(ctx context.Context, id, secret, client string) {
 93	row, err := g.dex.GetRefresh(ctx, id)
 94	if errors.Is(err, dexstorage.ErrNotFound) {
 95		return
 96	}
 97	if err != nil {
 98		g.logger.ErrorContext(ctx, "idp: refresh reuse check: read refresh token", "err", err)
 99		return
100	}
101	if row.ClientID != client || subtle.ConstantTimeCompare([]byte(row.Token), []byte(secret)) == 1 {
102		return
103	}
104	generation := "older"
105	if row.ObsoleteToken != "" && subtle.ConstantTimeCompare([]byte(row.ObsoleteToken), []byte(secret)) == 1 {
106		generation = "previous"
107	}
108	detail, _ := json.Marshal(map[string]string{"client_id": row.ClientID, "generation": generation})
109	revoked, err := g.revoker.RevokeRefreshTokenAudited(ctx, row.ID, storage.AppendAuditInput{Actor: "system", Action: storage.ActionRefreshReuseRevoked, Subject: row.Claims.UserID, Detail: string(detail)})
110	if err != nil {
111		g.logger.ErrorContext(ctx, "idp: refresh reuse: revoke refresh token", "err", err)
112		return
113	}
114	if !revoked {
115		return
116	}
117	g.logger.WarnContext(ctx, "idp: refresh token reused; its family is revoked", "client_id", row.ClientID, "user_id", row.Claims.UserID, "generation", generation)
118}
119
120func (g *reuseGuard) authenticatedClient(r *http.Request) (string, bool) {
121	id, secret, basic := r.BasicAuth()
122	if basic {
123		var err error
124		if id, err = url.QueryUnescape(id); err != nil {
125			return "", false
126		}
127		if secret, err = url.QueryUnescape(secret); err != nil {
128			return "", false
129		}
130	} else {
131		id, secret = r.PostForm.Get("client_id"), r.PostForm.Get("client_secret")
132	}
133	return id, g.authenticate(id, secret)
134}
135
136func refuseToken(w http.ResponseWriter, status int, description string) {
137	body, _ := json.Marshal(struct {
138		Error       string `json:"error"`
139		Description string `json:"error_description"`
140	}{"invalid_request", description})
141	w.Header().Set("Content-Type", "application/json")
142	w.WriteHeader(status)
143	w.Write(body)
144}
145
146func decodeRefreshToken(raw string) (id, secret string, ok bool) {
147	b, err := base64.RawURLEncoding.DecodeString(raw)
148	if err != nil || len(b) == 0 {
149		return "", "", false
150	}
151	for len(b) > 0 {
152		key, n := binary.Uvarint(b)
153		if n <= 0 || key>>3 == 0 {
154			return "", "", false
155		}
156		b = b[n:]
157		field, wire := key>>3, key&7
158		var value []byte
159		switch wire {
160		case 0:
161			if _, n = binary.Uvarint(b); n <= 0 {
162				return "", "", false
163			}
164			b = b[n:]
165		case 1, 5:
166			size := 8
167			if wire == 5 {
168				size = 4
169			}
170			if len(b) < size {
171				return "", "", false
172			}
173			b = b[size:]
174		case 2:
175			l, n := binary.Uvarint(b)
176			if n <= 0 || l > uint64(len(b)-n) {
177				return "", "", false
178			}
179			value, b = b[n:n+int(l)], b[n+int(l):]
180		default:
181			return "", "", false
182		}
183		if field != 1 && field != 2 {
184			continue
185		}
186		if wire != 2 || !utf8.Valid(value) {
187			return "", "", false
188		}
189		if field == 1 {
190			id = string(value)
191		} else {
192			secret = string(value)
193		}
194	}
195	return id, secret, id != ""
196}
197
198type statusRecorder struct {
199	http.ResponseWriter
200	status int
201}
202
203func (s *statusRecorder) WriteHeader(code int) {
204	if s.status == 0 {
205		s.status = code
206	}
207	s.ResponseWriter.WriteHeader(code)
208}
209
210func (s *statusRecorder) Write(b []byte) (int, error) {
211	if s.status == 0 {
212		s.status = http.StatusOK
213	}
214	return s.ResponseWriter.Write(b)
215}