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

allow custom token header key and custom prefix in oauth2 #1142

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions .changeset/twenty-parents-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'grafana-infinity-datasource': minor
---

**Experimental:** Allow custom token header key and custom token prefix in OAuth2 client credentials and OAuth2 JWT authentication
34 changes: 19 additions & 15 deletions docs/sources/setup/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,26 +84,30 @@ If your Grafana user is already authenticated via OAuth, this authentication met

OAuth 2.0 client credentials require the following parameters:

| Key | Description |
| ------------------- | ------------------------------------------------------------------------------------------------- |
| **Client ID** | ClientID is the application's ID. |
| **Client Secret** | ClientSecret is the application's secret. |
| **Token URL** | TokenURL is the resource server's token endpoint URL. This is a constant specific to each server. |
| **Scopes** | Scope specifies optional requested permissions. |
| **Endpoint params** | EndpointParams specifies additional parameters for requests to the token endpoint. |
| Key | Description |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Client ID** | ClientID is the application's ID. |
| **Client Secret** | ClientSecret is the application's secret. |
| **Token URL** | TokenURL is the resource server's token endpoint URL. This is a constant specific to each server. |
| **Scopes** | Scope specifies optional requested permissions. |
| **Endpoint params** | EndpointParams specifies additional parameters for requests to the token endpoint. |
| **Custom token header key** | Once the token retrieved, the same will be sent to subsequent request's header with the key "Authorization". If the API require different key, provide the key here |
| **Custom token prefix** | Once the token retrieved, the same will be sent to subsequent request's Authorization header with the prefix "Bearer " or whatever returned from the initial token retrieval request's response. If the API require different prefix, provide the prefix here. Refer https://datatracker.ietf.org/doc/html/rfc6749#section-7.1 for more details |

## OAuth 2.0 JWT

OAuth 2.0 JWT require the following parameters

| Key | Description |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| **Email** | Email is the OAuth client identifier used when communicating with the configured OAuth provider. |
| **Private Key** | PrivateKey contains the contents of an RSA private key or the contents of a PEM file that contains a private key. |
| **Private Key Identifier** | Optional. PrivateKeyID contains an optional hint indicating which key to use. |
| **Token URL** | TokenURL is the endpoint required to complete the 2-legged JWT flow. |
| **Subject** | Optional. Subject is the optional user to impersonate. |
| **Scopes** | Scopes optionally specifies a list of requested permission scopes. Provide scopes as a comma separated values. |
| Key | Description |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Email** | Email is the OAuth client identifier used when communicating with the configured OAuth provider. |
| **Private Key** | PrivateKey contains the contents of an RSA private key or the contents of a PEM file that contains a private key. |
| **Private Key Identifier** | Optional. PrivateKeyID contains an optional hint indicating which key to use. |
| **Token URL** | TokenURL is the endpoint required to complete the 2-legged JWT flow. |
| **Subject** | Optional. Subject is the optional user to impersonate. |
| **Scopes** | Scopes optionally specifies a list of requested permission scopes. Provide scopes as a comma separated values. |
| **Custom token header key** | Once the token retrieved, the same will be sent to subsequent request's header with the key "Authorization". If the API require different key, provide the key here |
| **Custom token prefix** | Once the token retrieved, the same will be sent to subsequent request's Authorization header with the prefix "Bearer " or whatever returned from the initial token retrieval request's response. If the API require different prefix, provide the prefix here. Refer https://datatracker.ietf.org/doc/html/rfc6749#section-7.1 for more details |

## Azure

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,5 @@ require (
gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

replace golang.org/x/oauth2 => github.com/yesoreyeram/oauth2 v0.0.0-20250203165345-7bba1e2e4544
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,8 @@ github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
github.com/xiatechs/jsonata-go v1.8.7 h1:3EYi1x16FSDsq82okupP69UuKrvCm9xrNGrW/OSyHC8=
github.com/xiatechs/jsonata-go v1.8.7/go.mod h1:+9C5kah6Dbq0+ECyywWFxxCm7gjSBbHPvc1rLmkWOVE=
github.com/yesoreyeram/oauth2 v0.0.0-20250203165345-7bba1e2e4544 h1:V7SKAgUZNNGCFB6CKLFL5y43+0E8zDmG+9Ec63+tpy8=
github.com/yesoreyeram/oauth2 v0.0.0-20250203165345-7bba1e2e4544/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
Expand Down Expand Up @@ -352,8 +354,6 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down
28 changes: 16 additions & 12 deletions pkg/httpclient/httpclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,14 @@ func applyOAuthClientCredentials(ctx context.Context, httpClient *http.Client, s
defer span.End()
if isOAuthCredentialsConfigured(settings) {
oauthConfig := clientcredentials.Config{
ClientID: settings.OAuth2Settings.ClientID,
ClientSecret: settings.OAuth2Settings.ClientSecret,
TokenURL: settings.OAuth2Settings.TokenURL,
Scopes: []string{},
EndpointParams: url.Values{},
AuthStyle: settings.OAuth2Settings.AuthStyle,
ClientID: settings.OAuth2Settings.ClientID,
ClientSecret: settings.OAuth2Settings.ClientSecret,
TokenURL: settings.OAuth2Settings.TokenURL,
Scopes: []string{},
EndpointParams: url.Values{},
AuthStyle: settings.OAuth2Settings.AuthStyle,
CustomTokenHeaderKey: settings.OAuth2Settings.CustomTokenHeaderKey,
CustomTokenPrefix: settings.OAuth2Settings.CustomTokenPrefix,
}
for _, scope := range settings.OAuth2Settings.Scopes {
if scope != "" {
Expand All @@ -132,12 +134,14 @@ func applyOAuthJWT(ctx context.Context, httpClient *http.Client, settings models
defer span.End()
if isOAuthJWTConfigured(settings) {
jwtConfig := jwt.Config{
Email: settings.OAuth2Settings.Email,
TokenURL: settings.OAuth2Settings.TokenURL,
PrivateKey: []byte(strings.ReplaceAll(settings.OAuth2Settings.PrivateKey, "\\n", "\n")),
PrivateKeyID: settings.OAuth2Settings.PrivateKeyID,
Subject: settings.OAuth2Settings.Subject,
Scopes: []string{},
Email: settings.OAuth2Settings.Email,
TokenURL: settings.OAuth2Settings.TokenURL,
PrivateKey: []byte(strings.ReplaceAll(settings.OAuth2Settings.PrivateKey, "\\n", "\n")),
PrivateKeyID: settings.OAuth2Settings.PrivateKeyID,
Subject: settings.OAuth2Settings.Subject,
Scopes: []string{},
CustomTokenHeaderKey: settings.OAuth2Settings.CustomTokenHeaderKey,
CustomTokenPrefix: settings.OAuth2Settings.CustomTokenPrefix,
}
for _, scope := range settings.OAuth2Settings.Scopes {
if scope != "" {
Expand Down
24 changes: 13 additions & 11 deletions pkg/models/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,19 @@ const (
)

type OAuth2Settings struct {
OAuth2Type string `json:"oauth2_type,omitempty"`
ClientID string `json:"client_id,omitempty"`
TokenURL string `json:"token_url,omitempty"`
Email string `json:"email,omitempty"`
PrivateKeyID string `json:"private_key_id,omitempty"`
Subject string `json:"subject,omitempty"`
Scopes []string `json:"scopes,omitempty"`
AuthStyle oauth2.AuthStyle `json:"authStyle,omitempty"`
ClientSecret string
PrivateKey string
EndpointParams map[string]string
OAuth2Type string `json:"oauth2_type,omitempty"`
ClientID string `json:"client_id,omitempty"`
TokenURL string `json:"token_url,omitempty"`
Email string `json:"email,omitempty"`
PrivateKeyID string `json:"private_key_id,omitempty"`
Subject string `json:"subject,omitempty"`
Scopes []string `json:"scopes,omitempty"`
AuthStyle oauth2.AuthStyle `json:"authStyle,omitempty"`
CustomTokenHeaderKey string `json:"customTokenHeaderKey,omitempty"`
CustomTokenPrefix string `json:"customTokenPrefix,omitempty"`
ClientSecret string
PrivateKey string
EndpointParams map[string]string
}

type AWSAuthType string
Expand Down
43 changes: 43 additions & 0 deletions pkg/testsuite/handler_querydata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,49 @@ func TestAuthentication(t *testing.T) {
require.NotNil(t, metaData)
require.Equal(t, map[string]any(map[string]any{"foo": "bar"}), metaData.Data)
})
t.Run("should respect custom token header key and token prefix", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.String() == "/token" {
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"access_token": "foo", "refresh_token": "bar"}`)
return
}
if r.Header.Get("Auth") != "key=foo" {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"foo":"bar"}`)
}))
defer server.Close()
client, err := infinity.NewClient(context.TODO(), models.InfinitySettings{
URL: server.URL,
AllowedHosts: []string{server.URL},
AuthenticationMethod: models.AuthenticationMethodOAuth,
OAuth2Settings: models.OAuth2Settings{
OAuth2Type: models.AuthOAuthTypeClientCredentials,
TokenURL: server.URL + "/token",
ClientID: "MY_CLIENT_ID",
ClientSecret: "MY_CLIENT_SECRET",
CustomTokenHeaderKey: "Auth",
CustomTokenPrefix: "key=",
Scopes: []string{"scope1", "scope2"},
},
})
require.Nil(t, err)
res := queryData(t, context.Background(), backend.DataQuery{
JSON: []byte(fmt.Sprintf(`{
"type": "json",
"source": "url",
"url": "%s/something-else"
}`, server.URL)),
}, *client, map[string]string{}, backend.PluginContext{})
require.NotNil(t, res)
require.Nil(t, res.Error)
metaData := res.Frames[0].Meta.Custom.(*infinity.CustomMeta)
require.NotNil(t, metaData)
require.Equal(t, map[string]any(map[string]any{"foo": "bar"}), metaData.Data)
})
t.Run("should throw error with invalid oauth credentials", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.String() == "/token" {
Expand Down
Loading
Loading