This repository has been archived by the owner on Aug 24, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 11
/
oauth2_http_client.go
200 lines (161 loc) · 4.53 KB
/
oauth2_http_client.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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
package client
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"sync"
"time"
)
// Oauth2HTTPClient sets the "Authorization" header of any outgoing request.
// It gets a JWT from the configured Oauth2 server. It only gets a new JWT
// when a request comes back with a 401.
type Oauth2HTTPClient struct {
c HTTPClient
oauth2Addr string
client string
clientSecret string
username string
userPassword string
mu sync.Mutex
token string
}
// NewOauth2HTTPClient creates a new Oauth2HTTPClient.
func NewOauth2HTTPClient(oauth2Addr, client, clientSecret string, opts ...Oauth2Option) *Oauth2HTTPClient {
c := &Oauth2HTTPClient{
oauth2Addr: oauth2Addr,
client: client,
clientSecret: clientSecret,
c: &http.Client{
Timeout: 5 * time.Second,
},
}
for _, o := range opts {
o.configure(c)
}
return c
}
// Oauth2Option configures the Oauth2HTTPClient.
type Oauth2Option interface {
configure(c *Oauth2HTTPClient)
}
// WithOauth2HTTPClient sets the HTTPClient for the Oauth2HTTPClient. It
// defaults to the same default as Client.
func WithOauth2HTTPClient(client HTTPClient) Oauth2Option {
return oauth2HTTPClientOptionFunc(func(c *Oauth2HTTPClient) {
c.c = client
})
}
// WithOauth2HTTPUser sets the username and password for user authentication.
func WithOauth2HTTPUser(username, password string) Oauth2Option {
return oauth2HTTPClientOptionFunc(func(c *Oauth2HTTPClient) {
c.username = username
c.userPassword = password
})
}
// oauth2HTTPClientOptionFunc enables a function to be a
// Oauth2Option.
type oauth2HTTPClientOptionFunc func(c *Oauth2HTTPClient)
// configure implements Oauth2Option.
func (f oauth2HTTPClientOptionFunc) configure(c *Oauth2HTTPClient) {
f(c)
}
// Do implements HTTPClient. It adds the Authorization header to the request
// (unless the header already exists). If the token is expired, it will reach
// out the Oauth2 server and get a new one. The given error CAN be from the
// request to the Oauth2 server.
//
// Do modifies the given Request. It is invalid to use the same Request
// instance on multiple go-routines.
func (c *Oauth2HTTPClient) Do(req *http.Request) (*http.Response, error) {
if _, ok := req.Header["Authorization"]; ok {
// Authorization Header is pre-populated, so just do the request.
return c.c.Do(req)
}
token, err := c.getToken()
if err != nil {
return nil, err
}
req.Header.Set("Authorization", token)
resp, err := c.c.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
c.token = ""
return resp, nil
}
return resp, nil
}
func (c *Oauth2HTTPClient) getToken() (string, error) {
if c.username == "" {
return c.getClientToken()
}
return c.getUserToken()
}
func (c *Oauth2HTTPClient) getClientToken() (string, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.token != "" {
return c.token, nil
}
v := make(url.Values)
v.Set("client_id", c.client)
v.Set("grant_type", "client_credentials")
req, err := http.NewRequest(
"POST",
c.oauth2Addr,
strings.NewReader(v.Encode()),
)
if err != nil {
return "", err
}
req.URL.Path = "/oauth/token"
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.URL.User = url.UserPassword(c.client, c.clientSecret)
return c.doTokenRequest(req)
}
func (c *Oauth2HTTPClient) getUserToken() (string, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.token != "" {
return c.token, nil
}
v := make(url.Values)
v.Set("client_id", c.client)
v.Set("client_secret", c.clientSecret)
v.Set("grant_type", "password")
v.Set("username", c.username)
v.Set("password", c.userPassword)
req, err := http.NewRequest(
"POST",
c.oauth2Addr,
strings.NewReader(v.Encode()),
)
if err != nil {
return "", err
}
req.URL.Path = "/oauth/token"
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return c.doTokenRequest(req)
}
func (c *Oauth2HTTPClient) doTokenRequest(req *http.Request) (string, error) {
resp, err := c.c.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("unexpected status code from Oauth2 server %d", resp.StatusCode)
}
token := struct {
TokenType string `json:"token_type"`
AccessToken string `json:"access_token"`
}{}
if err := json.NewDecoder(resp.Body).Decode(&token); err != nil {
return "", fmt.Errorf("failed to unmarshal response from Oauth2 server: %s", err)
}
c.token = fmt.Sprintf("%s %s", token.TokenType, token.AccessToken)
return c.token, nil
}