package oidc import ( "context" "fmt" "net/http" "net/url" "path" "sync" "time" "github.com/hashicorp/vault/api" ) // handler builds the http.HandlerFunc that completes the OIDC exchange. // once guards against a stray second request (retries, prefetchers, a // double-click) sending on the already-buffered done channel more than // once — sending twice would be harmless here (done is buffered 1 and // nobody reads twice) but responding twice to the browser is not, so we // gate the whole body. func (f *Flow) handler(c *api.Client, done chan<- result, once *sync.Once) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var res result handled := false defer func() { if !handled { return } if res.err != nil { w.WriteHeader(http.StatusBadRequest) _, _ = w.Write(errorHTML(res.err)) } else { _, _ = w.Write(successHTML()) } once.Do(func() { done <- res }) }() if r.URL.Path != f.cfg.CallbackPath { http.NotFound(w, r) return } handled = true if err := r.ParseForm(); err != nil { res.err = fmt.Errorf("parsing callback request: %w", err) return } if errCode := r.FormValue("error"); errCode != "" { res.err = ProviderRejectedError{Code: errCode, Description: r.FormValue("error_description")} return } state := r.FormValue("state") code := r.FormValue("code") idToken := r.FormValue("id_token") if state == "" { res.err = fmt.Errorf("OIDC callback missing state parameter") return } // The callback exchange is tied to the Flow's own context (captured // via closure below is not possible here since Login already holds // it) — use a background context bounded by a short deadline instead // of r.Context(), so a browser tab closed mid-exchange does not abort // an exchange that Vault is still processing. ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() data := url.Values{ "state": {state}, "code": {code}, "id_token": {idToken}, "client_nonce": {f.nonce}, } p := path.Join("auth", f.cfg.Mount, "oidc/callback") sec, err := c.Logical().ReadWithDataWithContext(ctx, p, data) if err != nil { res.err = fmt.Errorf("completing OIDC login: %w", err) return } if sec == nil || sec.Auth == nil || sec.Auth.ClientToken == "" { res.err = fmt.Errorf("Vault returned no token from the OIDC callback") return } res.secret = sec } } // ProviderRejectedError wraps an error= / error_description= pair the // identity provider appended to the redirect (e.g. the user clicked "deny"). type ProviderRejectedError struct { Code string Description string } func (e ProviderRejectedError) Error() string { if e.Description != "" { return fmt.Sprintf("identity provider rejected the login: %s (%s)", e.Code, e.Description) } return fmt.Sprintf("identity provider rejected the login: %s", e.Code) }