Skip to content

Commit

Permalink
Merge pull request #296 from go-kivik/jwt3
Browse files Browse the repository at this point in the history
Backport JWT auth support
  • Loading branch information
flimzy authored Jan 2, 2022
2 parents 44d071f + 06df4cf commit 8c0cd7a
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 4 deletions.
13 changes: 12 additions & 1 deletion auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"net/http"

"github.com/go-kivik/couchdb/v3/chttp"
kivik "github.com/go-kivik/kivik/v3"
"github.com/go-kivik/kivik/v3"
)

func (c *client) Authenticate(ctx context.Context, a interface{}) error {
Expand Down Expand Up @@ -79,6 +79,17 @@ func CookieAuth(user, password string) Authenticator {
})
}

// JWTAuth provides support for CouchDB JWT-based authentication. Kivik does
// no validation on the JWT token; it is passed verbatim to the server.
//
// See https://docs.couchdb.org/en/latest/api/server/authn.html#jwt-authentication
func JWTAuth(token string) Authenticator {
auth := chttp.JWTAuth{Token: token}
return authFunc(func(ctx context.Context, c *client) error {
return auth.Authenticate(c.Client)
})
}

// ProxyAuth provides support for Proxy authentication.
//
// The `secret` argument represents the `couch_httpd_auth/secret` value
Expand Down
12 changes: 12 additions & 0 deletions auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,18 @@ func TestAuthentication(t *testing.T) {
authStatus: http.StatusBadRequest,
authErr: "kivik: HTTP client transport already set",
})
tests.Add("JWTAuth", tst{
handler: func(t *testing.T) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if h := r.Header.Get("Authorization"); h != "Bearer tokentoken" {
t.Errorf("Unexpected Auth header: %s\n", h)
}
w.WriteHeader(200)
_, _ = w.Write([]byte(`{}`))
})
},
auther: JWTAuth("tokentoken"), // nolint:misspell
})

driver := &Couch{}
tests.Run(t, func(t *testing.T, test tst) {
Expand Down
13 changes: 12 additions & 1 deletion chttp/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ func TestAuthenticate(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close() // nolint: errcheck
var authed bool
if auth := r.Header.Get("Authorization"); auth == "Basic YWRtaW46YWJjMTIz" {
switch r.Header.Get("Authorization") {
case "Basic YWRtaW46YWJjMTIz", "Bearer tokennekot":
authed = true
}
if r.Method == http.MethodPost {
Expand Down Expand Up @@ -133,6 +134,16 @@ func TestAuthenticate(t *testing.T) {
jar: jar,
}
})
tests.Add("JWT auth", authTest{
addr: s.URL,
auther: &JWTAuth{Token: "tokennekot"}, // nolint: misspell
})
tests.Add("failed JWT auth", authTest{
addr: s.URL,
auther: &JWTAuth{Token: "nekot"}, // nolint: misspell
err: "Unauthorized",
status: http.StatusUnauthorized,
})

tests.Run(t, func(t *testing.T, test authTest) {
ctx := context.Background()
Expand Down
21 changes: 21 additions & 0 deletions chttp/basicauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,24 @@ func (a *BasicAuth) Authenticate(c *Client) error {
c.Transport = a
return nil
}

// JWTAuth provides JWT based auth for a client.
type JWTAuth struct {
Token string

transport http.RoundTripper
}

func (a *JWTAuth) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Set("Authorization", "Bearer "+a.Token)
return a.transport.RoundTrip(req)
}

func (a *JWTAuth) Authenticate(c *Client) error {
a.transport = c.Transport
if a.transport == nil {
a.transport = http.DefaultTransport
}
c.Transport = a
return nil
}
70 changes: 70 additions & 0 deletions chttp/basicauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,73 @@ func TestBasicAuthRoundTrip(t *testing.T) {
})
}
}

func TestJWTAuthRoundTrip(t *testing.T) {
type rtTest struct {
name string
auth *JWTAuth
req *http.Request
expected *http.Response
cleanup func()
}
tests := []rtTest{
{
name: "Provided transport",
req: httptest.NewRequest("GET", "/", nil),
auth: &JWTAuth{
Token: "token",
transport: customTransport(func(req *http.Request) (*http.Response, error) {
if h := req.Header.Get("Authorization"); h != "Bearer token" {
t.Errorf("Unexpected authorization header: %s", h)
}
return &http.Response{StatusCode: 200}, nil
}),
},
expected: &http.Response{StatusCode: 200},
},
func() rtTest {
h := func(w http.ResponseWriter, r *http.Request) {
if h := r.Header.Get("Authorization"); h != "Bearer token" {
t.Errorf("Unexpected authorization header: %s", h)
}
w.Header().Set("Date", "Wed, 01 Nov 2017 19:32:41 GMT")
w.Header().Set("Content-Type", "application/json")
}
s := httptest.NewServer(http.HandlerFunc(h))
return rtTest{
name: "default transport",
auth: &JWTAuth{
Token: "token",
transport: http.DefaultTransport,
},
req: httptest.NewRequest("GET", s.URL, nil),
expected: &http.Response{
Status: "200 OK",
StatusCode: 200,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: http.Header{
"Content-Length": {"0"},
"Content-Type": {"application/json"},
"Date": {"Wed, 01 Nov 2017 19:32:41 GMT"},
},
},
cleanup: func() { s.Close() },
}
}(),
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := test.auth.RoundTrip(test.req)
if err != nil {
t.Fatal(err)
}
res.Body = nil
res.Request = nil
if d := testy.DiffInterface(test.expected, res); d != nil {
t.Error(d)
}
})
}
}
2 changes: 1 addition & 1 deletion chttp/chttp.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const typeJSON = "application/json"
// The default UserAgent values
const (
UserAgent = "Kivik chttp"
Version = "3.2.7"
Version = "3.3.0"
)

// Client represents a client connection. It embeds an *http.Client
Expand Down
2 changes: 1 addition & 1 deletion constants.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package couchdb

// Version is the current version of this package.
const Version = "3.2.7"
const Version = "3.3.0"

const (
// OptionFullCommit is the option key used to set the `X-Couch-Full-Commit`
Expand Down

0 comments on commit 8c0cd7a

Please sign in to comment.