Skip to content

Commit 5836d93

Browse files
committed
tsidp-server.go,server: switch to slog
Switch to slog for structured logging with appropriate log levels for activity within the app. - log all token api errors at WARN level - move tsnet.Server logging to new flag: -debug-tsnet - move request/response logging to new flag: -debug-all-requests - unify API error handling logic to writeHTTPError - switch funnel error to http.StatusUnauthorized Updates #25 Signed-off-by: Benson Wong <[email protected]>
1 parent 5007403 commit 5836d93

File tree

11 files changed

+247
-199
lines changed

11 files changed

+247
-199
lines changed

server/appcap.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,13 @@ func (s *IDPServer) addGrantAccessContext(handler http.HandlerFunc) http.Handler
9191

9292
who, err = s.lc.WhoIs(r.Context(), remoteAddr)
9393
if err != nil {
94-
http.Error(w, fmt.Sprintf("Error getting WhoIs: %v", err), http.StatusInternalServerError)
94+
writeHTTPError(w, r, http.StatusInternalServerError, "server_error", fmt.Sprintf("Error getting WhoIs: %v", err))
9595
return
9696
}
9797

9898
rules, err := tailcfg.UnmarshalCapJSON[capRule](who.CapMap, "tailscale.com/cap/tsidp")
9999
if err != nil {
100-
http.Error(w, fmt.Sprintf("failed unmarshaling app cap rule %s", err.Error()), http.StatusInternalServerError)
100+
writeHTTPError(w, r, http.StatusInternalServerError, "server_error", fmt.Sprintf("failed unmarshaling app cap rule %s", err.Error()))
101101
return
102102
}
103103
accessRules.rules = rules

server/authorize.go

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ package server
66
import (
77
"crypto/subtle"
88
"fmt"
9-
"log"
9+
"log/slog"
1010
"net/http"
1111
"net/url"
1212
"slices"
@@ -22,9 +22,8 @@ func (s *IDPServer) serveAuthorize(w http.ResponseWriter, r *http.Request) {
2222
// This URL is visited by the user who is being authenticated. If they are
2323
// visiting the URL over Funnel, that means they are not part of the
2424
// tailnet that they are trying to be authenticated for.
25-
// NOTE: Funnel request behavior is the same regardless of secure or insecure mode.
2625
if isFunnelRequest(r) {
27-
http.Error(w, "tsidp: unauthorized", http.StatusUnauthorized)
26+
writeHTTPError(w, r, http.StatusUnauthorized, "access_denied", "not allowed over funnel")
2827
return
2928
}
3029

@@ -33,13 +32,13 @@ func (s *IDPServer) serveAuthorize(w http.ResponseWriter, r *http.Request) {
3332

3433
redirectURI := uq.Get("redirect_uri")
3534
if redirectURI == "" {
36-
http.Error(w, "tsidp: must specify redirect_uri", http.StatusBadRequest)
35+
writeHTTPError(w, r, http.StatusBadRequest, "invalid_request", "tsidp: must specify redirect_uri")
3736
return
3837
}
3938

4039
clientID := uq.Get("client_id")
4140
if clientID == "" {
42-
http.Error(w, "tsidp: must specify client_id", http.StatusBadRequest)
41+
writeHTTPError(w, r, http.StatusBadRequest, "invalid_request", "tsidp: must specify client_id")
4342
return
4443
}
4544

@@ -48,20 +47,20 @@ func (s *IDPServer) serveAuthorize(w http.ResponseWriter, r *http.Request) {
4847
s.mu.Unlock()
4948

5049
if !ok {
51-
http.Error(w, "tsidp: invalid client ID", http.StatusBadRequest)
50+
writeHTTPError(w, r, http.StatusBadRequest, "invalid_client", "tsidp: invalid client ID")
5251
return
5352
}
5453

5554
// Validate client_id matches (public identifier validation)
5655
clientIDcmp := subtle.ConstantTimeCompare([]byte(clientID), []byte(funnelClient.ID))
5756
if clientIDcmp != 1 {
58-
http.Error(w, "tsidp: invalid client ID", http.StatusBadRequest)
57+
writeHTTPError(w, r, http.StatusBadRequest, "invalid_client", "tsidp: invalid client ID")
5958
return
6059
}
6160

6261
// check for exact match of redirect_uri (OAuth 2.1 requirement)
6362
if !slices.Contains(funnelClient.RedirectURIs, redirectURI) {
64-
http.Error(w, "tsidp: redirect_uri mismatch", http.StatusBadRequest)
63+
writeHTTPError(w, r, http.StatusBadRequest, "invalid_request", "tsidp: redirect_uri mismatch")
6564
return
6665
}
6766

@@ -78,8 +77,8 @@ func (s *IDPServer) serveAuthorize(w http.ResponseWriter, r *http.Request) {
7877
var err error
7978
who, err = s.lc.WhoIs(r.Context(), remoteAddr)
8079
if err != nil {
81-
log.Printf("Error getting WhoIs: %v", err)
82-
http.Error(w, err.Error(), http.StatusInternalServerError)
80+
slog.Error("Error getting WhoIs", slog.Any("error", err))
81+
writeHTTPError(w, r, http.StatusInternalServerError, "server_error", err.Error())
8382
return
8483
}
8584

@@ -134,13 +133,12 @@ func (s *IDPServer) serveAuthorize(w http.ResponseWriter, r *http.Request) {
134133
}
135134
parsedURL, err := url.Parse(redirectURI)
136135
if err != nil {
137-
http.Error(w, "invalid redirect URI", http.StatusInternalServerError)
136+
writeHTTPError(w, r, http.StatusInternalServerError, "server_error", "invalid redirect URI")
138137
return
139138
}
140139
parsedURL.RawQuery = queryString.Encode()
141140
u := parsedURL.String()
142-
log.Printf("Redirecting to %q", u)
143-
141+
slog.Debug("authorize redirect", slog.String("url", u))
144142
http.Redirect(w, r, u, http.StatusFound)
145143
}
146144

@@ -178,7 +176,8 @@ func redirectAuthError(w http.ResponseWriter, r *http.Request, redirectURI, erro
178176
u, err := url.Parse(redirectURI)
179177
if err != nil {
180178
// If redirect URI is invalid, return error directly
181-
http.Error(w, "invalid redirect_uri", http.StatusBadRequest)
179+
slog.Warn("Invalid redirect_uri", slog.String("error", err.Error()))
180+
writeHTTPError(w, r, http.StatusBadRequest, "invalid_request", "invalid redirect_uri")
182181
return
183182
}
184183

@@ -192,5 +191,10 @@ func redirectAuthError(w http.ResponseWriter, r *http.Request, redirectURI, erro
192191
}
193192
u.RawQuery = q.Encode()
194193

194+
slog.Info("Redirecting to client with error",
195+
slog.String("error_code", errorCode),
196+
slog.String("state", state),
197+
slog.String("redirect_uri", u.String()),
198+
)
195199
http.Redirect(w, r, u.String(), http.StatusFound)
196200
}

server/client_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ func TestServeDynamicClientRegistration(t *testing.T) {
249249
method: "POST",
250250
body: `{"redirect_uris": ["https://example.com/callback"]}`,
251251
isFunnel: true,
252-
expectStatus: http.StatusForbidden,
252+
expectStatus: http.StatusUnauthorized,
253253
checkResponse: func(t *testing.T, body []byte) {
254254
var errResp map[string]interface{}
255255
if err := json.Unmarshal(body, &errResp); err != nil {

server/clients.go

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"bytes"
88
"encoding/json"
99
"fmt"
10-
"log"
10+
"log/slog"
1111
"net/http"
1212
"os"
1313
"path/filepath"
@@ -89,7 +89,7 @@ func (s *IDPServer) LoadFunnelClients() error {
8989
}
9090
}
9191
if migrationPerformed {
92-
log.Println("Migrated old funnel clients with single redirect_uri to redirect_uris field.")
92+
slog.Info("Migrated old funnel clients with single redirect_uri to redirect_uris field.")
9393
if err := s.storeFunnelClientsLocked(); err != nil {
9494
return fmt.Errorf("failed to store migrated clients: %w", err)
9595
}
@@ -122,7 +122,7 @@ func (s *IDPServer) storeFunnelClientsLocked() error {
122122
// Migrated from legacy/tsidp.go:2055-2094
123123
func (s *IDPServer) serveClients(w http.ResponseWriter, r *http.Request) {
124124
if isFunnelRequest(r) {
125-
http.Error(w, "tsidp: not found", http.StatusNotFound)
125+
writeHTTPError(w, r, http.StatusUnauthorized, "access_denied", "not available over funnel")
126126
return
127127
}
128128

@@ -142,7 +142,7 @@ func (s *IDPServer) serveClients(w http.ResponseWriter, r *http.Request) {
142142
s.mu.Unlock()
143143

144144
if !ok {
145-
http.Error(w, "tsidp: client not found", http.StatusNotFound)
145+
writeHTTPError(w, r, http.StatusNotFound, "not_found", "tsidp: client not found")
146146
return
147147
}
148148

@@ -157,21 +157,21 @@ func (s *IDPServer) serveClients(w http.ResponseWriter, r *http.Request) {
157157
RedirectURIs: c.RedirectURIs,
158158
})
159159
default:
160-
http.Error(w, "tsidp: method not allowed", http.StatusMethodNotAllowed)
160+
writeHTTPError(w, r, http.StatusMethodNotAllowed, "invalid_request", "tsidp: method not allowed")
161161
}
162162
}
163163

164164
// serveNewClient creates a new OAuth client
165165
// Migrated from legacy/tsidp.go:2096-2126
166166
func (s *IDPServer) serveNewClient(w http.ResponseWriter, r *http.Request) {
167167
if r.Method != "POST" {
168-
http.Error(w, "tsidp: method not allowed", http.StatusMethodNotAllowed)
168+
writeHTTPError(w, r, http.StatusMethodNotAllowed, "invalid_request", "tsidp: method not allowed")
169169
return
170170
}
171171
redirectURI := r.FormValue("redirect_uri")
172172
name := r.FormValue("name")
173173
if redirectURI == "" || name == "" {
174-
http.Error(w, "tsidp: missing redirect_uri or name", http.StatusBadRequest)
174+
writeHTTPError(w, r, http.StatusBadRequest, "invalid_request", "tsidp: missing redirect_uri or name")
175175
return
176176
}
177177

@@ -196,7 +196,7 @@ func (s *IDPServer) serveNewClient(w http.ResponseWriter, r *http.Request) {
196196
s.funnelClients[clientID] = client
197197

198198
if err := s.storeFunnelClientsLocked(); err != nil {
199-
http.Error(w, fmt.Sprintf("tsidp: failed to store client: %v", err), http.StatusInternalServerError)
199+
writeHTTPError(w, r, http.StatusInternalServerError, "server_error", fmt.Sprintf("tsidp: failed to store client: %v", err))
200200
return
201201
}
202202

@@ -207,7 +207,7 @@ func (s *IDPServer) serveNewClient(w http.ResponseWriter, r *http.Request) {
207207
// Migrated from legacy/tsidp.go:2128-2145
208208
func (s *IDPServer) serveGetClientsList(w http.ResponseWriter, r *http.Request) {
209209
if r.Method != "GET" {
210-
http.Error(w, "tsidp: method not allowed", http.StatusMethodNotAllowed)
210+
writeHTTPError(w, r, http.StatusMethodNotAllowed, "invalid_request", "tsidp: method not allowed")
211211
return
212212
}
213213
s.mu.Lock()
@@ -231,14 +231,14 @@ func (s *IDPServer) serveGetClientsList(w http.ResponseWriter, r *http.Request)
231231
// Migrated from legacy/tsidp.go:2239-2265
232232
func (s *IDPServer) serveDeleteClient(w http.ResponseWriter, r *http.Request, clientID string) {
233233
if r.Method != "DELETE" {
234-
http.Error(w, "tsidp: method not allowed", http.StatusMethodNotAllowed)
234+
writeHTTPError(w, r, http.StatusMethodNotAllowed, "invalid_request", "tsidp: method not allowed")
235235
return
236236
}
237237
s.mu.Lock()
238238
defer s.mu.Unlock()
239239

240240
if _, ok := s.funnelClients[clientID]; !ok {
241-
http.Error(w, "tsidp: client not found", http.StatusNotFound)
241+
writeHTTPError(w, r, http.StatusNotFound, "not_found", "tsidp: client not found")
242242
return
243243
}
244244

@@ -262,7 +262,7 @@ func (s *IDPServer) serveDeleteClient(w http.ResponseWriter, r *http.Request, cl
262262
}
263263

264264
if err := s.storeFunnelClientsLocked(); err != nil {
265-
http.Error(w, fmt.Sprintf("tsidp: failed to store clients: %v", err), http.StatusInternalServerError)
265+
writeHTTPError(w, r, http.StatusInternalServerError, "server_error", fmt.Sprintf("tsidp: failed to store clients: %v", err))
266266
return
267267
}
268268

@@ -274,9 +274,10 @@ func (s *IDPServer) serveDeleteClient(w http.ResponseWriter, r *http.Request, cl
274274
func (s *IDPServer) serveDynamicClientRegistration(w http.ResponseWriter, r *http.Request) {
275275
// Block funnel requests - dynamic registration is only available over tailnet
276276
if isFunnelRequest(r) {
277-
writeJSONError(w, http.StatusForbidden, "access_denied", "dynamic client registration not available over funnel")
277+
writeHTTPError(w, r, http.StatusUnauthorized, "access_denied", "not available over funnel")
278278
return
279279
}
280+
280281
h := w.Header()
281282
h.Set("Access-Control-Allow-Origin", "*")
282283
h.Set("Access-Control-Allow-Method", "POST, OPTIONS")
@@ -288,18 +289,18 @@ func (s *IDPServer) serveDynamicClientRegistration(w http.ResponseWriter, r *htt
288289
}
289290

290291
if r.Method != "POST" {
291-
writeJSONError(w, http.StatusMethodNotAllowed, "invalid_request", "method not allowed")
292+
writeHTTPError(w, r, http.StatusMethodNotAllowed, "invalid_request", "method not allowed")
292293
return
293294
}
294295

295296
access, ok := r.Context().Value(appCapCtxKey).(*accessGrantedRules)
296297
if !ok {
297-
writeJSONError(w, http.StatusForbidden, "access_denied", "application capability not found")
298+
writeHTTPError(w, r, http.StatusForbidden, "access_denied", "application capability not found")
298299
return
299300
}
300301

301302
if !access.allowDCR {
302-
writeJSONError(w, http.StatusForbidden, "access_denied", "application capability not granted")
303+
writeHTTPError(w, r, http.StatusForbidden, "access_denied", "application capability not granted")
303304
return
304305
}
305306

@@ -317,13 +318,13 @@ func (s *IDPServer) serveDynamicClientRegistration(w http.ResponseWriter, r *htt
317318
}
318319

319320
if err := json.NewDecoder(r.Body).Decode(&registrationRequest); err != nil {
320-
writeJSONError(w, http.StatusBadRequest, "invalid_request", "invalid request body")
321+
writeHTTPError(w, r, http.StatusBadRequest, "invalid_request", "invalid request body")
321322
return
322323
}
323324

324325
// Validate required fields
325326
if len(registrationRequest.RedirectURIs) == 0 {
326-
writeJSONError(w, http.StatusBadRequest, "invalid_client_metadata", "redirect_uris is required")
327+
writeHTTPError(w, r, http.StatusBadRequest, "invalid_client_metadata", "redirect_uris is required")
327328
return
328329
}
329330

@@ -371,7 +372,7 @@ func (s *IDPServer) serveDynamicClientRegistration(w http.ResponseWriter, r *htt
371372
s.funnelClients[clientID] = client
372373

373374
if err := s.storeFunnelClientsLocked(); err != nil {
374-
writeJSONError(w, http.StatusInternalServerError, "server_error", "failed to store client")
375+
writeHTTPError(w, r, http.StatusInternalServerError, "server_error", "failed to store client")
375376
return
376377
}
377378

server/extraclaims.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ package server
55

66
import (
77
"fmt"
8-
"log"
8+
"log/slog"
99
)
1010

1111
// withExtraClaims merges flattened extra claims from a list of capRule into the provided map[string]any,
@@ -27,7 +27,7 @@ func withExtraClaims(claimMap map[string]any, rules []capRule) (map[string]any,
2727
extra := flattenExtraClaims(rules)
2828
for k, v := range extra {
2929
if _, isProtected := protected[k]; isProtected {
30-
log.Printf("Skip overwriting of existing claim %q", k)
30+
slog.Info("Skip overwriting of existing claim", slog.String("claim", k))
3131
return nil, fmt.Errorf("extra claim %q overwriting existing claim", k)
3232
}
3333

0 commit comments

Comments
 (0)