This repository has been archived by the owner on Jul 21, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
auth.go
251 lines (206 loc) · 6.73 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
package connect
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"runtime"
"github.com/nats-io/jwt/v2"
"github.com/nats-io/nkeys"
overmind "github.com/overmindtech/api-client"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel/codes"
"golang.org/x/oauth2/clientcredentials"
)
const UserAgentVersion = "0.1"
// TokenClient Represents something that is capable of getting NATS JWT tokens
// for a given set of NKeys
type TokenClient interface {
// Returns a NATS token that can be used to connect
GetJWT() (string, error)
// Uses the NKeys associated with the token to sign some binary data
Sign([]byte) ([]byte, error)
}
// BasicTokenClient stores a static token and returns it when called, ignoring
// any provided NKeys or context since it already has the token and doesn't need
// to make any requests
type BasicTokenClient struct {
staticToken string
staticKeys nkeys.KeyPair
}
// NewBasicTokenClient Creates a new basic token client that simply returns a static token
func NewBasicTokenClient(token string, keys nkeys.KeyPair) *BasicTokenClient {
return &BasicTokenClient{
staticToken: token,
staticKeys: keys,
}
}
func (b *BasicTokenClient) GetJWT() (string, error) {
return b.staticToken, nil
}
func (b *BasicTokenClient) Sign(in []byte) ([]byte, error) {
return b.staticKeys.Sign(in)
}
// OAuthTokenClient Gets a NATS token by first authenticating to OAuth using the
// Client Credentials Flow, then using that token to retrieve a NATS token.
// Nkeys are also autogenerated
type OAuthTokenClient struct {
oAuthClient *clientcredentials.Config
natsConfig *overmind.Configuration
natsClient *overmind.APIClient
account string
jwt string
keys nkeys.KeyPair
}
// ClientCredentialsConfig Authenticates to Overmind using the Client
// Credentials flow
// https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-credentials-flow
type ClientCredentialsConfig struct {
// The ClientID of the application that we'll be authenticating as
ClientID string
// ClientSecret that cirresponds to the ClientID
ClientSecret string
// If Account is specified, then the ClientID must have `admin:write`
// permissions in order to be able to request a token for any account. If
// this is omitted then the account will be determined based on the account
// included in the resulting token. This will be stored in the
// `https://api.overmind.tech/account-name` claim
Account string
}
// NewOAuthTokenClient Generates a token client that authenticates to OAuth
// using the client credentials flow, then uses that auth to get a NATS token.
// `clientID` and `clientSecret` are used to authenticate using the client
// credentials flow with an API at `oAuthTokenURL`. `overmindAPIURL` is the root
// URL of the NATS token exchange API that will be used e.g.
// https://api.server.test/v1
//
// Tokens will be for the org specified under `org`. Note that the client must
// have admin rights for this
func NewOAuthTokenClient(oAuthTokenURL string, overmindAPIURL string, flowConfig ClientCredentialsConfig) *OAuthTokenClient {
conf := &clientcredentials.Config{
ClientID: flowConfig.ClientID,
ClientSecret: flowConfig.ClientSecret,
TokenURL: oAuthTokenURL,
EndpointParams: url.Values{
"audience": []string{"https://api.overmind.tech"},
},
}
// Get an authenticated client that we can then make more HTTP calls with
authenticatedClient := conf.Client(context.Background())
// inject otelhttp propagation
authenticatedClient.Transport = otelhttp.NewTransport(authenticatedClient.Transport)
// Configure the token exchange client to use the newly authenticated HTTP
// client among other things
tokenExchangeConf := &overmind.Configuration{
DefaultHeader: make(map[string]string),
UserAgent: fmt.Sprintf("Overmind/%v (%v/%v)", UserAgentVersion, runtime.GOOS, runtime.GOARCH),
Debug: false,
Servers: overmind.ServerConfigurations{
{
URL: overmindAPIURL,
Description: "Overmind API",
},
},
OperationServers: map[string]overmind.ServerConfigurations{},
HTTPClient: authenticatedClient,
}
nClient := overmind.NewAPIClient(tokenExchangeConf)
return &OAuthTokenClient{
oAuthClient: conf,
natsConfig: tokenExchangeConf,
natsClient: nClient,
account: flowConfig.Account,
}
}
// generateKeys Generates a new set of keys for the client
func (o *OAuthTokenClient) generateKeys() error {
var err error
o.keys, err = nkeys.CreateUser()
return err
}
// generateJWT Gets a new JWT from the auth API
func (o *OAuthTokenClient) generateJWT(ctx context.Context) error {
// If we don't yet have keys generate them
if o.keys == nil {
err := o.generateKeys()
if err != nil {
return err
}
}
var err error
var pubKey string
var hostname string
var response *http.Response
pubKey, err = o.keys.PublicKey()
if err != nil {
return err
}
hostname, err = os.Hostname()
if err != nil {
return err
}
// Create the request for a NATS token
if o.account == "" {
// Use the regular API and let it determine what our org should be
o.jwt, response, err = o.natsClient.CoreApi.CreateToken(ctx).TokenRequestData(overmind.TokenRequestData{
UserPubKey: pubKey,
UserName: hostname,
}).Execute()
} else {
// Explicitly request an org
o.jwt, response, err = o.natsClient.AdminApi.AdminCreateToken(ctx, o.account).TokenRequestData(overmind.TokenRequestData{
UserPubKey: pubKey,
UserName: hostname,
}).Execute()
}
if err != nil {
errString := fmt.Sprintf("getting NATS token failed: %v", err.Error())
if response != nil && response.Request != nil && response.Request.URL != nil {
errString = errString + fmt.Sprintf(". Request URL: %v", response.Request.URL.String())
}
return errors.New(errString)
}
return nil
}
func (o *OAuthTokenClient) GetJWT() (string, error) {
ctx, span := tracer.Start(context.Background(), "connect.GetJWT")
defer span.End()
// If we don't yet have a JWT, generate one
if o.jwt == "" {
err := o.generateJWT(ctx)
if err != nil {
span.SetStatus(codes.Error, err.Error())
return "", err
}
}
claims, err := jwt.DecodeUserClaims(o.jwt)
if err != nil {
span.SetStatus(codes.Error, err.Error())
return o.jwt, err
}
// Validate to make sure the JWT is valid. If it isn't we'll generate a new
// one
var vr jwt.ValidationResults
claims.Validate(&vr)
if vr.IsBlocking(true) {
// Regenerate the token
err := o.generateJWT(ctx)
if err != nil {
span.SetStatus(codes.Error, err.Error())
return "", err
}
}
span.SetStatus(codes.Ok, "Completed")
return o.jwt, nil
}
func (o *OAuthTokenClient) Sign(in []byte) ([]byte, error) {
if o.keys == nil {
err := o.generateKeys()
if err != nil {
return []byte{}, err
}
}
return o.keys.Sign(in)
}