@@ -27,18 +27,24 @@ import (
2727 "net/url"
2828 "os"
2929 "path/filepath"
30+ "slices"
3031 "strings"
3132 "sync"
3233 "time"
3334
34- conntrack "github.com/mwitkow/go-conntrack"
35+ "github.com/golang-jwt/jwt/v5"
36+ "github.com/mwitkow/go-conntrack"
3537 "go.yaml.in/yaml/v2"
3638 "golang.org/x/net/http/httpproxy"
3739 "golang.org/x/net/http2"
3840 "golang.org/x/oauth2"
3941 "golang.org/x/oauth2/clientcredentials"
4042)
4143
44+ const (
45+ grantTypeJWTBearer = "urn:ietf:params:oauth:grant-type:jwt-bearer"
46+ )
47+
4248var (
4349 // DefaultHTTPClientConfig is the default HTTP client configuration.
4450 DefaultHTTPClientConfig = HTTPClientConfig {
@@ -237,12 +243,38 @@ type OAuth2 struct {
237243 ClientSecretFile string `yaml:"client_secret_file" json:"client_secret_file"`
238244 // ClientSecretRef is the name of the secret within the secret manager to use as the client
239245 // secret.
240- ClientSecretRef string `yaml:"client_secret_ref" json:"client_secret_ref"`
241- Scopes []string `yaml:"scopes,omitempty" json:"scopes,omitempty"`
242- TokenURL string `yaml:"token_url" json:"token_url"`
243- EndpointParams map [string ]string `yaml:"endpoint_params,omitempty" json:"endpoint_params,omitempty"`
244- TLSConfig TLSConfig `yaml:"tls_config,omitempty"`
245- ProxyConfig `yaml:",inline"`
246+ ClientSecretRef string `yaml:"client_secret_ref" json:"client_secret_ref"`
247+ ClientCertificateKeyID string `yaml:"client_certificate_key_id" json:"client_certificate_key_id"`
248+ ClientCertificateKey Secret `yaml:"client_certificate_key" json:"client_certificate_key"`
249+ ClientCertificateKeyFile string `yaml:"client_certificate_key_file" json:"client_certificate_key_file"`
250+ // ClientCertificateKeyRef is the name of the secret within the secret manager to use as the client
251+ // secret.
252+ ClientCertificateKeyRef string `yaml:"client_certificate_key_ref" json:"client_certificate_key_ref"`
253+ // GrantType is the OAuth2 grant type to use. It can be one of
254+ // "client_credentials" or "urn:ietf:params:oauth:grant-type:jwt-bearer" (RFC 7523).
255+ // Default value is "client_credentials"
256+ GrantType string `yaml:"grant_type" json:"grant_type"`
257+ // SignatureAlgorithm is the RSA algorithm used to sign JWT token. Only used if
258+ // GrantType is set to "urn:ietf:params:oauth:grant-type:jwt-bearer".
259+ // Default value is RS256 and valid values RS256, RS384, RS512
260+ SignatureAlgorithm string `yaml:"signature_algorithm,omitempty" json:"signature_algorithm,omitempty"`
261+ // Iss is the OAuth client identifier used when communicating with
262+ // the configured OAuth provider. Default value is client_id. Only used if
263+ // GrantType is set to "urn:ietf:params:oauth:grant-type:jwt-bearer".
264+ Iss string `yaml:"iss,omitempty" json:"iss,omitempty"`
265+ // Audience optionally specifies the intended audience of the
266+ // request. If empty, the value of TokenURL is used as the
267+ // intended audience. Only used if
268+ // GrantType is set to "urn:ietf:params:oauth:grant-type:jwt-bearer".
269+ Audience string `yaml:"audience,omitempty" json:"audience,omitempty"`
270+ // Claims is a map of claims to be added to the JWT token. Only used if
271+ // GrantType is set to "urn:ietf:params:oauth:grant-type:jwt-bearer".
272+ Claims map [string ]interface {} `yaml:"claims,omitempty" json:"claims,omitempty"`
273+ Scopes []string `yaml:"scopes,omitempty" json:"scopes,omitempty"`
274+ TokenURL string `yaml:"token_url" json:"token_url"`
275+ EndpointParams map [string ]string `yaml:"endpoint_params,omitempty" json:"endpoint_params,omitempty"`
276+ TLSConfig TLSConfig `yaml:"tls_config,omitempty"`
277+ ProxyConfig `yaml:",inline"`
246278}
247279
248280// UnmarshalYAML implements the yaml.Unmarshaler interface.
@@ -408,8 +440,15 @@ func (c *HTTPClientConfig) Validate() error {
408440 if len (c .OAuth2 .TokenURL ) == 0 {
409441 return errors .New ("oauth2 token_url must be configured" )
410442 }
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" )
443+ if c .OAuth2 .GrantType == grantTypeJWTBearer {
444+ if nonZeroCount (len (c .OAuth2 .ClientCertificateKey ) > 0 , len (c .OAuth2 .ClientCertificateKeyFile ) > 0 , len (c .OAuth2 .ClientCertificateKeyRef ) > 0 ) > 1 {
445+ return errors .New ("at most one of oauth2 client_certificate_key, client_certificate_key_file & client_certificate_key_ref must be configured using grant-type=urn:ietf:params:oauth:grant-type:jwt-bearer" )
446+ }
447+ if c .OAuth2 .SignatureAlgorithm != "" && ! slices .Contains (validSignatureAlgorithm , c .OAuth2 .SignatureAlgorithm ) {
448+ return errors .New ("valid signature algorithms are RS256, RS384 and RS512" )
449+ }
450+ } else if nonZeroCount (len (c .OAuth2 .ClientSecret ) > 0 , len (c .OAuth2 .ClientSecretFile ) > 0 , len (c .OAuth2 .ClientSecretRef ) > 0 ) > 1 {
451+ return errors .New ("at most one of oauth2 client_secret, client_secret_file & client_secret_ref must be configured using grant-type=client_credentials" )
413452 }
414453 }
415454 if err := c .ProxyConfig .Validate (); err != nil {
@@ -668,11 +707,23 @@ func NewRoundTripperFromConfigWithContext(ctx context.Context, cfg HTTPClientCon
668707 }
669708
670709 if cfg .OAuth2 != nil {
671- clientSecret , err := toSecret (opts .secretManager , cfg .OAuth2 .ClientSecret , cfg .OAuth2 .ClientSecretFile , cfg .OAuth2 .ClientSecretRef )
672- if err != nil {
673- return nil , fmt .Errorf ("unable to use client secret: %w" , err )
710+ var (
711+ oauthCredential SecretReader
712+ err error
713+ )
714+
715+ if cfg .OAuth2 .GrantType == grantTypeJWTBearer {
716+ oauthCredential , err = toSecret (opts .secretManager , cfg .OAuth2 .ClientCertificateKey , cfg .OAuth2 .ClientCertificateKeyFile , cfg .OAuth2 .ClientCertificateKeyRef )
717+ if err != nil {
718+ return nil , fmt .Errorf ("unable to use client certificate: %w" , err )
719+ }
720+ } else {
721+ oauthCredential , err = toSecret (opts .secretManager , cfg .OAuth2 .ClientSecret , cfg .OAuth2 .ClientSecretFile , cfg .OAuth2 .ClientSecretRef )
722+ if err != nil {
723+ return nil , fmt .Errorf ("unable to use client secret: %w" , err )
724+ }
674725 }
675- rt = NewOAuth2RoundTripper (clientSecret , cfg .OAuth2 , rt , & opts )
726+ rt = NewOAuth2RoundTripper (oauthCredential , cfg .OAuth2 , rt , & opts )
676727 }
677728
678729 if cfg .HTTPHeaders != nil {
@@ -891,27 +942,31 @@ type oauth2RoundTripper struct {
891942 lastSecret string
892943
893944 // Required for interaction with Oauth2 server.
894- config * OAuth2
895- clientSecret SecretReader
896- opts * httpClientOptions
897- client * http.Client
945+ config * OAuth2
946+ oauthCredential SecretReader
947+ opts * httpClientOptions
948+ client * http.Client
898949}
899950
900- func NewOAuth2RoundTripper (clientSecret SecretReader , config * OAuth2 , next http.RoundTripper , opts * httpClientOptions ) http.RoundTripper {
901- if clientSecret == nil {
902- clientSecret = NewInlineSecret ("" )
951+ func NewOAuth2RoundTripper (oauthCredential SecretReader , config * OAuth2 , next http.RoundTripper , opts * httpClientOptions ) http.RoundTripper {
952+ if oauthCredential == nil {
953+ oauthCredential = NewInlineSecret ("" )
903954 }
904955
905956 return & oauth2RoundTripper {
906957 config : config ,
907958 // A correct tokenSource will be added later on.
908- lastRT : & oauth2.Transport {Base : next },
909- opts : opts ,
910- clientSecret : clientSecret ,
959+ lastRT : & oauth2.Transport {Base : next },
960+ opts : opts ,
961+ oauthCredential : oauthCredential ,
911962 }
912963}
913964
914- func (rt * oauth2RoundTripper ) newOauth2TokenSource (req * http.Request , secret string ) (client * http.Client , source oauth2.TokenSource , err error ) {
965+ type oauth2TokenSourceConfig interface {
966+ TokenSource (ctx context.Context ) oauth2.TokenSource
967+ }
968+
969+ func (rt * oauth2RoundTripper ) newOauth2TokenSource (req * http.Request , clientCredential string ) (client * http.Client , source oauth2.TokenSource , err error ) {
915970 tlsConfig , err := NewTLSConfig (& rt .config .TLSConfig , WithSecretManager (rt .opts .secretManager ))
916971 if err != nil {
917972 return nil , nil , err
@@ -949,12 +1004,49 @@ func (rt *oauth2RoundTripper) newOauth2TokenSource(req *http.Request, secret str
9491004 t = NewUserAgentRoundTripper (ua , t )
9501005 }
9511006
952- config := & clientcredentials.Config {
953- ClientID : rt .config .ClientID ,
954- ClientSecret : secret ,
955- Scopes : rt .config .Scopes ,
956- TokenURL : rt .config .TokenURL ,
957- EndpointParams : mapToValues (rt .config .EndpointParams ),
1007+ var config oauth2TokenSourceConfig
1008+
1009+ if rt .config .GrantType == grantTypeJWTBearer {
1010+ // RFC 7523 3.1 - JWT authorization grants
1011+ // RFC 7523 3.2 - Client Authentication Processing is not implement upstream yet,
1012+ // see https://github.com/golang/oauth2/pull/745
1013+
1014+ var sig * jwt.SigningMethodRSA
1015+ switch rt .config .SignatureAlgorithm {
1016+ case jwt .SigningMethodRS256 .Name :
1017+ sig = jwt .SigningMethodRS256
1018+ case jwt .SigningMethodRS384 .Name :
1019+ sig = jwt .SigningMethodRS384
1020+ case jwt .SigningMethodRS512 .Name :
1021+ sig = jwt .SigningMethodRS512
1022+ default :
1023+ sig = jwt .SigningMethodRS256
1024+ }
1025+
1026+ iss := rt .config .Iss
1027+ if iss == "" {
1028+ iss = rt .config .ClientID
1029+ }
1030+ config = & JwtGrantTypeConfig {
1031+ PrivateKey : []byte (clientCredential ),
1032+ PrivateKeyID : rt .config .ClientCertificateKeyID ,
1033+ Scopes : rt .config .Scopes ,
1034+ TokenURL : rt .config .TokenURL ,
1035+ SigningAlgorithm : sig ,
1036+ Iss : iss ,
1037+ Subject : rt .config .ClientID ,
1038+ Audience : rt .config .Audience ,
1039+ PrivateClaims : rt .config .Claims ,
1040+ EndpointParams : mapToValues (rt .config .EndpointParams ),
1041+ }
1042+ } else {
1043+ config = & clientcredentials.Config {
1044+ ClientID : rt .config .ClientID ,
1045+ ClientSecret : clientCredential ,
1046+ Scopes : rt .config .Scopes ,
1047+ TokenURL : rt .config .TokenURL ,
1048+ EndpointParams : mapToValues (rt .config .EndpointParams ),
1049+ }
9581050 }
9591051 client = & http.Client {Transport : t }
9601052 ctx := context .WithValue (context .Background (), oauth2 .HTTPClient , client )
@@ -973,8 +1065,8 @@ func (rt *oauth2RoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
9731065 rt .mtx .RUnlock ()
9741066
9751067 // Fetch the secret if it's our first run or always if the secret can change.
976- if ! rt .clientSecret .Immutable () || needsInit {
977- newSecret , err := rt .clientSecret .Fetch (req .Context ())
1068+ if ! rt .oauthCredential .Immutable () || needsInit {
1069+ newSecret , err := rt .oauthCredential .Fetch (req .Context ())
9781070 if err != nil {
9791071 return nil , fmt .Errorf ("unable to read oauth2 client secret: %w" , err )
9801072 }
0 commit comments