Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added OAuth2 options in http client #615

Merged
merged 7 commits into from
Feb 27, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/httpclient/http_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ func New(opts ...Options) (*http.Client, error) {
clientOpts.ConfigureClient(clientOpts, c)
}

if clientOpts.AuthenticationMethod == AuthenticationMethodOAuth2 {
return getHTTPClientOAuth2(c, clientOpts)
}

return c, nil
}

Expand Down
89 changes: 89 additions & 0 deletions backend/httpclient/httpclient_oauth2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package httpclient

import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"strings"

"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
"golang.org/x/oauth2/jwt"
)

func getHTTPClientOAuth2(client *http.Client, clientOptions Options) (*http.Client, error) {
client, options, err := normalizeOAuth2Options(client, clientOptions.OAuth2Options)
if err != nil {
return client, err
}
switch options.OAuth2Type {
case OAuth2TypeClientCredentials:
return getHTTPClientOAuth2ClientCredentials(client, options)
case OAuth2TypeJWT:
return getHTTPClientOAuth2JWT(client, options)
}
return client, fmt.Errorf("invalid/empty oauth2 type (%s)", options.OAuth2Type)
}

func getHTTPClientOAuth2ClientCredentials(client *http.Client, options *OAuth2Options) (*http.Client, error) {
client, options, err := normalizeOAuth2Options(client, options)
if err != nil {
return client, err
}
oauthConfig := clientcredentials.Config{
ClientID: options.ClientID,
ClientSecret: options.ClientSecret,
TokenURL: options.TokenURL,
Scopes: normalizeOAuth2Scopes(*options),
EndpointParams: normalizeOAuth2EndpointParams(*options),
}
return oauthConfig.Client(context.WithValue(context.Background(), oauth2.HTTPClient, client)), nil
}

func getHTTPClientOAuth2JWT(client *http.Client, options *OAuth2Options) (*http.Client, error) {
client, options, err := normalizeOAuth2Options(client, options)
if err != nil {
return client, err
}
jwtConfig := jwt.Config{
Email: options.Email,
TokenURL: options.TokenURL,
PrivateKey: options.PrivateKey,
PrivateKeyID: options.PrivateKeyID,
Subject: options.Subject,
Scopes: normalizeOAuth2Scopes(*options),
}
return jwtConfig.Client(context.WithValue(context.Background(), oauth2.HTTPClient, client)), nil
}

func normalizeOAuth2Options(client *http.Client, options *OAuth2Options) (*http.Client, *OAuth2Options, error) {
if options == nil {
return client, options, errors.New("invalid/empty options for oauth2 client")
}
if client == nil {
client, _ = New(Options{})
}
return client, options, nil
}

func normalizeOAuth2Scopes(options OAuth2Options) []string {
scopes := []string{}
for _, scope := range options.Scopes {
if scope != "" {
scopes = append(scopes, strings.TrimSpace(scope))
}
}
return scopes
}

func normalizeOAuth2EndpointParams(options OAuth2Options) url.Values {
endpointParams := url.Values{}
for k, v := range options.EndpointParams {
if k != "" && v != "" {
endpointParams.Set(strings.TrimSpace(k), strings.TrimSpace(v))
}
}
return endpointParams
}
147 changes: 147 additions & 0 deletions backend/httpclient/httpclient_oauth2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package httpclient_test

import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestHTTPClientOAuth2Invalid(t *testing.T) {
server := testGetOAuthServer(t)
defer server.Close()
hc, err := httpclient.New(httpclient.Options{
AuthenticationMethod: httpclient.AuthenticationMethodOAuth2,
Headers: map[string]string{"h1": "v1"},
OAuth2Options: &httpclient.OAuth2Options{
OAuth2Type: "invalid",
TokenURL: server.URL + "/token",
},
})
require.NotNil(t, hc)
require.NotNil(t, err)
assert.Equal(t, errors.New("invalid/empty oauth2 type (invalid)"), err)
}

func TestHTTPClientOAuth2ClientCredentials(t *testing.T) {
server := testGetOAuthServer(t)
defer server.Close()
t.Run("valid client credentials should respond correctly", func(t *testing.T) {
hc, err := httpclient.New(httpclient.Options{
AuthenticationMethod: httpclient.AuthenticationMethodOAuth2,
Headers: map[string]string{"h1": "v1"},
OAuth2Options: &httpclient.OAuth2Options{
OAuth2Type: httpclient.OAuth2TypeClientCredentials,
TokenURL: server.URL + "/token",
},
})
require.Nil(t, err)
require.NotNil(t, hc)
res, err := hc.Get(server.URL + "/foo")
require.Nil(t, err)
require.NotNil(t, res)
if res != nil && res.Body != nil {
defer res.Body.Close()
}
bodyBytes, err := io.ReadAll(res.Body)
require.Nil(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
assert.Equal(t, `"hello world"`, string(bodyBytes))
})
}

func TestHTTPClientOAuth2JWT(t *testing.T) {
server := testGetOAuthServer(t)
defer server.Close()
t.Run("invalid private key should throw error", func(t *testing.T) {
privateKey := testGenerateKey(t)
hc, err := httpclient.New(httpclient.Options{
AuthenticationMethod: httpclient.AuthenticationMethodOAuth2,
Headers: map[string]string{"h1": "v1"},
OAuth2Options: &httpclient.OAuth2Options{
OAuth2Type: httpclient.OAuth2TypeJWT,
TokenURL: server.URL + "/token",
PrivateKey: privateKey,
},
})
require.Nil(t, err)
require.NotNil(t, hc)
res, err := hc.Get(server.URL + "/foo")
if res != nil && res.Body != nil {
defer res.Body.Close()
}
require.NotNil(t, err)
assert.True(t, strings.Contains(err.Error(), "private key should be a PEM or plain PKCS1 or PKCS8; parse error: asn1: structure error"))
require.Nil(t, res)
})
t.Run("valid private key should not throw error", func(t *testing.T) {
privateKey := testGenerateKey(t)
hc, err := httpclient.New(httpclient.Options{
AuthenticationMethod: httpclient.AuthenticationMethodOAuth2,
Headers: map[string]string{"h1": "v1"},
OAuth2Options: &httpclient.OAuth2Options{
OAuth2Type: httpclient.OAuth2TypeJWT,
TokenURL: server.URL + "/token",
PrivateKey: privateKey,
},
})
require.Nil(t, err)
require.NotNil(t, hc)
res, err := hc.Get(server.URL + "/foo")
require.Nil(t, err)
require.NotNil(t, res)
if res != nil && res.Body != nil {
defer res.Body.Close()
}
bodyBytes, err := io.ReadAll(res.Body)
require.Nil(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
assert.Equal(t, `"hello world"`, string(bodyBytes))
})
}

func testGetOAuthServer(t *testing.T) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenValue := "foo"
if r.URL.String() != "/token" {
if r.Header.Get("Authorization") != "Bearer foo" {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `"hello world"`)
return
}
if r.Header.Get("h1") != "v1" {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, fmt.Sprintf(`{"access_token": "%s", "refresh_token": "bar"}`, tokenValue))
}))
}

func testGenerateKey(t *testing.T) (privateKey []byte) {
t.Helper()
if strings.Contains(t.Name(), "invalid_private_key") {
return []byte("invalid private key")
}
key, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
panic(err)
}
privateKey = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
return privateKey
}
38 changes: 38 additions & 0 deletions backend/httpclient/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,24 @@ type ConfigureTransportFunc func(opts Options, transport *http.Transport)
// Called after tls.Config creation.
type ConfigureTLSConfigFunc func(opts Options, tlsConfig *tls.Config)

// AuthenticationMethod defines the type of authentication method that needs to be use.
type AuthenticationMethod string

const (
// AuthenticationMethodOAuth2 is oauth2 type authentication.
// Currently support client credentials and JWT type OAuth2 workflows.
AuthenticationMethodOAuth2 AuthenticationMethod = "oauth2"
)

// Options defines options for creating HTTP clients.
type Options struct {

// AuthenticationMethod defines the type of authentication method used by the http client.
AuthenticationMethod AuthenticationMethod

// OAuth2Options defines options for OAuth2 client. This will be used only if AuthenticationMethod is AuthenticationMethodOAuth.
OAuth2Options *OAuth2Options

// Timeouts timeout/connection related options.
Timeouts *TimeoutOptions

Expand Down Expand Up @@ -65,6 +81,28 @@ type BasicAuthOptions struct {
Password string
}

// OAuth2Type defines type of oauth2 grant type
type OAuth2Type string

const (
OAuth2TypeClientCredentials OAuth2Type = "client_credentials"
OAuth2TypeJWT OAuth2Type = "jwt"
)

// OAuth2Options defines options for OAuth2 Client
type OAuth2Options struct {
OAuth2Type OAuth2Type
TokenURL string
Scopes []string
ClientID string
ClientSecret string
EndpointParams map[string]string
Subject string
Email string
PrivateKey []byte
PrivateKeyID string
}

// TimeoutOptions timeout/connection options.
type TimeoutOptions struct {
Timeout time.Duration
Expand Down
10 changes: 6 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ require (
github.com/prometheus/client_golang v1.12.1
github.com/prometheus/common v0.32.1
github.com/stretchr/testify v1.8.1
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9
golang.org/x/sys v0.5.0
google.golang.org/grpc v1.41.0
google.golang.org/protobuf v1.27.1
google.golang.org/protobuf v1.28.0
gopkg.in/yaml.v3 v3.0.1 // indirect
)

Expand All @@ -34,7 +34,8 @@ require (
github.com/google/uuid v1.3.0
github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8
github.com/urfave/cli v1.22.1
golang.org/x/text v0.3.6
golang.org/x/oauth2 v0.5.0
golang.org/x/text v0.7.0
)

require (
Expand Down Expand Up @@ -69,8 +70,9 @@ require (
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/unknwon/com v1.0.1 // indirect
github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3 // indirect
golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
golang.org/x/net v0.6.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20210630183607-d20f26d13c79 // indirect
gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
Expand Down
16 changes: 12 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -432,14 +432,17 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s=
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down Expand Up @@ -494,17 +497,19 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
Expand Down Expand Up @@ -588,6 +593,8 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
Expand Down Expand Up @@ -653,8 +660,9 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down