@@ -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
141162type 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
151260func 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