github.com/grafana/pyroscope@v1.18.0/pkg/frontend/vcs/github.go (about)

     1  package vcs
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"net/url"
    10  	"os"
    11  	"time"
    12  
    13  	"golang.org/x/oauth2"
    14  	"golang.org/x/oauth2/endpoints"
    15  )
    16  
    17  const (
    18  	githubRefreshURL = "https://github.com/login/oauth/access_token"
    19  
    20  	// Duration of a GitHub refresh token. The original OAuth flow doesn't
    21  	// return the refresh token expiry, so we need to store it separately.
    22  	// GitHub docs state this value will never change:
    23  	//
    24  	// https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/refreshing-user-access-tokens
    25  	githubRefreshExpiryDuration = 15897600 * time.Second
    26  )
    27  
    28  var (
    29  	githubAppClientID     = os.Getenv("GITHUB_CLIENT_ID")
    30  	githubAppClientSecret = os.Getenv("GITHUB_CLIENT_SECRET")
    31  	githubAppCallbackURL  = os.Getenv("GITHUB_CALLBACK_URL")
    32  )
    33  
    34  type githubAuthToken struct {
    35  	AccessToken           string        `json:"access_token"`
    36  	ExpiresIn             time.Duration `json:"expires_in"`
    37  	RefreshToken          string        `json:"refresh_token"`
    38  	RefreshTokenExpiresIn time.Duration `json:"refresh_token_expires_in"`
    39  	Scope                 string        `json:"scope"`
    40  	TokenType             string        `json:"token_type"`
    41  }
    42  
    43  // toOAuthToken converts a githubAuthToken to an OAuth token.
    44  func (t githubAuthToken) toOAuthToken() *oauth2.Token {
    45  	return &oauth2.Token{
    46  		AccessToken:  t.AccessToken,
    47  		TokenType:    t.TokenType,
    48  		RefreshToken: t.RefreshToken,
    49  		Expiry:       time.Now().Add(t.ExpiresIn),
    50  	}
    51  }
    52  
    53  // githubOAuthConfig creates a GitHub OAuth config.
    54  func githubOAuthConfig() (*oauth2.Config, error) {
    55  	if githubAppClientID == "" {
    56  		return nil, fmt.Errorf("missing GITHUB_CLIENT_ID environment variable")
    57  	}
    58  	if githubAppClientSecret == "" {
    59  		return nil, fmt.Errorf("missing GITHUB_CLIENT_SECRET environment variable")
    60  	}
    61  	return &oauth2.Config{
    62  		ClientID:     githubAppClientID,
    63  		ClientSecret: githubAppClientSecret,
    64  		Endpoint:     endpoints.GitHub,
    65  	}, nil
    66  }
    67  
    68  // refreshGithubToken sends a request configured for the GitHub API and marshals
    69  // the response into a githubAuthToken.
    70  func refreshGithubToken(req *http.Request, client *http.Client) (*githubAuthToken, error) {
    71  	res, err := client.Do(req)
    72  	if err != nil {
    73  		return nil, fmt.Errorf("failed to make request: %w", err)
    74  	}
    75  	defer res.Body.Close()
    76  
    77  	bytes, err := io.ReadAll(res.Body)
    78  	if err != nil {
    79  		return nil, fmt.Errorf("failed to read response body: %w", err)
    80  	}
    81  
    82  	// The response body is application/x-www-form-urlencoded, so we parse it
    83  	// via url.ParseQuery.
    84  	payload, err := url.ParseQuery(string(bytes))
    85  	if err != nil {
    86  		return nil, fmt.Errorf("failed to parse response body: %w", err)
    87  	}
    88  
    89  	githubToken, err := githubAuthTokenFromFormURLEncoded(payload)
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  
    94  	return githubToken, nil
    95  }
    96  
    97  // buildGithubRefreshRequest builds a cancelable http.Request which is
    98  // configured to hit the GitHub API's token refresh endpoint.
    99  func buildGithubRefreshRequest(ctx context.Context, oldToken *oauth2.Token) (*http.Request, error) {
   100  	req, err := http.NewRequestWithContext(ctx, "POST", githubRefreshURL, nil)
   101  	if err != nil {
   102  		return nil, fmt.Errorf("failed to create request: %w", err)
   103  	}
   104  
   105  	query := req.URL.Query()
   106  	query.Add("client_id", githubAppClientID)
   107  	query.Add("client_secret", githubAppClientSecret)
   108  	query.Add("grant_type", "refresh_token")
   109  	query.Add("refresh_token", oldToken.RefreshToken)
   110  
   111  	req.URL.RawQuery = query.Encode()
   112  	return req, nil
   113  }
   114  
   115  // githubAuthTokenFromFormURLEncoded converts a url-encoded form to a
   116  // githubAuthToken.
   117  func githubAuthTokenFromFormURLEncoded(values url.Values) (*githubAuthToken, error) {
   118  	token := &githubAuthToken{}
   119  	var err error
   120  
   121  	token.AccessToken, err = getStringValueFrom(values, "access_token")
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  
   126  	token.ExpiresIn, err = getDurationValueFrom(values, "expires_in", time.Second)
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  
   131  	token.RefreshToken, err = getStringValueFrom(values, "refresh_token")
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  
   136  	token.RefreshTokenExpiresIn, err = getDurationValueFrom(values, "refresh_token_expires_in", time.Second)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  
   141  	token.Scope, err = getStringValueFrom(values, "scope")
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  
   146  	token.TokenType, err = getStringValueFrom(values, "token_type")
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  
   151  	return token, nil
   152  }
   153  
   154  func isGitHubIntegrationConfigured() error {
   155  	var errs []error
   156  
   157  	if githubAppClientID == "" {
   158  		errs = append(errs, fmt.Errorf("missing GITHUB_CLIENT_ID environment variable"))
   159  	}
   160  
   161  	if githubAppClientSecret == "" {
   162  		errs = append(errs, fmt.Errorf("missing GITHUB_CLIENT_SECRET environment variable"))
   163  	}
   164  
   165  	if len(githubSessionSecret) == 0 {
   166  		errs = append(errs, fmt.Errorf("missing GITHUB_SESSION_SECRET environment variable"))
   167  	}
   168  
   169  	return errors.Join(errs...)
   170  }