-
Notifications
You must be signed in to change notification settings - Fork 6
/
auth.go
304 lines (263 loc) · 10.1 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
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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
package chatgpt
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"time"
)
type Auth struct {
// email represents the user's email address
email string
// password represents the user's password
password string
// authState is used to store the state of the user's authentication status
authState string
// apiKey stores the API key value for authentication
apiKey string
// accessToken stores the access token value generated after successful authentication
accessToken string
// expires stores the time when the access token will expire
expires time.Time
// enableCache is used to enable or disable caching of access tokens
enableCache bool
// clientStarted keeps track of whether or not the client has been started
clientStarted bool
// sessionName is used to store the name of the session
sessionName string
}
// GetAccessToken generates and retrieves the OpenAI API access token by performing a series of authentication steps.
func (a *Auth) GetAccessToken() (string, error) {
if a.enableCache {
a.loadCachedAccessToken()
}
// check if the access token is already present and not expired
if a.accessToken != "" && a.expires.After(time.Now()) {
return a.accessToken, nil
}
// validate if email and password are set for authentication
if a.email == "" || a.password == "" {
return "", fmt.Errorf("email and password must be set to authenticate with OpenAI")
}
// get the callback URL after step one of authentication
callback_url, err := a.stepOne()
if err != nil {
return "", err
}
// get the URL for step two of authentication using the obtained callback URL along with email and password
code_url, err := a.stepTwo(callback_url, a.email, a.password)
if err != nil {
return "", err
}
// complete the final step of authentication and fetch the response containing the access token and its expiry time
resp, err := a.stepThree(code_url)
if err != nil {
return "", err
}
// store the generated access token and its expiry in the Auth struct for future use
a.accessToken = resp.AccessToken
a.expires = resp.Expires
if a.enableCache {
a.cacheAccessToken() // cache the access token in a separate goroutine
}
return resp.AccessToken, nil
}
// ExpiresIn returns the remaining duration before the access token expires.
func (a *Auth) ExpiresIn() time.Duration {
// calculate the remaining time until access token expires using current time and expiry time stored in the Auth struct
return time.Until(a.expires)
}
type authCache struct {
AccessToken string `json:"access_token,omitempty"`
Expires time.Time `json:"expires,omitempty"`
}
func (a *Auth) cacheAccessToken() error {
var previousData map[string]authCache
if _, err := os.Stat("gpt-cache.json"); err == nil {
if file, err := os.Open("gpt-cache.json"); err == nil {
defer file.Close()
json.NewDecoder(file).Decode(&previousData)
}
}
if previousData == nil {
previousData = make(map[string]authCache)
}
previousData[a.sessionName] = authCache{
AccessToken: a.accessToken,
Expires: a.expires,
}
file, err := os.Create("gpt-cache.json")
if err != nil {
return err
}
defer file.Close()
return json.NewEncoder(file).Encode(previousData)
}
func (a *Auth) loadCachedAccessToken() {
if _, err := os.Stat("gpt-cache.json"); err != nil {
return // no cache file
}
file, err := os.Open("gpt-cache.json")
if err != nil {
return // error opening file
}
defer file.Close()
var data map[string]authCache
if err := json.NewDecoder(file).Decode(&data); err != nil {
return // error decoding file
}
if data == nil {
return // no data in file
}
if cache, ok := data[a.sessionName]; ok {
a.accessToken = cache.AccessToken
a.expires = cache.Expires
}
}
// copyCookies copies cookies from the source slice of http.Cookies to the destination http.Request.
func (a *Auth) copyCookies(from []*http.Cookie, to *http.Request) {
// iterate over each cookie in the source slice
for _, cookie := range from {
// add the current cookie to the destination request
to.AddCookie(cookie)
}
}
// This function performs StepOne for authentication using the Auth struct provided
func (a *Auth) stepOne() (string, error) {
// Send a GET request to the authentication endpoint given and retrieve the response
resp, err := http.Get("https://chat-api.ztorr.me/auth/endpoint")
if err != nil {
return "", err
}
defer resp.Body.Close()
// Check if the status of the response is ok, return an error message if not
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("bad status: %s", resp.Status)
}
// Decode the response body into a result variable that contains 'state' and 'url'
var result struct {
State string `json:"state"`
Url string `json:"url"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", err
}
// Set the Authentication state to the 'state' received in the response
a.authState = result.State
// Return the 'url' received in the response
return result.Url, nil
}
// StepTwo performs authentication using the given url, email, and password.
// It follows redirects, sets appropriate headers and cookies, and returns the final redirect URL,
// or an error if any occurred during the process.
func (a *Auth) stepTwo(auth_url, _email, _password string) (string, error) {
// create an http client with required cookie settings and redirect policy
httpx := http.Client{
Jar: http.DefaultClient.Jar,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
// prepare GET request for the specified authentication URL
req, _ := http.NewRequest("GET", auth_url, nil)
resp, err := httpx.Do(req)
_ref_cookies := resp.Cookies()
_url_prefix := "https://auth0.openai.com"
if err != nil {
return "", err
}
defer resp.Body.Close()
// check if server responded with a redirect status
if resp.StatusCode != 302 {
return "", fmt.Errorf("bad status for url: %s", auth_url)
}
// extract next URL from the response header and its associated state value
next_url := _url_prefix + resp.Header.Get("Location")
current_state := strings.Split(strings.Split(next_url, "state=")[1], "&")[0]
// prepare form data for POST request containing username/email as well as current state value obtained from previous step
form_data := `state=` + current_state + `&username=` + url.QueryEscape(_email) + `&js-available=true&webauthn-available=true&is-brave=false&webauthn-platform-available=false&action=default`
// prepare a POST request with the extracted form data and headers
req, _ = http.NewRequest("POST", next_url, strings.NewReader(form_data))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
// copy cookies from the previous response to the current request
a.copyCookies(_ref_cookies, req)
resp, err = httpx.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
// check for correct status code, and handle incorrect email/password combination error if received
if resp.StatusCode != 302 {
if resp.StatusCode == 400 {
return "", &ChatError{"email and password combination is incorrect or you have not verified your email address yet", 400}
}
return "", &ChatError{"bad status for url: " + next_url, resp.StatusCode}
}
// extract next URL from the response header and update the form data with provided password
next_url = _url_prefix + resp.Header.Get("Location")
form_data = `state=` + current_state + `&username=` + url.QueryEscape(_email) + `&password=` + url.QueryEscape(_password) + `&action=default`
// prepare another POST request with the updated form data and headers
req, _ = http.NewRequest("POST", next_url, strings.NewReader(form_data))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
// copy cookies from the previous response to the current request
a.copyCookies(_ref_cookies, req)
resp, err = httpx.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
// check for correct status code after performing final redirect
if resp.StatusCode != 302 {
if resp.StatusCode == 400 {
return "", &ChatError{"email and password combination is incorrect or you have not verified your email address yet", 400}
}
return "", &ChatError{"bad status for url: " + next_url, resp.StatusCode}
}
// extract the final redirect URL and return it
next_url = _url_prefix + resp.Header.Get("Location")
req, _ = http.NewRequest("GET", next_url, nil)
a.copyCookies(_ref_cookies, req)
resp, err = httpx.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
// check for correct status code after visiting the final URL
if resp.StatusCode != 302 {
return "", &ChatError{"bad status for url: " + next_url, resp.StatusCode}
}
return resp.Header.Get("Location"), nil
}
// AuthResp is a struct that represents the response returned by an authentication request.
type authResp struct {
// AccessToken contains the access token string returned by the auth server.
AccessToken string `json:"accessToken"`
// Expires is a time.Time object representing the time when the access token will expire.
Expires time.Time `json:"expires"`
// Detail provides additional information about the authentication response, if any.
Detail string `json:"detail"`
}
// StepThree completes the third step of the authentication process by exchanging the authorization
// code for an access token, using the provided callback URL.
func (a *Auth) stepThree(code_url string) (*authResp, error) {
// Compose the data payload for the request.
var data = strings.NewReader(`state=` + a.authState + `&callbackUrl=` + url.QueryEscape(code_url))
// Create a new HTTP POST request object with the appropriate endpoint URL and data payload.
req, _ := http.NewRequest("POST", "https://chat-api.ztorr.me/auth/token", data)
req.Header.Set("content-type", "application/x-www-form-urlencoded")
// Send the request and obtain the response.
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// Parse the response body as an AuthResp object.
var result authResp
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
// Return the resulting AuthResp object and any error that occurred during the request/response cycle.
return &result, nil
}