How to use JWT to secure your GitHub OAuth callback endpoint

Jagannath Bhat

By Jagannath Bhat

on March 7, 2023

JSON Web Tokens (JWTs) have become a popular way to manage user authentication and authorization. In this blog, we will explore how to use JWTs in the GitHub OAuth process, including how to encode additional parameters in the JWT to improve the security and functionality of your GitHub OAuth integration. Let's start by looking into what OAuth is.

OAuth 2.0

OAuth 2.0 is an authorization framework that enables an application to access data from a server on behalf of a user. For example, OAuth enables applications to access data from Google, Facebook, GitHub, etc., on behalf of users having accounts in that service.

Let's say you have an application that requires access to a user's data on GitHub. The following are the steps involved in authorizing your application with GitHub:

  1. The application requests authorization from GitHub. The following are some of the parameters this request should contain:

    • client_id - This is an ID used by GitHub for identifying the application. You need to register your application with GitHub to get this ID.
    • redirect_uri - The URI to which GitHub should send back a request once the authorization is approved.
    • login - This is a username on GitHub. The application requires access to data on behalf of the user with this username.
    • state - This should ideally be a string of random characters. Store this string in memory because it will be used in another step. This parameter is optional but highly recommended. We'll look into why later.
  2. GitHub then asks the user to grant the authorization. The user might be prompted to log in to GitHub if they have not already logged in. Then GitHub displays information on the application and lists all data the application wants to access. If the user denies authorization, the process ends here. If the user approves, we move on to the next step.

  3. GitHub sends a request to the application through the URI passed as redirect_uri in the first step. This request will contain a code parameter that serves as a temporary authorization code and a state parameter.

  4. The OAuth process must be dropped immediately if the value of the state parameter from the previous step does not match the random string stored in memory in step 1. This ensures that the OAuth process was initiated by your application. We'll look more into this later.

  5. The application should send another request to GitHub to generate a permanent authentication token. This request should contain the code sent by GitHub in step 3.

  6. GitHub responds with an authentication token that can be used by the application to access data on behalf of the user.

GitHub OAuth process

The state parameter

The state parameter plays a crucial role in ensuring that the GitHub OAuth process was initiated by your application or a trusted source. An unauthorized third party could pose as your application by using your client_id. The value of client_id is not exactly a secret. GitHub treats any OAuth process initiated using your ID as an OAuth process started by your application.

Let's see how the OAuth process would play out when initiated by an unauthorized third party:

  1. Third-party requests authorization from GitHub. The third party would pass your ID as client_id. This would lead GitHub to believe the request is from your application. The third party may or may not pass a state parameter.

  2. GitHub then asks the user to grant the authorization. If the third party uses one of their own accounts, they could grant the authorization. The third party could also convince a user to grant permission using social engineering. For example, they could perform a phishing attack using an email designed to trick the user into believing that the email was from your application.

  3. GitHub sends a request to the application through the URI passed as redirect_uri in the first step. This request would contain the code parameter, and the state parameter if it was passed in step 1.

  4. It would be clear that the process was not initiated by your application if the state parameter is missing. Even if there was a state parameter, it would not be found in memory. This is because the state parameter was generated by a third party and not your application. Only those state parameters generated by your application will be found in your memory.

JSON Web Tokens

JSON Web Tokens (JWT) is a structured format that contains header, payload, and signature components. Let's take a look at the significance of these components:

  1. Header - The header is a JSON string that typically contains the signing algorithm used, such as HMAC, SHA256, or RSA.

  2. Payload - The payload is a JSON string that contains claims and additional data. Claims can be used to enforce security constraints and validate the authenticity of the token and its contents. For example, a claims can specify the token expiration time. The expiration time, most commonly represented by exp, represents the date and time after which the token will no longer be considered valid.

  3. Signature - The signature is used to verify that the sender of the JWT is who it says it is and to ensure that the token has not been tampered with along the way. Just like the hand signature of a person, it is hard to forge a JWT signature. (Digital signatures are significantly harder to forge compared to a person's hand signature)

Generating a JWT

The following steps outline the process of generating a JWT:

  1. The header and the payload of the JWT are first encoded using Base64 encoding. So now we have two strings - the encoded header and the encoded payload.

  2. The encoded header and the encoded payload are concatenated into a single string, with dots (.) separating each part. The concatenated string is signed using the signing algorithm specified in the header. The signing algorithm uses a secret key, that is known only to the issuer (your application), which ensures that only the issuer can generate a valid signature. The result of the signing process is a signature, which is also a string.

  3. The encoded header, the encoded payload, and the signature are concatenated into a single string, with dots (.) separating each part. The resulting string is the JWT, which can be transferred securely between parties.

For example, let's say we have the following header:

1{ "alg": "RS256" }

The value of alg contains the algorithm used for signing the JWT. Also, let's say we have the following payload:

1{ "username": "sam@example.com", "exp": 1676263763 }

The username is data that needs to be transferred. exp contains the expiration time claim of the JWT.

When both these components are encoded, we get:

  • Header - "eyJhbGciOiJSUzI1NiJ9"
  • Payload - "eyJ1c2VyIjoic2FtQGV4YW1wbGUuY29tIiwiZXhwIjoxNjc2MjYzNzYzfQ"

When these are signed using the RS256 algorithm and a secret key, a signature is produced. The following signature was generated using the RS256 algorithm and a secret key (which will remain secret):

1NlAT6awp68dCEcFXbDeeLTzZekqUmB3f6kr3jkGSFmrKa5zvLmFGeraWba_fUuQLVhRtcXUPZbRR1DKnKH0HVf1rRDvOqezwbhe-hR1wlz6vZkHuPjtYSCLx_aybGm7dy2ijfTQwYd14cD9ZiMI5vf6XcDDfE7mkhu0ogCOnqR1v3KOEWJkMkvGBHfHKuf9FKYbWltHtUE6bAEO1orq0JayD8UNUKxdGkElXA7mkuIEexmBuieG9PJ2ow_uo05QCsqDvxlzOCMMIe7WdT7gmz4myiZ7lVuUcL1V2-Y1PJqWDyqDZbKNxd4X_CwW0RLOF1pw9S2URgybqHZFG0murNw

So the final JWT would be:

Screenshot of decoded JWT

The image above was captured from the decoding tool in jwt.io.

JWT is basically a Base64 encoded string with a signature attached to it. Having that signature component makes JWT a secure format for transferring data. It is possible to simply encode data with Base64 and transfer that string. From the example above, transferring the encoded payload "eyJ1c2VyIjoic2FtQGV4YW1wbGUuY29tIiwiZXhwIjoxNjc2MjYzNzYzfQ" can also get the data to the other party. However, the party that receives the data have no way of ensuring that the data was sent by a trusted source and that the data was not tampered with along the way.

Verifying JWT tokens

Anyone who has the secret key used to sign a JWT, can verify the integrity of the JWT. The steps involved in verifying the signature of a JSON Web Token (JWT) are:

  1. Split the JWT into the encoded header, the encoded payload and the signature, using the dot (.) used to separate the three components.

  2. Decode the encoded header and the encoded payload using Base64 to get the header and payload JSON strings.

  3. The application recreates the signature by signing the encoded header and the encoded payload using the signing algorithm in the header and the secret key. If the signature created in this step is the same as the signature in the JWT, the JWT is valid.

  4. The application validates the claims in the payload if there are any.

It can be verified that the JWT was generated by your application and has not been tampered with, if the signature is valid. If the JWT header or payload was tampered with, the signature produced while verifying would be different from the one in the JWT.

Using JWT for the state parameter

We can generate a JWT token and pass that as the state parameter in the GitHub OAuth authorizing process. Here's how the process will be different when using JWT:

  1. The application requests authorization from GitHub. This time the state parameter will be a JWT signed using a secure algorithm and a secret key. The JWT need not be stored in memory.

  2. GitHub gets the approval of the user.

  3. GitHub sends a request to the application through the URI passed as redirect_uri in the first step. This request will contain the code and the state parameters. Here, the state parameter would be a JWT.

  4. Validate the JWT from the state parameter. If the JWT is invalid, drop the authorization process immediately.

Advantages of using JWT for the state parameter

  1. No Storage requirement - When using a random string for the state parameter, that string has to be stored, so that it can be used later for verification. However, JWTs can be verified without storing them once they are generated.

  2. Ability to send additional data - The payload component of JWT can be used to send additional data such as user data, permissions data, etc.

  3. Security - Using JWT can ensure that the OAuth process was initiated by your application or a trusted source. In addition, JWT also ensures the integrity of the data. This means that we can ensure that the payload in the JWT has not been tampered with.

  4. Flexibility - The claims in the payload component of JWT can be used further enhance the security of the JWT by implementing custom security checks based on the claims in the payload.

  5. Standardization - JWT is a widely used format for transferring data. Using JWT would be beneficial when there are different applications and services involved in the OAuth process.

References

  1. OAuth 2.0 - https://www.digitalocean.com/community/tutorials/an-introduction-to-oauth-2

  2. GitHub OAuth Authorization process - https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps

  3. Phishing Attack - https://www.imperva.com/learn/application-security/phishing-attack-scam/

Stay up to date with our blogs. Sign up for our newsletter.

We write about Ruby on Rails, ReactJS, React Native, remote work,open source, engineering & design.