Skip to content

Commit afee07e

Browse files
committed
feat: replace api key with oauth2
1 parent dedfc08 commit afee07e

4 files changed

Lines changed: 276 additions & 129 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ go get github.com/netboxlabs/diode-sdk-go
2020

2121
### Environment variables
2222

23-
* `DIODE_API_KEY` - API key for the Diode service
2423
* `DIODE_SDK_LOG_LEVEL` - Log level for the SDK (default: `INFO`)
24+
* `DIODE_CLIENT_ID` - Client ID for OAuth2 authentication
25+
* `DIODE_CLIENT_SECRET` - Client Secret for OAuth2 authentication
2526

2627
### Example
2728

diode/client.go

Lines changed: 134 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import (
44
"context"
55
"crypto/tls"
66
"crypto/x509"
7+
"encoding/json"
78
"errors"
89
"fmt"
910
"log/slog"
11+
"net/http"
1012
"net/url"
1113
"os"
1214
"regexp"
@@ -31,8 +33,11 @@ const (
3133
// SDKVersion is the version of the Diode SDK
3234
SDKVersion = "0.2.0"
3335

34-
// DiodeAPIKeyEnvVarName is the environment variable name for the Diode API key
35-
DiodeAPIKeyEnvVarName = "DIODE_API_KEY"
36+
// DiodeClientIDEnvVarName is the environment variable name for the Diode Client ID
37+
DiodeClientIDEnvVarName = "DIODE_CLIENT_ID"
38+
39+
// DiodeClientSecretEnvVarName is the environment variable name for the Diode Client Secret
40+
DiodeClientSecretEnvVarName = "DIODE_CLIENT_SECRET"
3641

3742
// DiodeSDKLogLevelEnvVarName is the environment variable name for the Diode SDK log level
3843
DiodeSDKLogLevelEnvVarName = "DIODE_SDK_LOG_LEVEL"
@@ -76,17 +81,30 @@ func parseTarget(target string) (string, string, bool, error) {
7681
return authority, path, tlsVerify, nil
7782
}
7883

79-
// getAPIKey returns the API key either from provided value or environment variable
80-
func getAPIKey(apiKey string) (string, error) {
81-
if apiKey == "" {
82-
apiKey = os.Getenv(DiodeAPIKeyEnvVarName)
84+
// getClientID returns the client ID either from provided value or environment variable
85+
func getClientID(clientID string) (string, error) {
86+
if clientID == "" {
87+
clientID = os.Getenv(DiodeClientIDEnvVarName)
88+
}
89+
90+
if clientID == "" {
91+
return "", fmt.Errorf("client_id param or %s environment variable required", DiodeClientIDEnvVarName)
92+
}
93+
94+
return clientID, nil
95+
}
96+
97+
// getClientSecret returns the client secret either from provided value or environment variable
98+
func getClientSecret(clientSecret string) (string, error) {
99+
if clientSecret == "" {
100+
clientSecret = os.Getenv(DiodeClientSecretEnvVarName)
83101
}
84102

85-
if apiKey == "" {
86-
return "", fmt.Errorf("api_key param or %s environment variable required", DiodeAPIKeyEnvVarName)
103+
if clientSecret == "" {
104+
return "", fmt.Errorf("client_secret param or %s environment variable required", DiodeClientSecretEnvVarName)
87105
}
88106

89-
return apiKey, nil
107+
return clientSecret, nil
90108
}
91109

92110
// Client is an interface that defines the methods available from Diode API
@@ -115,8 +133,11 @@ type GRPCClient struct {
115133
// Producer's application version
116134
appVersion string
117135

118-
// An API key for the Diode API
119-
apiKey string
136+
// The client ID for the API
137+
clientID string
138+
139+
// The client secret for the API
140+
clientSecret string
120141

121142
// GRPC target
122143
target string
@@ -140,13 +161,101 @@ type GRPCClient struct {
140161
// ClientOption is a functional option for the GRPCClient
141162
type ClientOption func(*GRPCClient)
142163

143-
// WithAPIKey sets the API key for the client
144-
func WithAPIKey(apiKey string) ClientOption {
164+
// WithClientID sets the client ID for the GRPCClient
165+
func WithClientID(clientID string) ClientOption {
166+
return func(c *GRPCClient) {
167+
c.clientID = clientID
168+
}
169+
}
170+
171+
// WithClientSecret sets the client secret for the GRPCClient
172+
func WithClientSecret(clientSecret string) ClientOption {
145173
return func(c *GRPCClient) {
146-
c.apiKey = apiKey
174+
c.clientSecret = clientSecret
147175
}
148176
}
149177

178+
// authenticate fetches an OAuth2 token using client credentials and updates the metadata with the token.
179+
func (g *GRPCClient) authenticate(ctx context.Context) error {
180+
authClient := newDiodeAuthentication(g.target, g.tlsVerify, g.clientID, g.clientSecret)
181+
accessToken, err := authClient.authenticate(ctx)
182+
if err != nil {
183+
return fmt.Errorf("authentication failed: %w", err)
184+
}
185+
186+
// Update metadata with the new authorization token
187+
g.metadata.Set("authorization", fmt.Sprintf("Bearer %s", accessToken))
188+
return nil
189+
}
190+
191+
// DiodeAuthentication handles OAuth2 authentication for the Diode API.
192+
type diodeAuthentication struct {
193+
target string
194+
tlsVerify bool
195+
clientID string
196+
clientSecret string
197+
}
198+
199+
// NewDiodeAuthentication creates a new instance of DiodeAuthentication.
200+
func newDiodeAuthentication(target string, tlsVerify bool, clientID, clientSecret string) *diodeAuthentication {
201+
return &diodeAuthentication{
202+
target: target,
203+
tlsVerify: tlsVerify,
204+
clientID: clientID,
205+
clientSecret: clientSecret,
206+
}
207+
}
208+
209+
// Authenticate requests an OAuth2 token using client credentials and returns it.
210+
func (d *diodeAuthentication) authenticate(ctx context.Context) (string, error) {
211+
scheme := "http"
212+
if d.tlsVerify {
213+
scheme = "https"
214+
}
215+
216+
authURL := fmt.Sprintf("%s://%s/diode/auth/token", scheme, d.target)
217+
data := url.Values{}
218+
data.Set("grant_type", "client_credentials")
219+
data.Set("client_id", d.clientID)
220+
data.Set("client_secret", d.clientSecret)
221+
222+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, authURL, strings.NewReader(data.Encode()))
223+
if err != nil {
224+
return "", fmt.Errorf("failed to create request: %w", err)
225+
}
226+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
227+
228+
client := &http.Client{}
229+
if !d.tlsVerify {
230+
client.Transport = &http.Transport{
231+
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
232+
}
233+
}
234+
235+
resp, err := client.Do(req)
236+
if err != nil {
237+
return "", fmt.Errorf("failed to send request: %w", err)
238+
}
239+
defer resp.Body.Close()
240+
241+
if resp.StatusCode != http.StatusOK {
242+
return "", fmt.Errorf("authentication failed: %s", resp.Status)
243+
}
244+
245+
var result struct {
246+
AccessToken string `json:"access_token"`
247+
}
248+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
249+
return "", fmt.Errorf("failed to parse response: %w", err)
250+
}
251+
252+
if result.AccessToken == "" {
253+
return "", errors.New("access token not found in response")
254+
}
255+
256+
return result.AccessToken, nil
257+
}
258+
150259
// NewClient creates a new diode client based on gRPC
151260
func NewClient(target string, appName string, appVersion string, opts ...ClientOption) (Client, error) {
152261
logger := newLogger()
@@ -203,19 +312,26 @@ func NewClient(target string, appName string, appVersion string, opts ...ClientO
203312
goVersion: goVersion,
204313
}
205314

206-
var apiKey string
315+
var clientID string
316+
var clientSecret string
207317

208318
for _, o := range opts {
209319
o(c)
210320
}
211321

212-
apiKey, err = getAPIKey(c.apiKey)
322+
clientID, err = getClientID(c.clientID)
213323
if err != nil {
214324
return nil, err
215325
}
326+
clientSecret, err = getClientSecret(c.clientSecret)
327+
if err != nil {
328+
return nil, err
329+
}
330+
331+
c.clientID = clientID
332+
c.clientSecret = clientSecret
216333

217-
c.apiKey = apiKey
218-
c.metadata = metadata.Pairs(authAPIKeyName, c.apiKey, "platform", platform, "go-version", goVersion)
334+
c.metadata = metadata.Pairs("platform", platform, "go-version", goVersion)
219335

220336
return c, nil
221337
}

0 commit comments

Comments
 (0)