SERIES: A Guide to Developing GitHub Apps on .NET
In this post, I cover the steps required to create and sign a JSON Web Token, herein abbreviated as JWT, to authenticate a GitHub App built using .NET.
I want to state clearly up front that I’m learning as I go while experimenting with a hobby project to develop a GitHub App. Some of the approaches I use may not be the most efficient or even the most correct. This is not production-ready code, but hopefully, it is a good starting point.
The first thing we need to do is create a GitHub App over in the “Developer settings” GitHub Apps page on GitHub.com.
I won’t go through the specific details for creating the app here as they are slightly outside the scope of what I plan to cover. After you have completed any required fields and submitted the create GitHub App form, you will be prompted to create a private key. You can do this at any point but must complete this step to start authenticating.
After clicking the button to generate a private key, the key will be created, and a PEM file will be automatically downloaded. This file contains the RSA private key, which we’ll need for authentication. You should, of course, keep this file secure as it provides full access to any installations of the GitHub App. We’ll work with it locally for this example and during development, but you should strongly consider storing this somewhere secure, like Azure KeyVault.
With the file downloaded, we are ready to create a simple GitHub App. I’ll start with a .NET 8 console app for this example, using top-level statements.
We’ll begin by adding a NuGet package for a library that we’ll use to create the JWT to authenticate our requests to the GitHub REST API.
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.2.0" />
We can now get into the code. We’ll need several using directives for the namespaces we’ll be accessing.
using System.Diagnostics;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using Microsoft.IdentityModel.Tokens;
The next step is to create the signing credentials we’ll use to sign the JWT. The JWT must be signed using the RS256 algorithm. The code for this step is as follows:
const string pemFilePath = "C:\\dotnet-sample-app.2024-01-12.private-key.pem";
var keyText = await File.ReadAllTextAsync(pemFilePath);
var rsa = RSA.Create();
rsa.ImportFromPem(keyText);
var signingCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256);
In the above code, we define the path and filename of the PEM file holding our RSA private key. The code then reads the text from the PEM file. We create an instance of the RSA algorithm and then import the key from the PEM file text. We can then create an instance of the SigningCredentials we need.
The next step is to create a signed JWT to authenticate requests to the GitHub REST API.
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler { SetDefaultTimesOnTokenCreation = false };
var now = DateTime.UtcNow.AddSeconds(-60);
var jwt = jwtSecurityTokenHandler.CreateToken(new SecurityTokenDescriptor
{
Issuer = "123456",
Expires = now.AddMinutes(10),
IssuedAt = now,
SigningCredentials = signingCredentials
});
In the preceding code, we create a JwtSecurityTokenHandler that can be used to create JWTs. We set the SetDefaultTimesOnTokenCreation property to false as we’ll explicitly control the issued at and expires at times on the token. We store the current UTC date and time into a variable. We subtract 60 seconds to protect against clock drift between the computer running our code and the GitHub servers.
We call CreateToken on the handler, passing a SecurityTokenDescriptor that defines the properties of our token. The GitHub docs describe the required properties. We must include the issuer, which is our unique app ID that can be found on the GitHub App settings page. We set the expiry time for this token to 10 minutes (minus 60 seconds), the maximum allowed by GitHub. We set the ‘IssuedAt’ property using the value we assigned to the ‘now’ variable. Finally, we must attach our signing credentials. This method returns the JWT in its string form.
To conclude this example, we send a request to the GitHub REST API. We could opt to use a library such as OktoKit, which provides a typed client for programmatically interacting with the GitHub API. To demonstrate what this would do on our behalf, I’ll manually create a request.
var client = new HttpClient() { BaseAddress = new Uri("https://api.github.com") };
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", jwt);
client.DefaultRequestHeaders.UserAgent.ParseAdd("Sample-GitHubApp/1.0.0");
var response = await client.GetAsync("/app/installations");
Debug.Assert(response.IsSuccessStatusCode);
The above code creates a HttpClient instance, setting the base address to the REST API root URL. We then set two default headers on the client, which will be applied to all requests. The first adds a bearer authorization token with our JWT. The second adds a user agent. The GitHub API requires that all requests must include this header.
We then perform a GET request to the “/app/installations” path. The endpoint returns a list of any installations of the application. As this is a new app and has not been installed, this will be an empty array. As long as we get a 200 success status code, we can conclude that we have successfully authenticated with the API.
NOTE: Remember that the JWT will expire 9 minutes after creation, so for future requests, you may need to regenerate it.
Conclusion
That concludes this short post describing generating an application JWT from an RSA private key PEM file in .NET. We used the token to authenticate a request sent to the GitHub REST API. The next step would be to generate a token to start taking actions within a specific installation. That’s a topic for a future post.
Have you enjoyed this post and found it useful? If so, please consider supporting me:
One thought to “Authenticating a .NET GitHub App using a JSON Web Token (JWT)”
Comments are closed.