@@ -31,11 +31,12 @@ 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
@@ -241,8 +242,22 @@ type OAuth2 struct {
241242 Scopes []string `yaml:"scopes,omitempty" json:"scopes,omitempty"`
242243 TokenURL string `yaml:"token_url" json:"token_url"`
243244 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"`
246261}
247262
248263// UnmarshalYAML implements the yaml.Unmarshaler interface
@@ -408,8 +423,12 @@ func (c *HTTPClientConfig) Validate() error {
408423 if len (c .OAuth2 .TokenURL ) == 0 {
409424 return errors .New ("oauth2 token_url must be configured" )
410425 }
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" )
413432 }
414433 }
415434 if err := c .ProxyConfig .Validate (); err != nil {
@@ -662,11 +681,24 @@ func NewRoundTripperFromConfigWithContext(ctx context.Context, cfg HTTPClientCon
662681 }
663682
664683 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+ }
668699 }
669- rt = NewOAuth2RoundTripper (clientSecret , cfg .OAuth2 , rt , & opts )
700+
701+ rt = NewOAuth2RoundTripper (clientCredential , cfg .OAuth2 , rt , & opts )
670702 }
671703
672704 if cfg .HTTPHeaders != nil {
@@ -885,27 +917,34 @@ type oauth2RoundTripper struct {
885917 lastSecret string
886918
887919 // 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
892924}
893925
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 ("" )
897932 }
898933
899934 return & oauth2RoundTripper {
900935 config : config ,
901936 // 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 ,
905940 }
906941}
907942
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 ) {
909948 tlsConfig , err := NewTLSConfig (& rt .config .TLSConfig , WithSecretManager (rt .opts .secretManager ))
910949 if err != nil {
911950 return nil , nil , err
@@ -943,13 +982,30 @@ func (rt *oauth2RoundTripper) newOauth2TokenSource(req *http.Request, secret str
943982 t = NewUserAgentRoundTripper (ua , t )
944983 }
945984
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+ }
9521007 }
1008+
9531009 client = & http.Client {Transport : t }
9541010 ctx := context .WithValue (context .Background (), oauth2 .HTTPClient , client )
9551011 return client , config .TokenSource (ctx ), nil
@@ -967,8 +1023,8 @@ func (rt *oauth2RoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
9671023 rt .mtx .RUnlock ()
9681024
9691025 // 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 ())
9721028 if err != nil {
9731029 return nil , fmt .Errorf ("unable to read oauth2 client secret: %w" , err )
9741030 }
0 commit comments