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}