forked from openfaas/go-sdk
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathclient_credentials_auth.go
148 lines (122 loc) · 3.53 KB
/
client_credentials_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
package sdk
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"sync"
"time"
)
type ClientCredentialsAuth struct {
tokenSource TokenSource
}
func NewClientCredentialsAuth(ts TokenSource) *ClientCredentialsAuth {
return &ClientCredentialsAuth{
tokenSource: ts,
}
}
func (cca *ClientCredentialsAuth) Set(req *http.Request) error {
token, err := cca.tokenSource.Token()
if err != nil {
return err
}
req.Header.Add("Authorization", "Bearer "+token)
return nil
}
// ClientCredentialsTokenSource can be used to obtain
// an access token using the client credentials grant type.
// Tested with Keycloak's token endpoint, additional changes may
// be required for additional OIDC token endpoints.
type ClientCredentialsTokenSource struct {
clientID string
clientSecret string
tokenURL string
scope string
grantType string
audience string
token *ClientCredentialsToken
lock sync.RWMutex
}
func NewClientCredentialsTokenSource(clientID, clientSecret, tokenURL, scope, grantType, audience string) TokenSource {
return &ClientCredentialsTokenSource{
clientID: clientID,
clientSecret: clientSecret,
tokenURL: tokenURL,
scope: scope,
grantType: grantType,
audience: audience,
}
}
func (ts *ClientCredentialsTokenSource) Token() (string, error) {
ts.lock.RLock()
expired := ts.token == nil || ts.token.Expired()
if expired {
ts.lock.RUnlock()
token, err := obtainClientCredentialsToken(ts.clientID, ts.clientSecret, ts.tokenURL, ts.scope, ts.grantType, ts.audience)
if err != nil {
return "", err
}
ts.lock.Lock()
ts.token = token
ts.lock.Unlock()
return token.AccessToken, nil
}
ts.lock.RUnlock()
return ts.token.AccessToken, nil
}
func obtainClientCredentialsToken(clientID, clientSecret, tokenURL, scope, grantType, audience string) (*ClientCredentialsToken, error) {
reqBody := url.Values{}
reqBody.Set("client_id", clientID)
reqBody.Set("client_secret", clientSecret)
reqBody.Set("grant_type", grantType)
reqBody.Set("scope", scope)
if len(audience) > 0 {
reqBody.Set("audience", audience)
}
buffer := []byte(reqBody.Encode())
req, err := http.NewRequest(http.MethodPost, tokenURL, bytes.NewBuffer(buffer))
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
var body []byte
if res.Body != nil {
defer res.Body.Close()
body, _ = io.ReadAll(res.Body)
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code %d, body: %s", res.StatusCode, string(body))
}
token := &ClientCredentialsToken{}
if err := json.Unmarshal(body, token); err != nil {
return nil, fmt.Errorf("unable to unmarshal token: %s", err)
}
token.ObtainedAt = time.Now()
return token, nil
}
// ClientCredentialsToken represents an access_token
// obtained through the client credentials grant type.
// This token is not associated with a human user.
type ClientCredentialsToken struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
ObtainedAt time.Time
}
// Expired returns true if the token is expired
// or if the expiry time is not known.
// The token will always expire 10s early to avoid
// clock skew.
func (t *ClientCredentialsToken) Expired() bool {
if t.ExpiresIn == 0 {
return false
}
expiry := t.ObtainedAt.Add(time.Duration(t.ExpiresIn) * time.Second).Add(-expiryDelta)
return expiry.Before(time.Now())
}