github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/api/checks/jwt.go (about) 1 // Copyright 2018 The WPT Dashboard Project. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package checks 6 7 import ( 8 "context" 9 "crypto/x509" 10 "encoding/json" 11 "encoding/pem" 12 "errors" 13 "fmt" 14 "io" 15 "net/http" 16 "time" 17 18 jwt "github.com/golang-jwt/jwt" 19 "github.com/google/go-github/v47/github" 20 "github.com/web-platform-tests/wpt.fyi/shared" 21 "golang.org/x/oauth2" 22 ) 23 24 func getGitHubClient(ctx context.Context, appID, installationID int64) (*github.Client, error) { 25 jwtClient, err := getJWTClient(ctx, appID, installationID) 26 if err != nil { 27 return nil, err 28 } 29 30 return github.NewClient(jwtClient), nil 31 } 32 33 // NOTE(lukebjerring): oauth2/jwt has incorrect field-names, and doesn't allow 34 // passing in an http.Client (for GitHub's Authorization header flow), so we 35 // are forced to copy a little code here :(. 36 func getJWTClient(ctx context.Context, appID, installation int64) (*http.Client, error) { 37 ss, err := getSignedJWT(ctx, appID) 38 if err != nil { 39 return nil, err 40 } 41 tokenSource := oauth2.StaticTokenSource( 42 &oauth2.Token{AccessToken: ss}, // nolint:exhaustruct // WONTFIX: AccessToken only required. 43 ) 44 oauthClient := oauth2.NewClient(ctx, tokenSource) 45 46 tokenURL := fmt.Sprintf("https://api.github.com/app/installations/%v/access_tokens", installation) 47 req, _ := http.NewRequestWithContext(context.Background(), http.MethodPost, tokenURL, nil) 48 req.Header.Set("Accept", "application/vnd.github.machine-man-preview+json") 49 resp, err := oauthClient.Do(req) 50 if err != nil { 51 return nil, fmt.Errorf("cannot fetch installation token: %w", err) 52 } 53 54 defer resp.Body.Close() 55 body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) 56 if err != nil { 57 return nil, fmt.Errorf("cannot fetch installation token: %w", err) 58 } 59 if c := resp.StatusCode; c < 200 || c > 299 { 60 // nolint:exhaustruct // TODO: Fix exhaustruct lint error. 61 // Investigate which error code should be returned here. 62 return nil, &oauth2.RetrieveError{ 63 Response: resp, 64 Body: body, 65 } 66 } 67 // tokenResponse is the JSON response body. 68 var tokenResponse struct { 69 AccessToken string `json:"token"` 70 ExpiresAt time.Time `json:"expires_at"` 71 } 72 if err := json.Unmarshal(body, &tokenResponse); err != nil { 73 return nil, fmt.Errorf("oauth2: cannot fetch token: %w", err) 74 } 75 // nolint:exhaustruct // WONTFIX: AccessToken only required. 76 token := &oauth2.Token{ 77 AccessToken: tokenResponse.AccessToken, 78 Expiry: tokenResponse.ExpiresAt, 79 } 80 81 return oauth2.NewClient(ctx, oauth2.StaticTokenSource(token)), nil 82 } 83 84 // nolint:lll // Keep hyperlink 85 // https://developer.github.com/apps/building-github-apps/authenticating-with-github-apps/#authenticating-as-a-github-app 86 func getSignedJWT(ctx context.Context, appID int64) (string, error) { 87 ds := shared.NewAppEngineDatastore(ctx, false) 88 secret, err := shared.GetSecret(ds, fmt.Sprintf("github-app-private-key-%v", appID)) 89 if err != nil { 90 return "", err 91 } 92 block, _ := pem.Decode([]byte(secret)) 93 if block == nil { 94 return "", errors.New("Failed to decode private key") 95 } 96 key, err := x509.ParsePKCS1PrivateKey(block.Bytes) 97 if err != nil { 98 return "", err 99 } 100 101 /* Create the jwt token */ 102 now := time.Now() 103 claims := &jwt.StandardClaims{ 104 IssuedAt: now.Unix(), 105 ExpiresAt: now.Add(time.Minute * 10).Unix(), 106 Issuer: fmt.Sprintf("%v", appID), 107 } 108 109 jwtToken := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) 110 111 return jwtToken.SignedString(key) 112 }