Skip to content

Commit f47e586

Browse files
committed
feat(http_config): support JWT token auth as alternative to client secret (RFC 7523 3.1)
Signed-off-by: Jan-Otto Kröpke <[email protected]>
1 parent 8de85c2 commit f47e586

File tree

2 files changed

+242
-41
lines changed

2 files changed

+242
-41
lines changed

config/http_config.go

Lines changed: 88 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
4247
var (
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

Comments
 (0)