@@ -31,14 +31,19 @@ import (
3131 "sync"
3232 "time"
3333
34- conntrack "github.com/mwitkow/go-conntrack"
34+ "github.com/mwitkow/go-conntrack"
3535 "golang.org/x/net/http/httpproxy"
3636 "golang.org/x/net/http2"
3737 "golang.org/x/oauth2"
3838 "golang.org/x/oauth2/clientcredentials"
39+ "golang.org/x/oauth2/jwt"
3940 "gopkg.in/yaml.v2"
4041)
4142
43+ const (
44+ grantTypeJWTBearer = "urn:ietf:params:oauth:grant-type:jwt-bearer"
45+ )
46+
4247var (
4348 // DefaultHTTPClientConfig is the default HTTP client configuration.
4449 DefaultHTTPClientConfig = HTTPClientConfig {
@@ -241,8 +246,22 @@ type OAuth2 struct {
241246 Scopes []string `yaml:"scopes,omitempty" json:"scopes,omitempty"`
242247 TokenURL string `yaml:"token_url" json:"token_url"`
243248 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"`
246265}
247266
248267// UnmarshalYAML implements the yaml.Unmarshaler interface
@@ -408,8 +427,12 @@ func (c *HTTPClientConfig) Validate() error {
408427 if len (c .OAuth2 .TokenURL ) == 0 {
409428 return errors .New ("oauth2 token_url must be configured" )
410429 }
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" )
413436 }
414437 }
415438 if err := c .ProxyConfig .Validate (); err != nil {
@@ -662,11 +685,24 @@ func NewRoundTripperFromConfigWithContext(ctx context.Context, cfg HTTPClientCon
662685 }
663686
664687 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+ }
668703 }
669- rt = NewOAuth2RoundTripper (clientSecret , cfg .OAuth2 , rt , & opts )
704+
705+ rt = NewOAuth2RoundTripper (clientCredential , cfg .OAuth2 , rt , & opts )
670706 }
671707
672708 if cfg .HTTPHeaders != nil {
@@ -885,27 +921,34 @@ type oauth2RoundTripper struct {
885921 lastSecret string
886922
887923 // 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
892928}
893929
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 ("" )
897936 }
898937
899938 return & oauth2RoundTripper {
900939 config : config ,
901940 // 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 ,
905944 }
906945}
907946
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 ) {
909952 tlsConfig , err := NewTLSConfig (& rt .config .TLSConfig , WithSecretManager (rt .opts .secretManager ))
910953 if err != nil {
911954 return nil , nil , err
@@ -943,13 +986,30 @@ func (rt *oauth2RoundTripper) newOauth2TokenSource(req *http.Request, secret str
943986 t = NewUserAgentRoundTripper (ua , t )
944987 }
945988
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+ }
9521011 }
1012+
9531013 client = & http.Client {Transport : t }
9541014 ctx := context .WithValue (context .Background (), oauth2 .HTTPClient , client )
9551015 return client , config .TokenSource (ctx ), nil
@@ -967,8 +1027,8 @@ func (rt *oauth2RoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
9671027 rt .mtx .RUnlock ()
9681028
9691029 // 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 ())
9721032 if err != nil {
9731033 return nil , fmt .Errorf ("unable to read oauth2 client secret: %w" , err )
9741034 }
0 commit comments