-
Notifications
You must be signed in to change notification settings - Fork 67
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* reapply previous commit * add all the things
- Loading branch information
Showing
7 changed files
with
287 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package oauthtokenretriever | ||
|
||
import ( | ||
"crypto/x509" | ||
"encoding/pem" | ||
"errors" | ||
"fmt" | ||
|
||
"github.com/go-jose/go-jose/v3" | ||
"github.com/go-jose/go-jose/v3/jwt" | ||
) | ||
|
||
type signer interface { | ||
sign(payload interface{}) (string, error) | ||
} | ||
|
||
type jwtSigner struct { | ||
signer jose.Signer | ||
} | ||
|
||
// parsePrivateKey parses a PEM encoded private key. | ||
func parsePrivateKey(pemBytes []byte) (signer, error) { | ||
block, _ := pem.Decode(pemBytes) | ||
if block == nil { | ||
return nil, errors.New("crypto: no key found") | ||
} | ||
|
||
var rawkey interface{} | ||
var alg jose.SignatureAlgorithm | ||
switch block.Type { | ||
case "RSA PRIVATE KEY": | ||
alg = jose.RS256 | ||
rsa, err := x509.ParsePKCS1PrivateKey(block.Bytes) | ||
if err != nil { | ||
return nil, err | ||
} | ||
rawkey = rsa | ||
case "PRIVATE KEY": | ||
alg = jose.ES256 | ||
ecdsa, err := x509.ParsePKCS8PrivateKey(block.Bytes) | ||
if err != nil { | ||
return nil, err | ||
} | ||
rawkey = ecdsa | ||
default: | ||
return nil, fmt.Errorf("crypto: unsupported private key type %q", block.Type) | ||
} | ||
s, err := jose.NewSigner(jose.SigningKey{Algorithm: alg, Key: rawkey}, &jose.SignerOptions{}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return &jwtSigner{signer: s}, nil | ||
} | ||
|
||
func (s *jwtSigner) sign(payload interface{}) (string, error) { | ||
return jwt.Signed(s.signer).Claims(payload).CompactSerialize() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package oauthtokenretriever | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
const ( | ||
testRSAKey = `-----BEGIN RSA PRIVATE KEY----- | ||
MIICWwIBAAKBgQC35vznv35Kaby20gu+RQBDj/kHhPd64b6p9TKKxqiAs8kukNFj | ||
Q8keR6MOO41Md0Jh4b/ZSo1O3C3K3K587NORJDWz0H2wVyTWDvSMI36nI/EnGDhh | ||
4fImv5E/9jIvhOxCJ3Dej57//tMt8TEG1ZETrAKzUvB7EfCfsnazGraMQwIDAQAB | ||
AoGAfbFh4B+w+LlGY4oyvow4vvTTV4FZCOLsRwuwzMs09iprcelHQ9pbxtddqeeo | ||
DsBgXbhHQQPEi0bQAZxNolLX0m4nQ8n9H6by42qOJlwywYZIl7Di3aWYiOiT56v7 | ||
PfqCsShSqsvWH8Ok4Jy6/Vcc4QcO4mGi8y8EZdSqfytGvkkCQQDhO+1Y4x36ETAh | ||
NOQx1E/psPuSH8H6YeDoWYeap5z1KXzN4eTo01p8ckPSD93uXIig7LmfIWPMqlGV | ||
yOBSyqD/AkEA0QXBLeDksi8hX8B2XOMfY9hWOBwBRXrlKX6TVF/9Kw+ulJpe3sU5 | ||
lc53oytpk1VwXAfJrjNRqyIIIRnFyTJQvQJAMBgFxFcqzXziFBUhLOqy7amW7krN | ||
ttMznSmQ5RspTsg/GA9GO9j1l2EmzjIJJ56mpgYmVK5iiw9LQHqWO9d8rQJASUDz | ||
CtkeTTQnRh91W+hdP+i5jsCB0Y/YcEpj59YcK9M7I+lWBkyoec/6Lb0xKuluj1JL | ||
ZDmoDYnHv5IAtxpjIQJASxC/V51AHfuQ+rWvbZ6jzoHW6owbFpC2RbZPtFanOlda | ||
ozjy/YI5hvWLr/bre/wZ3N81pLA9lPgEpJiOPYem3Q== | ||
-----END RSA PRIVATE KEY----- | ||
` | ||
testECDSAKey = `-----BEGIN PRIVATE KEY----- | ||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgYH3q1su2TRDIr4RB | ||
2okegCNvfhn/Q9CycAXtPnfYsZehRANCAARSs6LcDI314KqKqGHbv2FLGoMXjm6B | ||
p6/mP7VLRqyPpiGmhCEKXD5R/695X5JYQRBF34hn2XZpMCW2z2Lr+d6s | ||
-----END PRIVATE KEY----- | ||
` | ||
) | ||
|
||
func Test_Sign(t *testing.T) { | ||
for _, test := range []struct { | ||
name string | ||
key string | ||
length int | ||
}{ | ||
{"RSA", testRSAKey, 196}, | ||
{"ECDSA", testECDSAKey, 111}, | ||
} { | ||
t.Run(test.name, func(t *testing.T) { | ||
signer, err := parsePrivateKey([]byte(test.key)) | ||
assert.NoError(t, err) | ||
signed, err := signer.sign(map[string]interface{}{}) | ||
assert.NoError(t, err) | ||
assert.Equal(t, test.length, len(signed)) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
package oauthtokenretriever | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net/url" | ||
"os" | ||
"strings" | ||
"time" | ||
|
||
"github.com/google/uuid" | ||
"golang.org/x/oauth2" | ||
"golang.org/x/oauth2/clientcredentials" | ||
) | ||
|
||
type TokenRetriever interface { | ||
OnBehalfOfUser(ctx context.Context, userID string) (string, error) | ||
Self(ctx context.Context) (string, error) | ||
} | ||
|
||
type tokenRetriever struct { | ||
signer signer | ||
conf *clientcredentials.Config | ||
} | ||
|
||
// tokenPayload returns a JWT payload for the given user ID, client ID, and host. | ||
func (t *tokenRetriever) tokenPayload(userID string) map[string]interface{} { | ||
iat := time.Now().Unix() | ||
exp := iat + 1800 | ||
u := uuid.New() | ||
payload := map[string]interface{}{ | ||
"iss": t.conf.ClientID, | ||
"sub": fmt.Sprintf("user:id:%s", userID), | ||
"aud": t.conf.TokenURL, | ||
"exp": exp, | ||
"iat": iat, | ||
"jti": u.String(), | ||
} | ||
return payload | ||
} | ||
|
||
func (t *tokenRetriever) Self(ctx context.Context) (string, error) { | ||
t.conf.EndpointParams = url.Values{} | ||
tok, err := t.conf.TokenSource(ctx).Token() | ||
if err != nil { | ||
return "", err | ||
} | ||
return tok.AccessToken, nil | ||
} | ||
|
||
func (t *tokenRetriever) OnBehalfOfUser(ctx context.Context, userID string) (string, error) { | ||
signed, err := t.signer.sign(t.tokenPayload(userID)) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
t.conf.EndpointParams = url.Values{ | ||
"grant_type": {"urn:ietf:params:oauth:grant-type:jwt-bearer"}, | ||
"assertion": {signed}, | ||
} | ||
tok, err := t.conf.TokenSource(ctx).Token() | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
return tok.AccessToken, nil | ||
} | ||
|
||
func New() (TokenRetriever, error) { | ||
// The Grafana URL is required to obtain tokens later on | ||
grafanaAppURL := strings.TrimRight(os.Getenv("GF_APP_URL"), "/") | ||
if grafanaAppURL == "" { | ||
// For debugging purposes only | ||
grafanaAppURL = "http://localhost:3000" | ||
} | ||
|
||
clientID := os.Getenv("GF_PLUGIN_APP_CLIENT_ID") | ||
if clientID == "" { | ||
return nil, fmt.Errorf("GF_PLUGIN_APP_CLIENT_ID is required") | ||
} | ||
|
||
clientSecret := os.Getenv("GF_PLUGIN_APP_CLIENT_SECRET") | ||
if clientSecret == "" { | ||
return nil, fmt.Errorf("GF_PLUGIN_APP_CLIENT_SECRET is required") | ||
} | ||
|
||
privateKey := os.Getenv("GF_PLUGIN_APP_PRIVATE_KEY") | ||
if privateKey == "" { | ||
return nil, fmt.Errorf("GF_PLUGIN_APP_PRIVATE_KEY is required") | ||
} | ||
|
||
signer, err := parsePrivateKey([]byte(privateKey)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &tokenRetriever{ | ||
signer: signer, | ||
conf: &clientcredentials.Config{ | ||
ClientID: clientID, | ||
ClientSecret: clientSecret, | ||
TokenURL: grafanaAppURL + "/oauth2/token", | ||
AuthStyle: oauth2.AuthStyleInParams, | ||
Scopes: []string{"profile", "email", "entitlements"}, | ||
}, | ||
}, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package oauthtokenretriever | ||
|
||
import ( | ||
"context" | ||
"io" | ||
"net/http" | ||
"net/http/httptest" | ||
"os" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func Test_GetExternalServiceToken(t *testing.T) { | ||
for _, test := range []struct { | ||
name string | ||
userID string | ||
}{ | ||
{"On Behalf Of", "1"}, | ||
{"Service account", ""}, | ||
} { | ||
t.Run(test.name, func(t *testing.T) { | ||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
b, err := io.ReadAll(r.Body) | ||
assert.NoError(t, err) | ||
if test.userID != "" { | ||
assert.Contains(t, string(b), "assertion=") | ||
assert.Contains(t, string(b), "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer") | ||
} else { | ||
assert.NotContains(t, string(b), "assertion=") | ||
assert.Contains(t, string(b), "grant_type=client_credentials") | ||
} | ||
assert.Contains(t, string(b), "client_id=test_client_id") | ||
assert.Contains(t, string(b), "client_secret=test_client_secret") | ||
|
||
w.Header().Set("Content-Type", "application/json") | ||
_, err = w.Write([]byte(`{"access_token":"test_token"}`)) | ||
assert.NoError(t, err) | ||
})) | ||
defer s.Close() | ||
|
||
os.Setenv("GF_APP_URL", s.URL) | ||
defer os.Unsetenv("GF_APP_URL") | ||
os.Setenv("GF_PLUGIN_APP_CLIENT_ID", "test_client_id") | ||
defer os.Unsetenv("GF_PLUGIN_APP_CLIENT_ID") | ||
os.Setenv("GF_PLUGIN_APP_CLIENT_SECRET", "test_client_secret") | ||
defer os.Unsetenv("GF_PLUGIN_APP_CLIENT_SECRET") | ||
os.Setenv("GF_PLUGIN_APP_PRIVATE_KEY", testECDSAKey) | ||
defer os.Unsetenv("GF_PLUGIN_APP_PRIVATE_KEY") | ||
|
||
ss, err := New() | ||
assert.NoError(t, err) | ||
|
||
var token string | ||
if test.userID != "" { | ||
token, err = ss.OnBehalfOfUser(context.Background(), test.userID) | ||
} else { | ||
token, err = ss.Self(context.Background()) | ||
} | ||
assert.NoError(t, err) | ||
assert.Equal(t, "test_token", token) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters