Skip to content

Commit 6be9232

Browse files
Experimental: OAuth Token Retriever (#702)
Co-authored-by: Mihaly Gyongyosi <[email protected]>
1 parent d40c8ed commit 6be9232

File tree

7 files changed

+287
-2
lines changed

7 files changed

+287
-2
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package oauthtokenretriever
2+
3+
import (
4+
"crypto/x509"
5+
"encoding/pem"
6+
"errors"
7+
"fmt"
8+
9+
"github.com/go-jose/go-jose/v3"
10+
"github.com/go-jose/go-jose/v3/jwt"
11+
)
12+
13+
type signer interface {
14+
sign(payload interface{}) (string, error)
15+
}
16+
17+
type jwtSigner struct {
18+
signer jose.Signer
19+
}
20+
21+
// parsePrivateKey parses a PEM encoded private key.
22+
func parsePrivateKey(pemBytes []byte) (signer, error) {
23+
block, _ := pem.Decode(pemBytes)
24+
if block == nil {
25+
return nil, errors.New("crypto: no key found")
26+
}
27+
28+
var rawkey interface{}
29+
var alg jose.SignatureAlgorithm
30+
switch block.Type {
31+
case "RSA PRIVATE KEY":
32+
alg = jose.RS256
33+
rsa, err := x509.ParsePKCS1PrivateKey(block.Bytes)
34+
if err != nil {
35+
return nil, err
36+
}
37+
rawkey = rsa
38+
case "PRIVATE KEY":
39+
alg = jose.ES256
40+
ecdsa, err := x509.ParsePKCS8PrivateKey(block.Bytes)
41+
if err != nil {
42+
return nil, err
43+
}
44+
rawkey = ecdsa
45+
default:
46+
return nil, fmt.Errorf("crypto: unsupported private key type %q", block.Type)
47+
}
48+
s, err := jose.NewSigner(jose.SigningKey{Algorithm: alg, Key: rawkey}, &jose.SignerOptions{})
49+
if err != nil {
50+
return nil, err
51+
}
52+
return &jwtSigner{signer: s}, nil
53+
}
54+
55+
func (s *jwtSigner) sign(payload interface{}) (string, error) {
56+
return jwt.Signed(s.signer).Claims(payload).CompactSerialize()
57+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package oauthtokenretriever
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
const (
10+
testRSAKey = `-----BEGIN RSA PRIVATE KEY-----
11+
MIICWwIBAAKBgQC35vznv35Kaby20gu+RQBDj/kHhPd64b6p9TKKxqiAs8kukNFj
12+
Q8keR6MOO41Md0Jh4b/ZSo1O3C3K3K587NORJDWz0H2wVyTWDvSMI36nI/EnGDhh
13+
4fImv5E/9jIvhOxCJ3Dej57//tMt8TEG1ZETrAKzUvB7EfCfsnazGraMQwIDAQAB
14+
AoGAfbFh4B+w+LlGY4oyvow4vvTTV4FZCOLsRwuwzMs09iprcelHQ9pbxtddqeeo
15+
DsBgXbhHQQPEi0bQAZxNolLX0m4nQ8n9H6by42qOJlwywYZIl7Di3aWYiOiT56v7
16+
PfqCsShSqsvWH8Ok4Jy6/Vcc4QcO4mGi8y8EZdSqfytGvkkCQQDhO+1Y4x36ETAh
17+
NOQx1E/psPuSH8H6YeDoWYeap5z1KXzN4eTo01p8ckPSD93uXIig7LmfIWPMqlGV
18+
yOBSyqD/AkEA0QXBLeDksi8hX8B2XOMfY9hWOBwBRXrlKX6TVF/9Kw+ulJpe3sU5
19+
lc53oytpk1VwXAfJrjNRqyIIIRnFyTJQvQJAMBgFxFcqzXziFBUhLOqy7amW7krN
20+
ttMznSmQ5RspTsg/GA9GO9j1l2EmzjIJJ56mpgYmVK5iiw9LQHqWO9d8rQJASUDz
21+
CtkeTTQnRh91W+hdP+i5jsCB0Y/YcEpj59YcK9M7I+lWBkyoec/6Lb0xKuluj1JL
22+
ZDmoDYnHv5IAtxpjIQJASxC/V51AHfuQ+rWvbZ6jzoHW6owbFpC2RbZPtFanOlda
23+
ozjy/YI5hvWLr/bre/wZ3N81pLA9lPgEpJiOPYem3Q==
24+
-----END RSA PRIVATE KEY-----
25+
`
26+
testECDSAKey = `-----BEGIN PRIVATE KEY-----
27+
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgYH3q1su2TRDIr4RB
28+
2okegCNvfhn/Q9CycAXtPnfYsZehRANCAARSs6LcDI314KqKqGHbv2FLGoMXjm6B
29+
p6/mP7VLRqyPpiGmhCEKXD5R/695X5JYQRBF34hn2XZpMCW2z2Lr+d6s
30+
-----END PRIVATE KEY-----
31+
`
32+
)
33+
34+
func Test_Sign(t *testing.T) {
35+
for _, test := range []struct {
36+
name string
37+
key string
38+
length int
39+
}{
40+
{"RSA", testRSAKey, 196},
41+
{"ECDSA", testECDSAKey, 111},
42+
} {
43+
t.Run(test.name, func(t *testing.T) {
44+
signer, err := parsePrivateKey([]byte(test.key))
45+
assert.NoError(t, err)
46+
signed, err := signer.sign(map[string]interface{}{})
47+
assert.NoError(t, err)
48+
assert.Equal(t, test.length, len(signed))
49+
})
50+
}
51+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package oauthtokenretriever
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/url"
7+
"os"
8+
"strings"
9+
"time"
10+
11+
"github.com/google/uuid"
12+
"golang.org/x/oauth2"
13+
"golang.org/x/oauth2/clientcredentials"
14+
)
15+
16+
type TokenRetriever interface {
17+
OnBehalfOfUser(ctx context.Context, userID string) (string, error)
18+
Self(ctx context.Context) (string, error)
19+
}
20+
21+
type tokenRetriever struct {
22+
signer signer
23+
conf *clientcredentials.Config
24+
}
25+
26+
// tokenPayload returns a JWT payload for the given user ID, client ID, and host.
27+
func (t *tokenRetriever) tokenPayload(userID string) map[string]interface{} {
28+
iat := time.Now().Unix()
29+
exp := iat + 1800
30+
u := uuid.New()
31+
payload := map[string]interface{}{
32+
"iss": t.conf.ClientID,
33+
"sub": fmt.Sprintf("user:id:%s", userID),
34+
"aud": t.conf.TokenURL,
35+
"exp": exp,
36+
"iat": iat,
37+
"jti": u.String(),
38+
}
39+
return payload
40+
}
41+
42+
func (t *tokenRetriever) Self(ctx context.Context) (string, error) {
43+
t.conf.EndpointParams = url.Values{}
44+
tok, err := t.conf.TokenSource(ctx).Token()
45+
if err != nil {
46+
return "", err
47+
}
48+
return tok.AccessToken, nil
49+
}
50+
51+
func (t *tokenRetriever) OnBehalfOfUser(ctx context.Context, userID string) (string, error) {
52+
signed, err := t.signer.sign(t.tokenPayload(userID))
53+
if err != nil {
54+
return "", err
55+
}
56+
57+
t.conf.EndpointParams = url.Values{
58+
"grant_type": {"urn:ietf:params:oauth:grant-type:jwt-bearer"},
59+
"assertion": {signed},
60+
}
61+
tok, err := t.conf.TokenSource(ctx).Token()
62+
if err != nil {
63+
return "", err
64+
}
65+
66+
return tok.AccessToken, nil
67+
}
68+
69+
func New() (TokenRetriever, error) {
70+
// The Grafana URL is required to obtain tokens later on
71+
grafanaAppURL := strings.TrimRight(os.Getenv("GF_APP_URL"), "/")
72+
if grafanaAppURL == "" {
73+
// For debugging purposes only
74+
grafanaAppURL = "http://localhost:3000"
75+
}
76+
77+
clientID := os.Getenv("GF_PLUGIN_APP_CLIENT_ID")
78+
if clientID == "" {
79+
return nil, fmt.Errorf("GF_PLUGIN_APP_CLIENT_ID is required")
80+
}
81+
82+
clientSecret := os.Getenv("GF_PLUGIN_APP_CLIENT_SECRET")
83+
if clientSecret == "" {
84+
return nil, fmt.Errorf("GF_PLUGIN_APP_CLIENT_SECRET is required")
85+
}
86+
87+
privateKey := os.Getenv("GF_PLUGIN_APP_PRIVATE_KEY")
88+
if privateKey == "" {
89+
return nil, fmt.Errorf("GF_PLUGIN_APP_PRIVATE_KEY is required")
90+
}
91+
92+
signer, err := parsePrivateKey([]byte(privateKey))
93+
if err != nil {
94+
return nil, err
95+
}
96+
97+
return &tokenRetriever{
98+
signer: signer,
99+
conf: &clientcredentials.Config{
100+
ClientID: clientID,
101+
ClientSecret: clientSecret,
102+
TokenURL: grafanaAppURL + "/oauth2/token",
103+
AuthStyle: oauth2.AuthStyleInParams,
104+
Scopes: []string{"profile", "email", "entitlements"},
105+
},
106+
}, nil
107+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package oauthtokenretriever
2+
3+
import (
4+
"context"
5+
"io"
6+
"net/http"
7+
"net/http/httptest"
8+
"os"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
func Test_GetExternalServiceToken(t *testing.T) {
15+
for _, test := range []struct {
16+
name string
17+
userID string
18+
}{
19+
{"On Behalf Of", "1"},
20+
{"Service account", ""},
21+
} {
22+
t.Run(test.name, func(t *testing.T) {
23+
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
24+
b, err := io.ReadAll(r.Body)
25+
assert.NoError(t, err)
26+
if test.userID != "" {
27+
assert.Contains(t, string(b), "assertion=")
28+
assert.Contains(t, string(b), "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer")
29+
} else {
30+
assert.NotContains(t, string(b), "assertion=")
31+
assert.Contains(t, string(b), "grant_type=client_credentials")
32+
}
33+
assert.Contains(t, string(b), "client_id=test_client_id")
34+
assert.Contains(t, string(b), "client_secret=test_client_secret")
35+
36+
w.Header().Set("Content-Type", "application/json")
37+
_, err = w.Write([]byte(`{"access_token":"test_token"}`))
38+
assert.NoError(t, err)
39+
}))
40+
defer s.Close()
41+
42+
os.Setenv("GF_APP_URL", s.URL)
43+
defer os.Unsetenv("GF_APP_URL")
44+
os.Setenv("GF_PLUGIN_APP_CLIENT_ID", "test_client_id")
45+
defer os.Unsetenv("GF_PLUGIN_APP_CLIENT_ID")
46+
os.Setenv("GF_PLUGIN_APP_CLIENT_SECRET", "test_client_secret")
47+
defer os.Unsetenv("GF_PLUGIN_APP_CLIENT_SECRET")
48+
os.Setenv("GF_PLUGIN_APP_PRIVATE_KEY", testECDSAKey)
49+
defer os.Unsetenv("GF_PLUGIN_APP_PRIVATE_KEY")
50+
51+
ss, err := New()
52+
assert.NoError(t, err)
53+
54+
var token string
55+
if test.userID != "" {
56+
token, err = ss.OnBehalfOfUser(context.Background(), test.userID)
57+
} else {
58+
token, err = ss.Self(context.Background())
59+
}
60+
assert.NoError(t, err)
61+
assert.Equal(t, "test_token", token)
62+
})
63+
}
64+
}

experimental/testdata/folder.golden.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Frame[0] {
99
"pathSeparator": "/"
1010
}
1111
Name:
12-
Dimensions: 2 Fields by 15 Rows
12+
Dimensions: 2 Fields by 16 Rows
1313
+----------------------------+------------------+
1414
| Name: name | Name: media-type |
1515
| Labels: | Labels: |
@@ -29,4 +29,4 @@ Dimensions: 2 Fields by 15 Rows
2929

3030

3131
====== TEST DATA RESPONSE (arrow base64) ======
32-
FRAME=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
32+
FRAME=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

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ require (
5656
github.com/davecgh/go-spew v1.1.1 // indirect
5757
github.com/elazarl/goproxy/ext v0.0.0-20220115173737-adb46da277ac // indirect
5858
github.com/fatih/color v1.15.0 // indirect
59+
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
5960
github.com/go-logr/logr v1.2.3 // indirect
6061
github.com/go-logr/stdr v1.2.2 // indirect
6162
github.com/go-openapi/jsonpointer v0.19.5 // indirect
@@ -86,6 +87,7 @@ require (
8687
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.14.0 // indirect
8788
go.opentelemetry.io/otel/metric v0.37.0 // indirect
8889
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
90+
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
8991
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
9092
google.golang.org/appengine v1.6.7 // indirect
9193
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmn
108108
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
109109
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
110110
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
111+
github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
112+
github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
111113
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
112114
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
113115
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
@@ -368,7 +370,9 @@ go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
368370
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
369371
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
370372
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
373+
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
371374
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
375+
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
372376
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
373377
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
374378
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=

0 commit comments

Comments
 (0)