From 6191066b2e825a3cd25f92e5a703eaa7933f0572 Mon Sep 17 00:00:00 2001 From: Avinash Sridhar Date: Wed, 30 Apr 2025 12:50:48 -0400 Subject: [PATCH] feat(auth): enhance token validation to ensure association with GitHub application and improve error handling --- internal/auth/github.go | 67 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/internal/auth/github.go b/internal/auth/github.go index aac04b6..ce1daf6 100644 --- a/internal/auth/github.go +++ b/internal/auth/github.go @@ -2,6 +2,7 @@ package auth import ( + "bytes" "encoding/json" "errors" "fmt" @@ -65,14 +66,74 @@ func NewGitHubDeviceAuth(config GitHubOAuthConfig) *GitHubDeviceAuth { } } -// ValidateToken checks if a token has write:packages access to the required repository -// and verifies that the user matches the owner in the required repository +// ValidateToken validates if a GitHub token has the necessary permissions to access the required repository. +// It verifies the token owner matches the repository owner or is a member of the owning organization. +// It also verifies that the token was created for the same ClientID used to set up the authentication. +// Returns true if valid, false otherwise along with an error explaining the validation failure. func (g *GitHubDeviceAuth) ValidateToken(token string, requiredRepo string) (bool, error) { // If no repo is required, we can't validate properly if requiredRepo == "" { return false, fmt.Errorf("repository reference is required for token validation") } + // First, validate that the token is associated with our ClientID + tokenReq, err := http.NewRequest("GET", "https://api.github.com/applications/"+g.config.ClientID+"/token", nil) + if err != nil { + return false, err + } + + // The applications endpoint requires basic auth with client ID and secret + tokenReq.SetBasicAuth(g.config.ClientID, g.config.ClientSecret) + tokenReq.Header.Set("Accept", "application/vnd.github+json") + + // Create request body with the token + type tokenCheck struct { + AccessToken string `json:"access_token"` + } + + checkBody, err := json.Marshal(tokenCheck{AccessToken: token}) + if err != nil { + return false, err + } + + // POST instead of GET for security reasons per GitHub API + tokenReq, err = http.NewRequest("POST", "https://api.github.com/applications/"+g.config.ClientID+"/token", io.NopCloser(bytes.NewReader(checkBody))) + if err != nil { + return false, err + } + + tokenReq.SetBasicAuth(g.config.ClientID, g.config.ClientSecret) + tokenReq.Header.Set("Accept", "application/vnd.github+json") + tokenReq.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + tokenResp, err := client.Do(tokenReq) + if err != nil { + return false, err + } + defer tokenResp.Body.Close() + + // Check response - 200 means token is valid and associated with our app + // 404 means token is not associated with our app + if tokenResp.StatusCode != http.StatusOK { + return false, fmt.Errorf("token is not associated with this application (status: %d)", tokenResp.StatusCode) + } + + var tokenInfo TokenValidationResponse + tokenRespBody, err := io.ReadAll(tokenResp.Body) + if err != nil { + return false, err + } + + if err := json.Unmarshal(tokenRespBody, &tokenInfo); err != nil { + return false, err + } + + // Check if there's an error in the response + if tokenInfo.Error != "" { + return false, fmt.Errorf("token validation error: %s", tokenInfo.Error) + } + // Get the authenticated user userReq, err := http.NewRequest("GET", "https://api.github.com/user", nil) if err != nil { @@ -81,7 +142,7 @@ func (g *GitHubDeviceAuth) ValidateToken(token string, requiredRepo string) (boo userReq.Header.Set("Accept", "application/vnd.github+json") userReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - client := &http.Client{} + client = &http.Client{} userResp, err := client.Do(userReq) if err != nil { return false, err