Fixing go-oauth2’s case sensitive bearer token authorization headers

Last update:

The Go OAuth2 package does not always comply with the OAuth 2.0 specification in regards to case sensitivity in authorization headers using bearer tokens. It can cause incompatibilities with strict OAuth 2.0 providers. This article presents a workaround, without requiring any changes to the go-oauth2 code itself.

Problem statement

As per RFC 6749 section 5.1 the token_type literal returned when requesting access or refresh tokens is case insensitive:

token_type: Required. The type of the token issued as described in Section 7.1. Value is case insensitive.

The token type determines the authorization scheme used for requesting access and refresh tokens. The most common token type is the bearer token. Usually the provider describes it as “Bearer”, but some providers will return “bearer” (lowercase) instead.

On the other hand, RFC 6750 section 2.1 states that the Authorization header scheme for bearer tokens must be capitalized:

Clients should make authenticated requests with a bearer token using the “Authorization” request header field with the “Bearer” HTTP authorization scheme.

For example:

GET /resource HTTP/1.1
Host: server.example.com
Authorization: Bearer mF_9.B5f-4.1JqM

The syntax for Bearer credentials is as follows:

b64token    = 1*( ALPHA / DIGIT / "-" / "." /
                  "_" / "~" / "+" / "/" ) *"="
credentials = "Bearer" 1*SP b64token

Yet go-oauth2 stores the token_type as returned by the provider and uses it verbatim when requesting access and refresh tokens. This causes problems when a provider replies with a non-capitalized “bearer” token type but expects a capitalized “Bearer” scheme for authentication. Such a provider still adheres to the specification but go-oauth2 cannot interact with it.

The Amazon Cloud Drive API does exactly that: it returns a lowercase token_type but expects a capitalized scheme name in authorization requests. I’ve ran into this issue while working on acdcli and received HTTP 403: Forbidden errors when refreshing the token.

Solution

While researching the problem I came across an open issue on github and two solutions. Both solutions require modifying the go-oauth2 source code, which I want to avoid.

Instead I’ve created a transport wrapper for the HTTP client. When it encounters a request with an Authorization header of scheme bearer, it will replace it with a cloned request of authorization scheme Bearer and pass it on to the wrapped transport. This complies with the OAuth 2.0 specification and fixes the issue.

package client

import (
    "net/http"
    "strings"
)

// BearerAuthTransport wraps a RoundTripper. It capitalized bearer token
// authorization headers.
type BearerAuthTransport struct {
    rt http.RoundTripper
}

// RoundTrip satisfies the RoundTripper interface. It replaces authorization
// headers of scheme `bearer` by capitalized `Bearer` (as per OAuth 2.0 spec).
func (t *BearerAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    auth := req.Header.Get("Authorization")
    if strings.HasPrefix(strings.ToLower(auth), "bearer ") {
        auth = "Bearer " + auth[7:]
    }

    req2 := cloneRequest(req) // per RoundTripper contract
    req2.Header.Set("Authorization", auth)

    return t.rt.RoundTrip(req2)
}

// cloneRequest returns a clone of the provided *http.Request.
// The clone is a shallow copy of the struct and its Header map.
func cloneRequest(r *http.Request) *http.Request {
    // shallow copy of the struct
    r2 := new(http.Request)
    *r2 = *r
    // deep copy of the Header
    r2.Header = make(http.Header, len(r.Header))
    for k, s := range r.Header {
        r2.Header[k] = append([]string(nil), s...)
    }
    return r2
}

The transport wrapper BearerAuthTransport is used as follows:

ctx := context.Background()
// Prepare wrapper to fix Bearer authorization
var transport http.RoundTripper = &BearerAuthTransport{http.DefaultTransport}
// Override default HTTP client in ctx
ctx = context.WithValue(ctx, oauth2.HTTPClient, &http.Client{Transport: transport})

config := ... // the oauth2 config
token := ...  // your favorite way to get the token, or nil
client := config.Client(ctx, token)

Out in the wild, the transport can be found here and its usage here.

This solution will continue to work even when (or if) go-oauth2 gets fixed. It won’t conflict in any way.


See all posts in the archive.

Comments

Comments were disabled in March 2022. Since this page was created earlier, there may have been previous comments which are now inaccessible. Sorry.