|
| 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 | +} |
0 commit comments