@@ -27,7 +27,6 @@ import (
27
27
"fmt"
28
28
"io"
29
29
"net/http"
30
- "strings"
31
30
"time"
32
31
33
32
"golang.org/x/oauth2"
@@ -199,10 +198,25 @@ func (g *App) AppToken() ([]byte, error) {
199
198
return token , nil
200
199
}
201
200
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
+
202
216
// AccessToken calls the GitHub API to generate a new access token for this
203
217
// application installation with the requested permissions and repositories.
204
218
func (g * App ) AccessToken (ctx context.Context , request * TokenRequest ) (string , error ) {
205
- if request .Repositories == nil {
219
+ if request == nil || request .Repositories == nil {
206
220
return "" , fmt .Errorf ("requested repositories cannot be nil, did you mean to use AccessTokenAllRepos to request all repos?" )
207
221
}
208
222
@@ -214,6 +228,21 @@ func (g *App) AccessToken(ctx context.Context, request *TokenRequest) (string, e
214
228
return g .githubAccessToken (ctx , requestJSON )
215
229
}
216
230
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
+
217
246
// AccessTokenAllRepos calls the GitHub API to generate a new access token for
218
247
// this application installation with the requested permissions and all granted
219
248
// repositories.
@@ -226,92 +255,64 @@ func (g *App) AccessTokenAllRepos(ctx context.Context, request *TokenRequestAllR
226
255
return g .githubAccessToken (ctx , requestJSON )
227
256
}
228
257
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
+
229
272
// githubAccessToken calls the GitHub API to generate a new access token with
230
273
// provided JSON payload bytes.
231
274
func (g * App ) githubAccessToken (ctx context.Context , requestJSON []byte ) (string , error ) {
232
275
appJWT , err := g .AppToken ()
233
276
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 )
235
278
}
236
279
requestURL := fmt .Sprintf (g .accessTokenURLPattern , g .InstallationID )
237
280
238
281
requestReader := bytes .NewReader (requestJSON )
239
282
req , err := http .NewRequestWithContext (ctx , http .MethodPost , requestURL , requestReader )
240
283
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 )
242
285
}
243
286
req .Header .Set ("Accept" , "application/vnd.github+json" )
244
287
req .Header .Set ("Authorization" , fmt .Sprintf ("Bearer %s" , appJWT ))
245
288
246
289
res , err := g .httpClient .Do (req )
247
290
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 )
249
292
}
250
293
defer res .Body .Close ()
251
294
252
295
b , err := io .ReadAll (io .LimitReader (res .Body , 64_000 ))
253
296
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 )
255
298
}
256
299
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 ))
259
302
}
260
303
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"`
269
311
}
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
315
316
}
316
317
317
318
// generateAppJWT builds a signed JWT that can be used to communicate with
@@ -363,18 +364,3 @@ func parseRSAPrivateKeyPEM(data []byte) (*rsa.PrivateKey, error) {
363
364
}
364
365
return key , nil
365
366
}
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