package shared import ( "crypto/rand" "encoding/json" "errors" "fmt" "net" "net/http" "net/url" "os" "os/exec" "time" ) // DefaultOIDCListen is the callback listener, matching the vault CLI default. const DefaultOIDCListen = "localhost:8250" // OIDCTimeout bounds how long LoginOIDC waits for the browser callback. var OIDCTimeout = 2 * time.Minute // OpenBrowser opens url in the user's browser; a package variable so tests can // drive the callback instead. var OpenBrowser = func(u string) error { return exec.Command("xdg-open", u).Start() } type oidcResult struct { tok *Token err error } // LoginOIDC runs the vault CLI's OIDC flow (vault-plugin-auth-jwt cli.go): // fetch auth_url, open the browser, receive code+state on a local callback, // and exchange them via auth//oidc/callback. func LoginOIDC(rc ResolvedContext) (*Token, error) { host, _, err := net.SplitHostPort(rc.OIDCListen) if err != nil { return nil, fmt.Errorf("oidc listen address %q: %w", rc.OIDCListen, err) } if host != "localhost" && host != "127.0.0.1" && host != "::1" { return nil, fmt.Errorf("oidc listen address %q must be on localhost, 127.0.0.1 or [::1]", rc.OIDCListen) } ln, err := net.Listen("tcp", rc.OIDCListen) if err != nil { return nil, fmt.Errorf("oidc callback listener: %w", err) } redirectURI := "http://" + rc.OIDCListen + "/oidc/callback" nonce := rand.Text() data, err := doRequest(http.MethodPost, rc.Address, "auth/"+rc.Path+"/oidc/auth_url", rc.Namespace, "", map[string]string{"role": rc.Role, "redirect_uri": redirectURI, "client_nonce": nonce}) if err != nil { _ = ln.Close() return nil, err } var ar struct { Data struct { AuthURL string `json:"auth_url"` } `json:"data"` } if err := json.Unmarshal(data, &ar); err != nil { _ = ln.Close() return nil, fmt.Errorf("decoding auth_url response: %w", err) } if ar.Data.AuthURL == "" { _ = ln.Close() return nil, fmt.Errorf("vault returned no auth_url for role %q with redirect_uri %q (check the role's allowed_redirect_uris)", rc.Role, redirectURI) } done := make(chan oidcResult, 1) mux := http.NewServeMux() mux.HandleFunc("/oidc/callback", func(w http.ResponseWriter, r *http.Request) { res := oidcCallback(rc, r, nonce) if res.err != nil { http.Error(w, "Vault login failed: "+res.err.Error(), http.StatusBadRequest) } else { _, _ = fmt.Fprintln(w, "Vault login successful. You can close this window.") } select { case done <- res: default: } }) srv := &http.Server{Handler: mux, ReadHeaderTimeout: 10 * time.Second} go func() { _ = srv.Serve(ln) }() defer func() { _ = srv.Close() }() fmt.Fprintf(os.Stderr, "Complete the login via your OIDC provider:\n\n %s\n\n", ar.Data.AuthURL) if err := OpenBrowser(ar.Data.AuthURL); err != nil { fmt.Fprintf(os.Stderr, "Could not open a browser (%v); visit the URL above manually.\n", err) } select { case res := <-done: return res.tok, res.err case <-time.After(OIDCTimeout): return nil, fmt.Errorf("timed out after %s waiting for the OIDC callback on %s", OIDCTimeout, redirectURI) } } // oidcCallback exchanges the provider's state+code for a Vault token. A POST // is the form_post response mode: forward the form to Vault, then GET as usual. func oidcCallback(rc ResolvedContext, r *http.Request, nonce string) oidcResult { if e := r.FormValue("error"); e != "" { return oidcResult{err: fmt.Errorf("provider returned %s: %s", e, r.FormValue("error_description"))} } state, code := r.FormValue("state"), r.FormValue("code") if state == "" || (code == "" && r.Method != http.MethodPost) { return oidcResult{err: errors.New("callback missing code or state")} } cb := "auth/" + rc.Path + "/oidc/callback" if r.Method == http.MethodPost { if _, err := doRequest(http.MethodPost, rc.Address, cb, rc.Namespace, "", map[string]string{"state": state, "code": code, "id_token": r.FormValue("id_token"), "client_nonce": nonce}); err != nil { return oidcResult{err: err} } } v := url.Values{"state": {state}, "code": {code}, "client_nonce": {nonce}} data, err := doRequest(http.MethodGet, rc.Address, cb+"?"+v.Encode(), rc.Namespace, "", nil) if err != nil { return oidcResult{err: err} } var ar authResponse if err := json.Unmarshal(data, &ar); err != nil { return oidcResult{err: fmt.Errorf("decoding callback response: %w", err)} } if ar.Auth.ClientToken == "" { return oidcResult{err: fmt.Errorf("oidc login for context %q returned no token", rc.Name)} } return oidcResult{tok: tokenFromAuth(rc, ar)} }