Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[QUESTION] How to use with GitHub Apps? #163

Open
LarryBarker opened this issue Jun 9, 2024 · 1 comment
Open

[QUESTION] How to use with GitHub Apps? #163

LarryBarker opened this issue Jun 9, 2024 · 1 comment

Comments

@LarryBarker
Copy link

Hey there, thank you for sharing this package - it has been tremendously helpful! I'm wondering if you can provide some general guidance or help answer some questions about how to integrate a GitHub App with my Laravel app?

For context, I was using Laravel Socialite to provide oauth authentication, and then piggybacking off this and using the oauth token to work with my users pull requests, comments, etc. and based on their feedback, they prefer these actions to come from the app itself, instead of their account. For example, when my app leaves a comment on their PR, it appears as if the user left the comment, instead of my app.

I have started researching and testing GitHub Apps, and have run into a couple issues and would greatly appreciate any feedback you may have.

I do have a working proof of concept, although it seems a bit "hacky" and I want to see if you have any recommendations on a better approach? So here's what I'm doing currently:

  1. I found this doc in the KnpLaps repo: https://github.com/KnpLabs/php-github-api/blob/v3.14.0/doc/security.md#authenticating-as-an-integration
  2. I am basically using this to generate my JWT token, and then using the same package to generate an installation token: https://github.com/KnpLabs/php-github-api/blob/v3.14.0/doc/apps.md#create-a-new-installation-token
  3. Next, using the installation token, I am using Laravel's HTTP facade to make a request, rather than your package:
$builder = new Builder();
    $github = new Client($builder, 'machine-man-preview');
    $config = Configuration::forSymmetricSigner(
        new Sha256(),
        InMemory::file('/Users/larry/Herd/glimpse/private-key.pem')
    );

    $now = new \DateTimeImmutable();
    $jwt = $config->builder(ChainedFormatter::withUnixTimestampDates())
        ->issuedBy('914264')
        ->issuedAt($now)
        ->expiresAt($now->modify('+1 minute'))
        ->getToken($config->signer(), $config->signingKey());

    $github->authenticate($jwt->toString(), null, AuthMethod::JWT);
    $token = $github->api('apps')->createInstallationToken(51661655);


    $response = Http::withToken($token['token'])
                ->post('https://api.github.com/repos/'...);

I would love to continue using your package if possible, just not sure how to do so at this point?

I have tried using other connection methods, including app, private, and jwt but continue to face the Bad credentials error from GitHub.

Again, thank you for your work here, and I look forward to your reply in either case 🙏

@ejntaylor
Copy link

ejntaylor commented Dec 10, 2024

I've got this working. Main thing I found is to fetch the JWT but then use 'token' in the default connection (rather than application or JWT).

Sorry to bombard with code in a comment but I thought that would help!

In my github.php i set

[
    'default' => 'main',
    'connections' => [

        'main' => [
            'method' => 'token',
            'token' => '', // We'll set this dynamically
        ],

        'jwt' => [
            'method' => 'jwt',
            'token' => '', // We'll set this dynamically
        ],
        
        ...
 ]

and then had some files like so

<?php

namespace App\Domain\GitHub\Auth;

use Github\AuthMethod;
use Github\Client;
use GrahamCampbell\GitHub\GitHubManager;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;

class ConnectionManager
{
    private GitHubManager $github;

    private JwtService $jwtService;

    private const CACHE_KEY = 'github_installation_token';

    public function __construct(GitHubManager $github, JwtService $jwtService)
    {
        $this->github = $github;
        $this->jwtService = $jwtService;
    }

    public function getConnection(): Client
    {
        $token = $this->getInstallationToken();

        $client = $this->github->connection('main');
        $client->authenticate($token, null, AuthMethod::ACCESS_TOKEN);

        return $client;
    }

    public function getInstallationToken(): string
    {
        return Cache::remember(self::CACHE_KEY, 55, function () {
            $jwt = $this->jwtService->generateJwt();

            $response = Http::withToken($jwt)
                ->withHeaders([
                    'Accept' => 'application/vnd.github+json',
                    'X-GitHub-Api-Version' => '2022-11-28',
                ])
                ->post(
                    'https://api.github.com/app/installations/'.
                    config('services.github.installation_id').
                    '/access_tokens'
                );

            if (! $response->successful()) {
                throw new \RuntimeException('Failed to get installation token: '.$response->body());
            }

            return $response->json()['token'];
        });
    }

    // Additional helper methods for direct HTTP calls
    public function makeRequest(string $method, string $url, ?array $data = null)
    {
        return Http::withToken($this->getInstallationToken())
            ->withHeaders([
                'Accept' => 'application/vnd.github+json',
                'X-GitHub-Api-Version' => '2022-11-28',
            ])
            ->{$method}($url, $data);
    }
}

and

<?php

namespace App\Domain\GitHub\Auth;

use Firebase\JWT\JWT;
use UnexpectedValueException;

class JwtService
{
    public function generateJwt(): string
    {
        $privateKey = $this->formatPrivateKey(config('services.github.private_key'));

        $payload = [
            'iat' => time() - 60,        // Issued 60 seconds ago
            'exp' => time() + 600,       // Expires in 10 minutes
            'iss' => config('services.github.client_id'),  // Using client_id instead of app_id
            'alg' => 'RS256',             // Required by GitHub
        ];

        try {
            return JWT::encode($payload, $privateKey, 'RS256');
        } catch (\Exception $e) {
            throw new UnexpectedValueException(
                'Failed to generate JWT token. Error: '.$e->getMessage()
            );
        }
    }

    private function formatPrivateKey(string $key): string
    {
        // If key doesn't start with the header, something is wrong
        if (! str_contains($key, '-----BEGIN RSA PRIVATE KEY-----')) {
            throw new UnexpectedValueException('Invalid private key format');
        }

        // Replace literal "\n" strings with actual newlines
        $key = str_replace('\n', "\n", $key);

        // Ensure key has proper line breaks
        $key = trim($key);

        // If key is all on one line, add proper formatting
        if (! str_contains($key, "\n")) {
            $key = "-----BEGIN RSA PRIVATE KEY-----\n".
                chunk_split(str_replace(
                    ['-----BEGIN RSA PRIVATE KEY-----', '-----END RSA PRIVATE KEY-----'],
                    '',
                    $key
                ), 64, "\n").
                '-----END RSA PRIVATE KEY-----';
        }

        return $key;
    }
}

and then in my service classes I'd call the client with

$this->githubClient = $connectionManager->getConnection();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants