@@ -31,11 +31,12 @@ import (
31
31
"sync"
32
32
"time"
33
33
34
- conntrack "github.com/mwitkow/go-conntrack"
34
+ "github.com/mwitkow/go-conntrack"
35
35
"golang.org/x/net/http/httpproxy"
36
36
"golang.org/x/net/http2"
37
37
"golang.org/x/oauth2"
38
38
"golang.org/x/oauth2/clientcredentials"
39
+ "golang.org/x/oauth2/jwt"
39
40
"gopkg.in/yaml.v2"
40
41
)
41
42
@@ -241,8 +242,22 @@ type OAuth2 struct {
241
242
Scopes []string `yaml:"scopes,omitempty" json:"scopes,omitempty"`
242
243
TokenURL string `yaml:"token_url" json:"token_url"`
243
244
EndpointParams map [string ]string `yaml:"endpoint_params,omitempty" json:"endpoint_params,omitempty"`
244
- TLSConfig TLSConfig `yaml:"tls_config,omitempty"`
245
- ProxyConfig `yaml:",inline"`
245
+
246
+ ClientCertificateKeyID string `yaml:"client_certificate_key_id" json:"client_certificate_key_id"`
247
+ ClientCertificateKey Secret `yaml:"client_certificate_key" json:"client_certificate_key"`
248
+ ClientCertificateKeyFile string `yaml:"client_certificate_key_file" json:"client_certificate_key_file"`
249
+ // ClientCertificateKeyRef is the name of the secret within the secret manager to use as the client
250
+ // secret.
251
+ ClientCertificateKeyRef string `yaml:"client_certificate_key_ref" json:"client_certificate_key_ref"`
252
+ // GrantType is the OAuth2 grant type to use. It can be one of
253
+ // "client_credentials" or "urn:ietf:params:oauth:grant-type:jwt-bearer" (RFC 7523).
254
+ GrantType string `yaml:"grant_type" json:"grant_type"`
255
+ // Claims is a map of claims to be added to the JWT token. Only used if
256
+ // GrantType is set to "urn:ietf:params:oauth:grant-type:jwt-bearer".
257
+ Claims map [string ]interface {} `yaml:"claims,omitempty" json:"claims,omitempty"`
258
+
259
+ TLSConfig TLSConfig `yaml:"tls_config,omitempty"`
260
+ ProxyConfig `yaml:",inline"`
246
261
}
247
262
248
263
// UnmarshalYAML implements the yaml.Unmarshaler interface
@@ -408,8 +423,12 @@ func (c *HTTPClientConfig) Validate() error {
408
423
if len (c .OAuth2 .TokenURL ) == 0 {
409
424
return errors .New ("oauth2 token_url must be configured" )
410
425
}
411
- if nonZeroCount (len (c .OAuth2 .ClientSecret ) > 0 , len (c .OAuth2 .ClientSecretFile ) > 0 , len (c .OAuth2 .ClientSecretRef ) > 0 ) > 1 {
412
- return errors .New ("at most one of oauth2 client_secret, client_secret_file & client_secret_ref must be configured" )
426
+ if nonZeroCount (
427
+ len (c .OAuth2 .ClientSecret ) > 0 , len (c .OAuth2 .ClientSecretFile ) > 0 , len (c .OAuth2 .ClientSecretRef ) > 0 ,
428
+ len (c .OAuth2 .ClientCertificateKey ) > 0 , len (c .OAuth2 .ClientCertificateKeyFile ) > 0 , len (c .OAuth2 .ClientCertificateKeyRef ) > 0 ,
429
+ ) > 1 {
430
+ return errors .New ("at most one of oauth2 client_secret, client_secret_file, client_secret_ref, " +
431
+ "client_certificate_key, client_certificate_key_file, client_certificate_key_ref must be configured" )
413
432
}
414
433
}
415
434
if err := c .ProxyConfig .Validate (); err != nil {
@@ -662,11 +681,24 @@ func NewRoundTripperFromConfigWithContext(ctx context.Context, cfg HTTPClientCon
662
681
}
663
682
664
683
if cfg .OAuth2 != nil {
665
- clientSecret , err := toSecret (opts .secretManager , cfg .OAuth2 .ClientSecret , cfg .OAuth2 .ClientSecretFile , cfg .OAuth2 .ClientSecretRef )
666
- if err != nil {
667
- return nil , fmt .Errorf ("unable to use client secret: %w" , err )
684
+ var (
685
+ clientCredential SecretReader
686
+ err error
687
+ )
688
+
689
+ if cfg .OAuth2 .GrantType == "urn:ietf:params:oauth:grant-type:jwt-bearer" {
690
+ clientCredential , err = toSecret (opts .secretManager , cfg .OAuth2 .ClientCertificateKey , cfg .OAuth2 .ClientCertificateKeyFile , cfg .OAuth2 .ClientCertificateKeyRef )
691
+ if err != nil {
692
+ return nil , fmt .Errorf ("unable to use client certificate: %w" , err )
693
+ }
694
+ } else {
695
+ clientCredential , err = toSecret (opts .secretManager , cfg .OAuth2 .ClientSecret , cfg .OAuth2 .ClientSecretFile , cfg .OAuth2 .ClientSecretRef )
696
+ if err != nil {
697
+ return nil , fmt .Errorf ("unable to use client secret: %w" , err )
698
+ }
668
699
}
669
- rt = NewOAuth2RoundTripper (clientSecret , cfg .OAuth2 , rt , & opts )
700
+
701
+ rt = NewOAuth2RoundTripper (clientCredential , cfg .OAuth2 , rt , & opts )
670
702
}
671
703
672
704
if cfg .HTTPHeaders != nil {
@@ -885,27 +917,34 @@ type oauth2RoundTripper struct {
885
917
lastSecret string
886
918
887
919
// Required for interaction with Oauth2 server.
888
- config * OAuth2
889
- clientSecret SecretReader
890
- opts * httpClientOptions
891
- client * http.Client
920
+ config * OAuth2
921
+ clientCredential SecretReader // SecretReader for client secret or client certificate key.
922
+ opts * httpClientOptions
923
+ client * http.Client
892
924
}
893
925
894
- func NewOAuth2RoundTripper (clientSecret SecretReader , config * OAuth2 , next http.RoundTripper , opts * httpClientOptions ) http.RoundTripper {
895
- if clientSecret == nil {
896
- clientSecret = NewInlineSecret ("" )
926
+ // NewOAuth2RoundTripper returns a http.RoundTripper
927
+ // that handles the OAuth2 authentication.
928
+ // It uses the provided clientCredential to fetch the client secret or client certificate key.
929
+ func NewOAuth2RoundTripper (clientCredential SecretReader , config * OAuth2 , next http.RoundTripper , opts * httpClientOptions ) http.RoundTripper {
930
+ if clientCredential == nil {
931
+ clientCredential = NewInlineSecret ("" )
897
932
}
898
933
899
934
return & oauth2RoundTripper {
900
935
config : config ,
901
936
// A correct tokenSource will be added later on.
902
- lastRT : & oauth2.Transport {Base : next },
903
- opts : opts ,
904
- clientSecret : clientSecret ,
937
+ lastRT : & oauth2.Transport {Base : next },
938
+ opts : opts ,
939
+ clientCredential : clientCredential ,
905
940
}
906
941
}
907
942
908
- func (rt * oauth2RoundTripper ) newOauth2TokenSource (req * http.Request , secret string ) (client * http.Client , source oauth2.TokenSource , err error ) {
943
+ type oauth2TokenSourceConfig interface {
944
+ TokenSource (ctx context.Context ) oauth2.TokenSource
945
+ }
946
+
947
+ func (rt * oauth2RoundTripper ) newOauth2TokenSource (req * http.Request , clientCredential string ) (client * http.Client , source oauth2.TokenSource , err error ) {
909
948
tlsConfig , err := NewTLSConfig (& rt .config .TLSConfig , WithSecretManager (rt .opts .secretManager ))
910
949
if err != nil {
911
950
return nil , nil , err
@@ -943,13 +982,30 @@ func (rt *oauth2RoundTripper) newOauth2TokenSource(req *http.Request, secret str
943
982
t = NewUserAgentRoundTripper (ua , t )
944
983
}
945
984
946
- config := & clientcredentials.Config {
947
- ClientID : rt .config .ClientID ,
948
- ClientSecret : secret ,
949
- Scopes : rt .config .Scopes ,
950
- TokenURL : rt .config .TokenURL ,
951
- EndpointParams : mapToValues (rt .config .EndpointParams ),
985
+ var config oauth2TokenSourceConfig
986
+
987
+ if rt .config .GrantType == "urn:ietf:params:oauth:grant-type:jwt-bearer" {
988
+ // RFC 7523 3.1 - JWT authorization grants
989
+ // RFC 7523 3.2 - Client Authentication Processing is not implement upstream yet,
990
+ // see https://github.com/golang/oauth2/pull/745
991
+
992
+ config = & jwt.Config {
993
+ PrivateKey : []byte (clientCredential ),
994
+ PrivateKeyID : rt .config .ClientCertificateKeyID ,
995
+ Scopes : rt .config .Scopes ,
996
+ TokenURL : rt .config .TokenURL ,
997
+ PrivateClaims : rt .config .Claims ,
998
+ }
999
+ } else {
1000
+ config = & clientcredentials.Config {
1001
+ ClientID : rt .config .ClientID ,
1002
+ ClientSecret : clientCredential ,
1003
+ Scopes : rt .config .Scopes ,
1004
+ TokenURL : rt .config .TokenURL ,
1005
+ EndpointParams : mapToValues (rt .config .EndpointParams ),
1006
+ }
952
1007
}
1008
+
953
1009
client = & http.Client {Transport : t }
954
1010
ctx := context .WithValue (context .Background (), oauth2 .HTTPClient , client )
955
1011
return client , config .TokenSource (ctx ), nil
@@ -967,8 +1023,8 @@ func (rt *oauth2RoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
967
1023
rt .mtx .RUnlock ()
968
1024
969
1025
// Fetch the secret if it's our first run or always if the secret can change.
970
- if ! rt .clientSecret .Immutable () || needsInit {
971
- newSecret , err := rt .clientSecret .Fetch (req .Context ())
1026
+ if ! rt .clientCredential .Immutable () || needsInit {
1027
+ newSecret , err := rt .clientCredential .Fetch (req .Context ())
972
1028
if err != nil {
973
1029
return nil , fmt .Errorf ("unable to read oauth2 client secret: %w" , err )
974
1030
}
0 commit comments