diff --git a/agent/http.go b/agent/http.go index 1a0586f28ae2..d50651111be7 100644 --- a/agent/http.go +++ b/agent/http.go @@ -563,15 +563,36 @@ func (s *HTTPServer) parseDC(req *http.Request, dc *string) { } } -// parseTokenInternal is used to parse the ?token query param or the X-Consul-Token header and -// optionally resolve proxy tokens to real ACL tokens. If no token is specified it will populate +// parseTokenInternal is used to parse the ?token query param or the X-Consul-Token header or +// Authorization Bearer token (RFC6750) and +// optionally resolve proxy tokens to real ACL tokens. If the token is invalid or not specified it will populate // the token with the agents UserToken (acl_token in the consul configuration) +// Parsing has the following priority: ?token, X-Consul-Token and last "Authorization: Bearer " func (s *HTTPServer) parseTokenInternal(req *http.Request, token *string, resolveProxyToken bool) { tok := "" if other := req.URL.Query().Get("token"); other != "" { tok = other } else if other := req.Header.Get("X-Consul-Token"); other != "" { tok = other + } else if other := req.Header.Get("Authorization"); other != "" { + // HTTP Authorization headers are in the format: [SPACE] + // Ref. https://tools.ietf.org/html/rfc7236#section-3 + parts := strings.Split(other, " ") + + // Authorization Header is invalid if containing 1 or 0 parts, e.g.: + // "" || "" || "" || "" + if len(parts) > 1 { + scheme := parts[0] + // Everything after "" is "", trimmed + value := strings.TrimSpace(strings.Join(parts[1:], " ")) + + // must be "Bearer" + if scheme == "Bearer" { + // Since Bearer tokens shouldnt contain spaces (rfc6750#section-2.1) + // "value" is tokenized, only the first item is used + tok = strings.TrimSpace(strings.Split(value, " ")[0]) + } + } } if tok != "" { @@ -589,13 +610,14 @@ func (s *HTTPServer) parseTokenInternal(req *http.Request, token *string, resolv *token = s.agent.tokens.UserToken() } -// parseToken is used to parse the ?token query param or the X-Consul-Token header and -// resolve proxy tokens to real ACL tokens +// parseToken is used to parse the ?token query param or the X-Consul-Token header or +// Authorization Bearer token header (RFC6750) and resolve proxy tokens to real ACL tokens func (s *HTTPServer) parseToken(req *http.Request, token *string) { s.parseTokenInternal(req, token, true) } // parseTokenWithoutResolvingProxyToken is used to parse the ?token query param or the X-Consul-Token header +// or Authorization Bearer header token (RFC6750) and func (s *HTTPServer) parseTokenWithoutResolvingProxyToken(req *http.Request, token *string) { s.parseTokenInternal(req, token, false) } diff --git a/agent/http_test.go b/agent/http_test.go index 8338af0374b7..a527dcdaed30 100644 --- a/agent/http_test.go +++ b/agent/http_test.go @@ -736,6 +736,40 @@ func TestACLResolution(t *testing.T) { reqBothTokens, _ := http.NewRequest("GET", "/v1/catalog/nodes?token=baz", nil) reqBothTokens.Header.Add("X-Consul-Token", "zap") + // Request with Authorization Bearer token + reqAuthBearerToken, _ := http.NewRequest("GET", "/v1/catalog/nodes", nil) + reqAuthBearerToken.Header.Add("Authorization", "Bearer bearer-token") + + // Request with invalid Authorization scheme + reqAuthBearerInvalidScheme, _ := http.NewRequest("GET", "/v1/catalog/nodes", nil) + reqAuthBearerInvalidScheme.Header.Add("Authorization", "Beer") + + // Request with empty Authorization Bearer token + reqAuthBearerTokenEmpty, _ := http.NewRequest("GET", "/v1/catalog/nodes", nil) + reqAuthBearerTokenEmpty.Header.Add("Authorization", "Bearer") + + // Request with empty Authorization Bearer token + reqAuthBearerTokenInvalid, _ := http.NewRequest("GET", "/v1/catalog/nodes", nil) + reqAuthBearerTokenInvalid.Header.Add("Authorization", "Bearertoken") + + // Request with more than one space between Bearer and token + reqAuthBearerTokenMultiSpaces, _ := http.NewRequest("GET", "/v1/catalog/nodes", nil) + reqAuthBearerTokenMultiSpaces.Header.Add("Authorization", "Bearer bearer-token") + + // Request with Authorization Bearer token containing spaces + reqAuthBearerTokenSpaces, _ := http.NewRequest("GET", "/v1/catalog/nodes", nil) + reqAuthBearerTokenSpaces.Header.Add("Authorization", "Bearer bearer-token "+ + " the rest is discarded ") + + // Request with Authorization Bearer and querystring token + reqAuthBearerAndQsToken, _ := http.NewRequest("GET", "/v1/catalog/nodes?token=qstoken", nil) + reqAuthBearerAndQsToken.Header.Add("Authorization", "Bearer bearer-token") + + // Request with Authorization Bearer and X-Consul-Token header token + reqAuthBearerAndXToken, _ := http.NewRequest("GET", "/v1/catalog/nodes", nil) + reqAuthBearerAndXToken.Header.Add("X-Consul-Token", "xtoken") + reqAuthBearerAndXToken.Header.Add("Authorization", "Bearer bearer-token") + a := NewTestAgent(t.Name(), "") defer a.Shutdown() @@ -770,6 +804,58 @@ func TestACLResolution(t *testing.T) { if token != "baz" { t.Fatalf("bad: %s", token) } + + // + // Authorization Bearer token tests + // + + // Check if Authorization bearer token header is parsed correctly + a.srv.parseToken(reqAuthBearerToken, &token) + if token != "bearer-token" { + t.Fatalf("bad: %s", token) + } + + // Check Authorization Bearer scheme invalid + a.srv.parseToken(reqAuthBearerInvalidScheme, &token) + if token != "agent" { + t.Fatalf("bad: %s", token) + } + + // Check if Authorization Bearer token is empty + a.srv.parseToken(reqAuthBearerTokenEmpty, &token) + if token != "agent" { + t.Fatalf("bad: %s", token) + } + + // Check if the Authorization Bearer token is invalid + a.srv.parseToken(reqAuthBearerTokenInvalid, &token) + if token != "agent" { + t.Fatalf("bad: %s", token) + } + + // Check multi spaces between Authorization Bearer and token value + a.srv.parseToken(reqAuthBearerTokenMultiSpaces, &token) + if token != "bearer-token" { + t.Fatalf("bad: %s", token) + } + + // Check if Authorization Bearer token with spaces is parsed correctly + a.srv.parseToken(reqAuthBearerTokenSpaces, &token) + if token != "bearer-token" { + t.Fatalf("bad: %s", token) + } + + // Check if explicit token has precedence over Authorization bearer token + a.srv.parseToken(reqAuthBearerAndQsToken, &token) + if token != "qstoken" { + t.Fatalf("bad: %s", token) + } + + // Check if X-Consul-Token has precedence over Authorization bearer token + a.srv.parseToken(reqAuthBearerAndXToken, &token) + if token != "xtoken" { + t.Fatalf("bad: %s", token) + } } func TestEnableWebUI(t *testing.T) { diff --git a/website/source/api/index.html.md b/website/source/api/index.html.md index df9622d5d512..ed3d5d555dc2 100644 --- a/website/source/api/index.html.md +++ b/website/source/api/index.html.md @@ -21,9 +21,10 @@ All API routes are prefixed with `/v1/`. This documentation is only for the v1 A Several endpoints in Consul use or require ACL tokens to operate. An agent can be configured to use a default token in requests using the `acl_token` configuration option. However, the token can also be specified per-request -by using the `X-Consul-Token` request header or the `token` query string -parameter. The request header takes precedence over the default token, and -the query string parameter takes precedence over everything. +by using the `X-Consul-Token` request header or Bearer header in Authorization +header or the `token` query string parameter. The request header takes +precedence over the default token, and the query string parameter takes +precedence over everything. For more details about ACLs, please see the [ACL Guide](/docs/guides/acl.html). diff --git a/website/source/docs/guides/acl.html.md b/website/source/docs/guides/acl.html.md index c25666ab9480..9f5db4681857 100644 --- a/website/source/docs/guides/acl.html.md +++ b/website/source/docs/guides/acl.html.md @@ -32,7 +32,8 @@ The type is either "client" (meaning the token cannot modify ACL rules) or "mana The token ID is passed along with each RPC request to the servers. Consul's [HTTP endpoints](/api/index.html) can accept tokens via the `token` -query string parameter, or the `X-Consul-Token` request header. Consul's +query string parameter, or the `X-Consul-Token` request header, or Authorization Bearer +token [RFC6750](https://tools.ietf.org/html/rfc6750). Consul's [CLI commands](/docs/commands/index.html) can accept tokens via the `token` argument, or the `CONSUL_HTTP_TOKEN` environment variable. @@ -612,9 +613,9 @@ On success, the token ID is returned: ``` This token ID can then be passed into Consul's HTTP APIs via the `token` -query string parameter, or the `X-Consul-Token` request header, or Consul's -CLI commands via the `token` argument, or the `CONSUL_HTTP_TOKEN` environment -variable. +query string parameter, or the `X-Consul-Token` request header, or Authorization +Bearer token header, or Consul's CLI commands via the `token` argument, +or the `CONSUL_HTTP_TOKEN` environment variable. #### Agent Rules