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 }