package idp import ( "bytes" "context" "encoding/json" "fmt" "html/template" "maps" "net/http" "net/url" "slices" "strconv" "strings" transporthttp "tug/internal/transport/http" "tug/internal/transport/web/text" ) const DiscoveryPath = "/.well-known/openid-configuration" var ( getPaths = map[string]bool{ DiscoveryPath: true, "/keys": true, "/auth": true, "/auth/" + ConnectorID: true, "/callback": true, "/userinfo": true, "/static/main.css": true, "/theme/styles.css": true, "/theme/logo.png": true, "/theme/favicon.png": true, } postPaths = map[string]bool{ "/token": true, "/userinfo": true, } noStorePaths = map[string]bool{ "/auth": true, "/auth/" + ConnectorID: true, "/callback": true, } hiddenDiscoveryKeys = []string{"introspection_endpoint", "device_authorization_endpoint"} ) type handler struct { dex http.Handler discovery []byte public map[string]bool page transporthttp.ErrorPage } func newHandler(dex http.Handler, public map[string]bool, page transporthttp.ErrorPage) (*handler, error) { doc, err := filteredDiscovery(dex) if err != nil { return nil, err } return &handler{dex: dex, discovery: doc, public: public, page: page}, nil } func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { p := r.URL.Path if !(r.Method == http.MethodGet && getPaths[p]) && !(r.Method == http.MethodPost && postPaths[p]) { w.Header().Set("Cache-Control", "no-store") h.page(w, r, http.StatusNotFound) return } front := p == "/callback" if noStorePaths[p] { w.Header().Set("Cache-Control", "no-store") } switch { case p == DiscoveryPath: w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Length", strconv.Itoa(len(h.discovery))) w.Write(h.discovery) return case p == "/auth/"+ConnectorID || p == "/auth": q, err := transporthttp.Query(r) if err != nil { h.page(w, r, http.StatusBadRequest) return } if p == "/auth" && q.Get("req") != "" { front = true break } fresh, status := h.fresh(r, q) if status != 0 { h.page(w, r, status) return } r = fresh.WithContext(context.WithValue(fresh.Context(), hintContext{}, q.Get("id_token_hint"))) } if front && frontOn() { h.front(w, r, p) return } h.dex.ServeHTTP(w, r) } var ( frontOn = func() bool { return true } frontTap = func(path string, status int, header http.Header, body []byte) {} ) var interruptions = []struct { path string status int message string }{ {"/callback", http.StatusBadRequest, "Requested resource does not exist."}, {"/callback", http.StatusInternalServerError, "Login error."}, {"/callback", http.StatusInternalServerError, "Authentication failed. Please contact your administrator or try again later."}, {"/auth", http.StatusInternalServerError, "Internal server error."}, } var interruptedPage = template.Must(template.New("interrupted").Parse(` {{.Title}}

{{.Title}}

{{.Body}}

`)) var interruptedPages = map[int][]byte{ http.StatusBadRequest: renderInterrupted(text.ErrorBadRequestTitle), http.StatusInternalServerError: renderInterrupted(text.ErrorInternalTitle), } func renderInterrupted(title text.Key) []byte { heading, _ := text.Lookup(title) body, _ := text.Lookup(text.LoginInterrupted) var buf bytes.Buffer if err := interruptedPage.Execute(&buf, struct{ Title, Body string }{heading, body}); err != nil { panic(err) } return buf.Bytes() } func (h *handler) front(w http.ResponseWriter, r *http.Request, p string) { c := &capture{header: w.Header().Clone()} h.dex.ServeHTTP(c, r) if c.status == 0 { c.status = http.StatusOK } frontTap(p, c.status, c.header, c.body.Bytes()) if interrupted(p, c.status, c.body.Bytes()) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(c.status) w.Write(interruptedPages[c.status]) return } dst := w.Header() clear(dst) maps.Copy(dst, c.header) w.WriteHeader(c.status) w.Write(c.body.Bytes()) } func interrupted(p string, status int, body []byte) bool { for _, i := range interruptions { if i.path == p && i.status == status && bytes.Contains(body, []byte(i.message)) { return true } } return false } func (h *handler) fresh(r *http.Request, q url.Values) (*http.Request, int) { if q.Get("nonce") == "" { return r, http.StatusBadRequest } if h.public[q.Get("client_id")] && (q.Get("code_challenge") == "" || q.Get("code_challenge_method") != "S256") { return r, http.StatusBadRequest } changed := false if prompts, ok := q["prompt"]; ok { var kept []string for _, v := range prompts { for _, p := range strings.Fields(v) { if p == "consent" && !slices.Contains(strings.Fields(v), "none") { changed = true continue } kept = append(kept, p) } } if changed { if len(kept) == 0 { q.Del("prompt") } else { q.Set("prompt", strings.Join(kept, " ")) } } } if q.Get("approval_prompt") == "force" { q.Del("approval_prompt") changed = true } if !changed { return r, 0 } stripped := r.Clone(r.Context()) stripped.URL.RawQuery = q.Encode() stripped.RequestURI = stripped.URL.RequestURI() return stripped, 0 } type capture struct { header http.Header status int body bytes.Buffer } func (c *capture) Header() http.Header { return c.header } func (c *capture) WriteHeader(status int) { if c.status == 0 { c.status = status } } func (c *capture) Write(b []byte) (int, error) { if c.status == 0 { c.status = http.StatusOK } return c.body.Write(b) } func filteredDiscovery(dex http.Handler) ([]byte, error) { c := &capture{header: http.Header{}} dex.ServeHTTP(c, &http.Request{Method: http.MethodGet, URL: &url.URL{Path: DiscoveryPath}, Header: http.Header{}, Host: "localhost"}) if c.status != http.StatusOK { return nil, fmt.Errorf("idp: dex discovery answered %d", c.status) } var doc map[string]json.RawMessage if err := json.Unmarshal(c.body.Bytes(), &doc); err != nil { return nil, fmt.Errorf("idp: decode dex discovery: %w", err) } for _, k := range hiddenDiscoveryKeys { delete(doc, k) } out, err := json.MarshalIndent(doc, "", " ") if err != nil { return nil, fmt.Errorf("idp: encode discovery: %w", err) } return out, nil }