Fixes #4483: Add support for Authorization: Bearer token Header (#4502)

Added Authorization Bearer token support as per RFC6750

* appended Authorization header token parsing after X-Consul-Token
* added test cases
* updated website documentation to mention Authorization header

* improve tests, improve Bearer parsing
This commit is contained in:
Miroslav Bagljas 2018-08-17 22:18:42 +02:00 committed by Matt Keeler
parent 89194ee16c
commit 8f7e87439a
4 changed files with 121 additions and 11 deletions

View File

@ -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 // parseTokenInternal is used to parse the ?token query param or the X-Consul-Token header or
// optionally resolve proxy tokens to real ACL tokens. If no token is specified it will populate // 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) // 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) { func (s *HTTPServer) parseTokenInternal(req *http.Request, token *string, resolveProxyToken bool) {
tok := "" tok := ""
if other := req.URL.Query().Get("token"); other != "" { if other := req.URL.Query().Get("token"); other != "" {
tok = other tok = other
} else if other := req.Header.Get("X-Consul-Token"); other != "" { } else if other := req.Header.Get("X-Consul-Token"); other != "" {
tok = other tok = other
} else if other := req.Header.Get("Authorization"); other != "" {
// HTTP Authorization headers are in the format: <Scheme>[SPACE]<Value>
// 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.:
// "" || "<Scheme><Value>" || "<Scheme>" || "<Value>"
if len(parts) > 1 {
scheme := parts[0]
// Everything after "<Scheme>" is "<Value>", trimmed
value := strings.TrimSpace(strings.Join(parts[1:], " "))
// <Scheme> 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 != "" { if tok != "" {
@ -589,13 +610,14 @@ func (s *HTTPServer) parseTokenInternal(req *http.Request, token *string, resolv
*token = s.agent.tokens.UserToken() *token = s.agent.tokens.UserToken()
} }
// parseToken is used to parse the ?token query param or the X-Consul-Token header and // parseToken is used to parse the ?token query param or the X-Consul-Token header or
// resolve proxy tokens to real ACL tokens // Authorization Bearer token header (RFC6750) and resolve proxy tokens to real ACL tokens
func (s *HTTPServer) parseToken(req *http.Request, token *string) { func (s *HTTPServer) parseToken(req *http.Request, token *string) {
s.parseTokenInternal(req, token, true) s.parseTokenInternal(req, token, true)
} }
// parseTokenWithoutResolvingProxyToken is used to parse the ?token query param or the X-Consul-Token header // 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) { func (s *HTTPServer) parseTokenWithoutResolvingProxyToken(req *http.Request, token *string) {
s.parseTokenInternal(req, token, false) s.parseTokenInternal(req, token, false)
} }

View File

@ -736,6 +736,40 @@ func TestACLResolution(t *testing.T) {
reqBothTokens, _ := http.NewRequest("GET", "/v1/catalog/nodes?token=baz", nil) reqBothTokens, _ := http.NewRequest("GET", "/v1/catalog/nodes?token=baz", nil)
reqBothTokens.Header.Add("X-Consul-Token", "zap") 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(), "") a := NewTestAgent(t.Name(), "")
defer a.Shutdown() defer a.Shutdown()
@ -770,6 +804,58 @@ func TestACLResolution(t *testing.T) {
if token != "baz" { if token != "baz" {
t.Fatalf("bad: %s", token) 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) { func TestEnableWebUI(t *testing.T) {

View File

@ -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 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` 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 configuration option. However, the token can also be specified per-request
by using the `X-Consul-Token` request header or the `token` query string by using the `X-Consul-Token` request header or Bearer header in Authorization
parameter. The request header takes precedence over the default token, and header or the `token` query string parameter. The request header takes
the query string parameter takes precedence over everything. 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). For more details about ACLs, please see the [ACL Guide](/docs/guides/acl.html).

View File

@ -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 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` [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 [CLI commands](/docs/commands/index.html) can accept tokens via the
`token` argument, or the `CONSUL_HTTP_TOKEN` environment variable. `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` 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 query string parameter, or the `X-Consul-Token` request header, or Authorization
CLI commands via the `token` argument, or the `CONSUL_HTTP_TOKEN` environment Bearer token header, or Consul's CLI commands via the `token` argument,
variable. or the `CONSUL_HTTP_TOKEN` environment variable.
#### Agent Rules #### Agent Rules