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 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:
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.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.
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.
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.
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.
GitHub responds with an authentication token that can be used by the application to access data on behalf of the user.
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:
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.
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.
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.
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 (JWT) is a structured format that contains header, payload, and signature components. Let's take a look at the significance of these components:
Header - The header is a JSON string that typically contains the signing algorithm used, such as HMAC, SHA256, or RSA.
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.
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)
The following steps outline the process of generating a JWT:
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.
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.
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:
{ "alg": "RS256" }
The value of alg
contains the algorithm used for signing the JWT. Also, let's
say we have the following payload:
{ "username": "[email protected]", "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:
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):
NlAT6awp68dCEcFXbDeeLTzZekqUmB3f6kr3jkGSFmrKa5zvLmFGeraWba_fUuQLVhRtcXUPZbRR1DKnKH0HVf1rRDvOqezwbhe-hR1wlz6vZkHuPjtYSCLx_aybGm7dy2ijfTQwYd14cD9ZiMI5vf6XcDDfE7mkhu0ogCOnqR1v3KOEWJkMkvGBHfHKuf9FKYbWltHtUE6bAEO1orq0JayD8UNUKxdGkElXA7mkuIEexmBuieG9PJ2ow_uo05QCsqDvxlzOCMMIe7WdT7gmz4myiZ7lVuUcL1V2-Y1PJqWDyqDZbKNxd4X_CwW0RLOF1pw9S2URgybqHZFG0murNw
So the final JWT would be:
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.
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:
Split the JWT into the encoded header, the encoded payload and the signature, using the dot (.) used to separate the three components.
Decode the encoded header and the encoded payload using Base64 to get the header and payload JSON strings.
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.
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.
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:
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.
GitHub gets the approval of the user.
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.
Validate the JWT from the state
parameter. If the JWT is invalid, drop the
authorization process immediately.
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.
Ability to send additional data - The payload component of JWT can be used to send additional data such as user data, permissions data, etc.
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.
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.
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.
OAuth 2.0 - https://www.digitalocean.com/community/tutorials/an-introduction-to-oauth-2
GitHub OAuth Authorization process - https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps
Phishing Attack - https://www.imperva.com/learn/application-security/phishing-attack-scam/
If this blog was helpful, check out our full blog archive.