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
// 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: <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 != "" {
@ -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)
}

View File

@ -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) {

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
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).

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
[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