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