blob: be1854dcc29d41392164b3b8f8b1376c05a11756 [file]
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// This script lists GitHub App installations and tests the token refresh flow.
//
// Usage:
//
// GITHUB_APP_ID=123456 GITHUB_APP_PEM="$(cat private-key.pem)" go run ./plugins/github/token/cmd/test_refresh/
//
// Or if the key is in a file:
//
// GITHUB_APP_ID=123456 GITHUB_APP_PEM_FILE=/path/to/private-key.pem go run ./plugins/github/token/cmd/test_refresh/
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/golang-jwt/jwt/v5"
)
type installation struct {
ID int `json:"id"`
Account struct {
Login string `json:"login"`
} `json:"account"`
AppID int `json:"app_id"`
}
type installationToken struct {
Token string `json:"token"`
ExpiresAt time.Time `json:"expires_at"`
}
func main() {
appID := os.Getenv("GITHUB_APP_ID")
if appID == "" {
fatal("GITHUB_APP_ID env var is required")
}
pemData := os.Getenv("GITHUB_APP_PEM")
if pemData == "" {
pemFile := os.Getenv("GITHUB_APP_PEM_FILE")
if pemFile == "" {
fatal("Set GITHUB_APP_PEM (contents) or GITHUB_APP_PEM_FILE (path)")
}
data, err := os.ReadFile(pemFile)
if err != nil {
fatal("failed to read PEM file: %v", err)
}
pemData = string(data)
}
// Step 1: Create a JWT signed with the app's private key
fmt.Println("=== Step 1: Creating JWT from App ID and private key ===")
jwtToken, err := createJWT(appID, pemData)
if err != nil {
fatal("failed to create JWT: %v", err)
}
fmt.Printf("JWT created (first 20 chars): %s...\n\n", jwtToken[:20])
// Step 2: List installations
fmt.Println("=== Step 2: Listing installations for this app ===")
installations, err := listInstallations(jwtToken)
if err != nil {
fatal("failed to list installations: %v", err)
}
if len(installations) == 0 {
fatal("no installations found for this app")
}
for _, inst := range installations {
fmt.Printf(" Installation ID: %d Account: %s\n", inst.ID, inst.Account.Login)
}
fmt.Println()
// Step 3: Get an installation token for the first installation
inst := installations[0]
fmt.Printf("=== Step 3: Minting installation token for %s (ID: %d) ===\n", inst.Account.Login, inst.ID)
token1, err := getInstallationToken(jwtToken, inst.ID)
if err != nil {
fatal("failed to get installation token: %v", err)
}
fmt.Printf("Token 1: %s... Expires: %s\n\n", token1.Token[:10], token1.ExpiresAt.Format(time.RFC3339))
// Step 4: Make an API call with the token to verify it works
fmt.Println("=== Step 4: Verifying token works (GET /installation/repositories) ===")
err = verifyToken(token1.Token)
if err != nil {
fatal("token verification failed: %v", err)
}
fmt.Printf("Token is valid and working.\n\n")
// Step 5: Simulate the refresh flow — mint a second token (as our refreshFn would)
fmt.Println("=== Step 5: Simulating token refresh (minting a second token) ===")
jwtToken2, err := createJWT(appID, pemData)
if err != nil {
fatal("failed to create second JWT: %v", err)
}
token2, err := getInstallationToken(jwtToken2, inst.ID)
if err != nil {
fatal("failed to get second installation token: %v", err)
}
fmt.Printf("Token 2: %s... Expires: %s\n", token2.Token[:10], token2.ExpiresAt.Format(time.RFC3339))
if token1.Token == token2.Token {
fmt.Println("Note: Both tokens are identical (GitHub may cache short-lived tokens)")
} else {
fmt.Println("Tokens are different — refresh produced a new token.")
}
fmt.Println()
// Step 6: Verify the new token works
fmt.Println("=== Step 6: Verifying refreshed token works ===")
err = verifyToken(token2.Token)
if err != nil {
fatal("refreshed token verification failed: %v", err)
}
fmt.Println("Refreshed token is valid and working.")
fmt.Println("\n=== All steps passed. The token refresh flow works correctly. ===")
fmt.Printf("\nFor reference, your Installation ID is: %d\n", inst.ID)
}
func createJWT(appID, pemData string) (string, error) {
privateKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(pemData))
if err != nil {
return "", fmt.Errorf("invalid PEM key: %w", err)
}
now := time.Now().Unix()
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
"iat": now,
"exp": now + (10 * 60), // 10 minutes
"iss": appID,
})
signed, err := token.SignedString(privateKey)
if err != nil {
return "", fmt.Errorf("failed to sign JWT: %w", err)
}
return signed, nil
}
func listInstallations(jwtToken string) ([]installation, error) {
req, err := http.NewRequest("GET", "https://api.github.com/app/installations", nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+jwtToken)
req.Header.Set("Accept", "application/vnd.github+json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != 200 {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
var result []installation
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
return result, nil
}
func getInstallationToken(jwtToken string, installationID int) (*installationToken, error) {
url := fmt.Sprintf("https://api.github.com/app/installations/%d/access_tokens", installationID)
req, err := http.NewRequest("POST", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+jwtToken)
req.Header.Set("Accept", "application/vnd.github+json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
var token installationToken
if err := json.Unmarshal(body, &token); err != nil {
return nil, fmt.Errorf("failed to parse token response: %w", err)
}
return &token, nil
}
func verifyToken(token string) error {
req, err := http.NewRequest("GET", "https://api.github.com/installation/repositories?per_page=1", nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/vnd.github+json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
fmt.Printf(" HTTP 200 OK (X-RateLimit-Remaining: %s)\n", resp.Header.Get("X-RateLimit-Remaining"))
return nil
}
func fatal(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, "FATAL: "+format+"\n", args...)
os.Exit(1)
}