forked from go-kivik/couchdb
/
auth.go
156 lines (140 loc) · 4.08 KB
/
auth.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
package chttp
import (
"context"
"encoding/json"
"net/http"
"net/http/cookiejar"
"net/url"
"golang.org/x/net/publicsuffix"
"github.com/flimzy/kivik"
"github.com/flimzy/kivik/errors"
)
// Authenticator is an interface that provides authentication to a server.
type Authenticator interface {
Authenticate(context.Context, *Client) error
}
// BasicAuth provides HTTP Basic Auth for a client.
type BasicAuth struct {
Username string
Password string
// transport stores the original transport that is overridden by this auth
// mechanism
transport http.RoundTripper
}
var _ Authenticator = &BasicAuth{}
// RoundTrip fulfills the http.RoundTripper interface. It sets HTTP Basic Auth
// on outbound requests.
func (a *BasicAuth) RoundTrip(req *http.Request) (*http.Response, error) {
req.SetBasicAuth(a.Username, a.Password)
transport := a.transport
if transport == nil {
transport = http.DefaultTransport
}
return transport.RoundTrip(req)
}
// Authenticate sets HTTP Basic Auth headers for the client.
func (a *BasicAuth) Authenticate(ctx context.Context, c *Client) error {
// First see if the credentials seem good
req, err := c.NewRequest(ctx, kivik.MethodGet, "/_session", nil)
if err != nil {
return err // impossible error
}
req.SetBasicAuth(a.Username, a.Password)
res, err := c.Do(req)
if err != nil {
return err
}
defer res.Body.Close() // nolint: errcheck
if err = ResponseError(res); err != nil {
return err
}
result := struct {
Ctx struct {
Name string `json:"name"`
} `json:"userCtx"`
}{}
if err = json.NewDecoder(res.Body).Decode(&result); err != nil {
return errors.WrapStatus(kivik.StatusBadResponse, err)
}
if result.Ctx.Name != a.Username {
return errors.Status(kivik.StatusBadResponse, "authentication failed")
}
// Everything looks good, lets make this official
a.transport = c.Transport
c.Transport = a
return nil
}
// CookieAuth provides CouchDB Cookie auth services as described at
// http://docs.couchdb.org/en/2.0.0/api/server/authn.html#cookie-authentication
//
// CookieAuth stores authentication state after use, so should not be re-used.
type CookieAuth struct {
Username string `json:"name"`
Password string `json:"password"`
transport http.RoundTripper
// Set to true if the authenticator created the cookie jar; It will then
// also destroy it on logout.
setJar bool
jar http.CookieJar
dsn *url.URL
}
var _ Authenticator = &CookieAuth{}
// Authenticate initiates a session with the CouchDB server.
func (a *CookieAuth) Authenticate(ctx context.Context, c *Client) error {
if err := a.setCookieJar(c); err != nil {
return err // impossible error
}
a.jar = c.Jar
a.dsn = c.dsn
opts := &Options{
Body: EncodeBody(a),
}
if _, err := c.DoError(ctx, kivik.MethodPost, "/_session", opts); err != nil {
return err
}
return ValidateAuth(ctx, a.Username, c)
}
// Cookie returns the current session cookie and true, if found, or nil and
// false if not.
func (a *CookieAuth) Cookie() (*http.Cookie, bool) {
if a.jar == nil || a.dsn == nil {
return nil, false
}
for _, cookie := range a.jar.Cookies(a.dsn) {
if cookie.Name == kivik.SessionCookieName {
return cookie, true
}
}
return nil, false
}
// ValidateAuth validates that the requested username is authenticated.
func ValidateAuth(ctx context.Context, username string, client *Client) error {
// This does a final request to validate that auth was successful. Cookies
// may be filtered by a proxy, or a misconfigured client, so this check is
// necessary.
result := struct {
Ctx struct {
Name string `json:"name"`
} `json:"userCtx"`
}{}
if _, err := client.DoJSON(ctx, "GET", "/_session", nil, &result); err != nil {
return err
}
if result.Ctx.Name != username {
return errors.Status(kivik.StatusBadResponse, "auth response for unexpected user")
}
return nil
}
func (a *CookieAuth) setCookieJar(c *Client) error {
// If a jar is already set, just use it
if c.Jar != nil {
return nil
}
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
if err != nil {
return err // impossible error
}
c.Jar = jar
a.setJar = true
return nil
}