Skip to content

Commit bba64a1

Browse files
authored
githubauth: fix response parsing (#291)
This fixes a bug where the TokenSource would return the correct value, but the direct invocations would fail. This is affecting the latest Minty release. Review the first commit for the nicer diff. The second commit is moving things around so this mistake doesn't happen again: [1dc001b](1dc001b).
1 parent 5694ef0 commit bba64a1

File tree

2 files changed

+322
-188
lines changed

2 files changed

+322
-188
lines changed

githubauth/app.go

+62-76
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import (
2727
"fmt"
2828
"io"
2929
"net/http"
30-
"strings"
3130
"time"
3231

3332
"golang.org/x/oauth2"
@@ -199,10 +198,25 @@ func (g *App) AppToken() ([]byte, error) {
199198
return token, nil
200199
}
201200

201+
// OAuthAppTokenSource adheres to the oauth2 TokenSource interface and returns a oauth2 token
202+
// by creating a JWT token.
203+
func (g *App) OAuthAppTokenSource() oauth2.TokenSource {
204+
return oauth2TokenSource(func() (*oauth2.Token, error) {
205+
jwt, err := g.AppToken()
206+
if err != nil {
207+
return nil, fmt.Errorf("failed to generate app token: %w", err)
208+
}
209+
210+
return &oauth2.Token{
211+
AccessToken: string(jwt),
212+
}, nil
213+
})
214+
}
215+
202216
// AccessToken calls the GitHub API to generate a new access token for this
203217
// application installation with the requested permissions and repositories.
204218
func (g *App) AccessToken(ctx context.Context, request *TokenRequest) (string, error) {
205-
if request.Repositories == nil {
219+
if request == nil || request.Repositories == nil {
206220
return "", fmt.Errorf("requested repositories cannot be nil, did you mean to use AccessTokenAllRepos to request all repos?")
207221
}
208222

@@ -214,6 +228,21 @@ func (g *App) AccessToken(ctx context.Context, request *TokenRequest) (string, e
214228
return g.githubAccessToken(ctx, requestJSON)
215229
}
216230

231+
// SelectedReposTokenSource returns a [TokenSource] that mints a GitHub token
232+
// with permissions on the selected repos.
233+
func (g *App) SelectedReposTokenSource(permissions map[string]string, repos ...string) TokenSource {
234+
return TokenSourceFunc(func(ctx context.Context) (string, error) {
235+
token, err := g.AccessToken(ctx, &TokenRequest{
236+
Permissions: permissions,
237+
Repositories: repos,
238+
})
239+
if err != nil {
240+
return "", fmt.Errorf("failed to get github access token for repos %q: %w", repos, err)
241+
}
242+
return token, nil
243+
})
244+
}
245+
217246
// AccessTokenAllRepos calls the GitHub API to generate a new access token for
218247
// this application installation with the requested permissions and all granted
219248
// repositories.
@@ -226,92 +255,64 @@ func (g *App) AccessTokenAllRepos(ctx context.Context, request *TokenRequestAllR
226255
return g.githubAccessToken(ctx, requestJSON)
227256
}
228257

258+
// AllReposTokenSource returns a [TokenSource] that mints a GitHub token with
259+
// permissions on all repos.
260+
func (g *App) AllReposTokenSource(permissions map[string]string) TokenSource {
261+
return TokenSourceFunc(func(ctx context.Context) (string, error) {
262+
token, err := g.AccessTokenAllRepos(ctx, &TokenRequestAllRepos{
263+
Permissions: permissions,
264+
})
265+
if err != nil {
266+
return "", fmt.Errorf("failed to get github access token for all repos: %w", err)
267+
}
268+
return token, nil
269+
})
270+
}
271+
229272
// githubAccessToken calls the GitHub API to generate a new access token with
230273
// provided JSON payload bytes.
231274
func (g *App) githubAccessToken(ctx context.Context, requestJSON []byte) (string, error) {
232275
appJWT, err := g.AppToken()
233276
if err != nil {
234-
return "", fmt.Errorf("error generating app jwt: %w", err)
277+
return "", fmt.Errorf("failed to generate github app jws: %w", err)
235278
}
236279
requestURL := fmt.Sprintf(g.accessTokenURLPattern, g.InstallationID)
237280

238281
requestReader := bytes.NewReader(requestJSON)
239282
req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL, requestReader)
240283
if err != nil {
241-
return "", fmt.Errorf("error creating http request for GitHub installation information: %w", err)
284+
return "", fmt.Errorf("failed to create http request: %w", err)
242285
}
243286
req.Header.Set("Accept", "application/vnd.github+json")
244287
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", appJWT))
245288

246289
res, err := g.httpClient.Do(req)
247290
if err != nil {
248-
return "", fmt.Errorf("error making http request for GitHub installation access token %w", err)
291+
return "", fmt.Errorf("failed to make http request: %w", err)
249292
}
250293
defer res.Body.Close()
251294

252295
b, err := io.ReadAll(io.LimitReader(res.Body, 64_000))
253296
if err != nil {
254-
return "", fmt.Errorf("error reading http response for GitHub installation access token %w", err)
297+
return "", fmt.Errorf("failed to read response body: %w", err)
255298
}
256299

257-
if res.StatusCode != http.StatusCreated {
258-
return "", fmt.Errorf("failed to retrieve token from GitHub - Status: %s - Body: %s", res.Status, string(b))
300+
if got, want := res.StatusCode, http.StatusCreated; got != want {
301+
return "", fmt.Errorf("invalid http response status (expected %d to be %d): %s", got, want, string(b))
259302
}
260303

261-
// GitHub will respond with a 201 when you send a request for an invalid combination,
262-
// e.g. 'issues':'write' for an empty repository list. This 201 comes with a response that is not actually JSON.
263-
// Attempt to parse the JSON to see if this is a valid token, if it is not
264-
// then respond with an error containing the
265-
// actual response from GitHub.
266-
tokenContent := map[string]any{}
267-
if err := json.Unmarshal(b, &tokenContent); err != nil {
268-
return "", fmt.Errorf("invalid access token from GitHub - Body: %s", string(b))
304+
// GitHub will respond with a 201 when you send a request for an invalid
305+
// combination, e.g. 'issues':'write' for an empty repository list. This 201
306+
// comes with a response that is not actually JSON. Attempt to parse the JSON
307+
// to see if this is a valid token, if it is not then respond with an error
308+
// containing the actual response from GitHub.
309+
var resp struct {
310+
Token string `json:"token"`
269311
}
270-
return string(b), nil
271-
}
272-
273-
// OAuthAppTokenSource adheres to the oauth2 TokenSource interface and returns a oauth2 token
274-
// by creating a JWT token.
275-
func (g *App) OAuthAppTokenSource() oauth2.TokenSource {
276-
return oauth2TokenSource(func() (*oauth2.Token, error) {
277-
jwt, err := g.AppToken()
278-
if err != nil {
279-
return nil, fmt.Errorf("failed to generate app token: %w", err)
280-
}
281-
282-
return &oauth2.Token{
283-
AccessToken: string(jwt),
284-
}, nil
285-
})
286-
}
287-
288-
// AllReposTokenSource returns a [TokenSource] that mints a GitHub token with
289-
// permissions on all repos.
290-
func (g *App) AllReposTokenSource(permissions map[string]string) TokenSource {
291-
return TokenSourceFunc(func(ctx context.Context) (string, error) {
292-
resp, err := g.AccessTokenAllRepos(ctx, &TokenRequestAllRepos{
293-
Permissions: permissions,
294-
})
295-
if err != nil {
296-
return "", fmt.Errorf("failed to get github access token for all repos: %w", err)
297-
}
298-
return parseAppTokenResponse(resp)
299-
})
300-
}
301-
302-
// SelectedReposTokenSource returns a [TokenSource] that mints a GitHub token
303-
// with permissions on the selected repos.
304-
func (g *App) SelectedReposTokenSource(permissions map[string]string, repos ...string) TokenSource {
305-
return TokenSourceFunc(func(ctx context.Context) (string, error) {
306-
resp, err := g.AccessToken(ctx, &TokenRequest{
307-
Permissions: permissions,
308-
Repositories: repos,
309-
})
310-
if err != nil {
311-
return "", fmt.Errorf("failed to get github access token for repos %q: %w", repos, err)
312-
}
313-
return parseAppTokenResponse(resp)
314-
})
312+
if err := json.Unmarshal(b, &resp); err != nil {
313+
return "", fmt.Errorf("failed to parse response as json: %w: %s", err, string(b))
314+
}
315+
return resp.Token, nil
315316
}
316317

317318
// generateAppJWT builds a signed JWT that can be used to communicate with
@@ -363,18 +364,3 @@ func parseRSAPrivateKeyPEM(data []byte) (*rsa.PrivateKey, error) {
363364
}
364365
return key, nil
365366
}
366-
367-
// parseAppTokenResponse parses the given JWT and returns the embedded token.
368-
func parseAppTokenResponse(data string) (string, error) {
369-
var resp struct {
370-
Token string `json:"token"`
371-
}
372-
373-
if err := json.NewDecoder(strings.NewReader(data)).Decode(&resp); err != nil {
374-
return "", fmt.Errorf("failed to parse json: %w", err)
375-
}
376-
if resp.Token == "" {
377-
return "", fmt.Errorf("no token in json response")
378-
}
379-
return resp.Token, nil
380-
}

0 commit comments

Comments
 (0)