-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathauth.go
More file actions
128 lines (106 loc) · 3.11 KB
/
Copy pathauth.go
File metadata and controls
128 lines (106 loc) · 3.11 KB
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
package bcstrade
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/tigusigalpa/bcs-trade-go/models"
"golang.org/x/oauth2"
)
// bcsTokenSource implements oauth2.TokenSource for the BCS API.
// It uses the refresh_token grant to obtain and refresh access tokens.
// It is safe for concurrent use by multiple goroutines.
type bcsTokenSource struct {
mu sync.Mutex
refreshToken string
clientID ClientID
authURL string
httpClient *http.Client
token *oauth2.Token
}
// newTokenSource creates a new BCS token source.
func newTokenSource(ctx context.Context, cfg *Config) (*bcsTokenSource, error) {
ts := &bcsTokenSource{
refreshToken: cfg.RefreshToken,
clientID: cfg.ClientID,
authURL: cfg.BaseURLs.Auth,
httpClient: cfg.HTTPClient,
}
if ts.httpClient == nil {
ts.httpClient = &http.Client{
Timeout: cfg.Timeout,
}
}
token, err := ts.fetchToken(ctx)
if err != nil {
return nil, err
}
ts.token = token
return ts, nil
}
// Token returns a valid access token, refreshing it if necessary.
// It is safe for concurrent use by multiple goroutines.
func (ts *bcsTokenSource) Token() (*oauth2.Token, error) {
ts.mu.Lock()
defer ts.mu.Unlock()
if ts.token != nil && ts.token.Valid() {
return ts.token, nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
token, err := ts.fetchToken(ctx)
if err != nil {
return nil, err
}
ts.token = token
return token, nil
}
// fetchToken fetches a new access token using the refresh token.
func (ts *bcsTokenSource) fetchToken(ctx context.Context) (*oauth2.Token, error) {
data := url.Values{}
data.Set("client_id", string(ts.clientID))
data.Set("refresh_token", ts.refreshToken)
data.Set("grant_type", "refresh_token")
req, err := http.NewRequestWithContext(ctx, "POST", ts.authURL, strings.NewReader(data.Encode()))
if err != nil {
return nil, fmt.Errorf("failed to create token request: %w", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := ts.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch token: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read token response: %w", err)
}
if resp.StatusCode == http.StatusUnauthorized {
return nil, &AuthError{
Message: "refresh token is invalid or expired, please obtain a new one from BCS Mir Investitsiy",
}
}
if resp.StatusCode != http.StatusOK {
return nil, &APIError{
StatusCode: resp.StatusCode,
Body: string(body),
Message: fmt.Sprintf("token request failed with status %d", resp.StatusCode),
}
}
var tokenResp models.TokenResponse
if err := json.Unmarshal(body, &tokenResp); err != nil {
return nil, fmt.Errorf("failed to parse token response: %w", err)
}
token := &oauth2.Token{
AccessToken: tokenResp.AccessToken,
TokenType: tokenResp.TokenType,
RefreshToken: tokenResp.RefreshToken,
Expiry: time.Now().Add(time.Duration(tokenResp.ExpiresIn-60) * time.Second),
}
return token, nil
}