From 78ef15d413299eff61dc202a21a452d67a401e59 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Mar 2015 17:42:08 -0500 Subject: [PATCH] api: store token cookie, tests --- api/SPEC.md | 6 +++++- api/api_test.go | 26 +++++++++++++++++++++++++ api/client.go | 41 +++++++++++++++++++++++++++++++++++----- api/client_test.go | 47 ++++++++++++++++++++++++++++++++++++++++++++++ api/sys_login.go | 32 +++++++++++++++++++++++++++++++ 5 files changed, 146 insertions(+), 6 deletions(-) create mode 100644 api/api_test.go create mode 100644 api/client_test.go create mode 100644 api/sys_login.go diff --git a/api/SPEC.md b/api/SPEC.md index 8a49e07215..44cf3d386e 100644 --- a/api/SPEC.md +++ b/api/SPEC.md @@ -220,7 +220,11 @@ are immediately invalidated. Authenticate with Vault, returning an access token to use for future requests. This access token should be passed in as a cookie -for future requests. +for future requests with the key `token`. + +The token will also be set using the `Set-Cookie` headers as well, +so if the HTTP client being used has a well behaved cookie jar +implementation, the token will automatically be set for future requests. The request body of this request is arbitrary depending on the authentication method being used above. Authentication strategies diff --git a/api/api_test.go b/api/api_test.go new file mode 100644 index 0000000000..cca4584e06 --- /dev/null +++ b/api/api_test.go @@ -0,0 +1,26 @@ +package api + +import ( + "fmt" + "net" + "net/http" + "testing" +) + +// testHTTPServer creates a test HTTP server that handles requests until +// the listener returned is closed. +func testHTTPServer( + t *testing.T, handler http.Handler) (Config, net.Listener) { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("err: %s", err) + } + + server := &http.Server{Handler: handler} + go server.Serve(ln) + + config := DefaultConfig() + config.Address = fmt.Sprintf("http://%s", ln.Addr()) + + return config, ln +} diff --git a/api/client.go b/api/client.go index 814e3edc56..057a4a8f58 100644 --- a/api/client.go +++ b/api/client.go @@ -4,6 +4,13 @@ import ( "net/http" "net/http/cookiejar" "net/url" + "time" +) + +const ( + // TokenCookieName is the name of the cookie that contains the + // access token after logging in. + TokenCookieName = "token" ) // Config is used to configure the creation of the client. @@ -24,8 +31,8 @@ type Config struct { // DefaultConfig returns a default configuration for the client. It is // safe to modify the return value of this function. -func DefaultConfig() *Config { - config := &Config{ +func DefaultConfig() Config { + config := Config{ Address: "https://127.0.0.1:8200", HttpClient: http.DefaultClient, } @@ -52,14 +59,13 @@ func NewClient(c Config) (*Client, error) { // // If no cookie jar is set on the client, we set a default empty // cookie jar. - client := *c.HttpClient - if client.Jar == nil { + if c.HttpClient.Jar == nil { jar, err := cookiejar.New(&cookiejar.Options{}) if err != nil { return nil, err } - client.Jar = jar + c.HttpClient.Jar = jar } return &Client{ @@ -68,6 +74,31 @@ func NewClient(c Config) (*Client, error) { }, nil } +// Token returns the access token being used by this client. It will +// return the empty string if there is no token set. +func (c *Client) Token() string { + r := c.NewRequest("GET", "/") + for _, cookie := range c.config.HttpClient.Jar.Cookies(r.URL) { + if cookie.Name == TokenCookieName { + return cookie.Value + } + } + + return "" +} + +// ClearToken deletes the token cookie if it is set or does nothing otherwise. +func (c *Client) ClearToken() { + r := c.NewRequest("GET", "/") + c.config.HttpClient.Jar.SetCookies(r.URL, []*http.Cookie{ + &http.Cookie{ + Name: TokenCookieName, + Value: "", + Expires: time.Now().Add(-1 * time.Hour), + }, + }) +} + // NewRequest creates a new raw request object to query the Vault server // configured for this client. This is an advanced method and generally // doesn't need to be called externally. diff --git a/api/client_test.go b/api/client_test.go new file mode 100644 index 0000000000..9d4c57a6d9 --- /dev/null +++ b/api/client_test.go @@ -0,0 +1,47 @@ +package api + +import ( + "net/http" + "testing" + "time" +) + +func TestClientToken(t *testing.T) { + tokenValue := "foo" + handler := func(w http.ResponseWriter, req *http.Request) { + http.SetCookie(w, &http.Cookie{ + Name: TokenCookieName, + Value: tokenValue, + Expires: time.Now().Add(time.Hour), + }) + } + + config, ln := testHTTPServer(t, http.HandlerFunc(handler)) + defer ln.Close() + + client, err := NewClient(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + // Should have no token initially + if v := client.Token(); v != "" { + t.Fatalf("bad: %s", v) + } + + // Do a raw "/" request to set the cookie + if _, err := client.RawRequest(client.NewRequest("GET", "/")); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the token is set + if v := client.Token(); v != tokenValue { + t.Fatalf("bad: %s", v) + } + + client.ClearToken() + + if v := client.Token(); v != "" { + t.Fatalf("bad: %s", v) + } +} diff --git a/api/sys_login.go b/api/sys_login.go new file mode 100644 index 0000000000..e23d61a568 --- /dev/null +++ b/api/sys_login.go @@ -0,0 +1,32 @@ +package api + +import ( + "fmt" +) + +// Login performs the /sys/login API call. +// +// This API call is stateful: it will set the access token on the client +// for future API calls to be authenticated. The access token can be retrieved +// at any time from the client using `client.Token()` and it can be cleared +// with `sys.Logout()`. +func (c *Sys) Login(vars map[string]string) error { + r := c.c.NewRequest("PUT", "/sys/login") + if err := r.SetJSONBody(vars); err != nil { + return err + } + + resp, err := c.c.RawRequest(r) + if err != nil { + return err + } + defer resp.Body.Close() + + if c.c.Token() == "" { + return fmt.Errorf( + "Login had status code %d, but token cookie was not set!", + resp.StatusCode) + } + + return nil +}