In a world full of web services, APIs and clouds, authentication and authorisation needs to be rethought. Basic methods such as Kerberos are no longer applicable, or only partially applicable, to these workloads. Imagine having to expose your domain controllers to the Internet. Holy cow, what a dangerous world that would be!
Modern Auth
This is why we have “modern authentication” like SAML or OAuth. Some would say “what about OpenID Connect?”, correct, but this is just a modern form of OAuth. These protocols are designed to best fit the modern world of new workloads. Basically they can be thought of as a combination of authentication and authorisation methods. These are required between a client and a server in a connection and can be enriched with security mechanisms. There are many more use cases, here I only cover the basics.
OAuth
This article is about OAuth only. OAuth does not work with a username and password as Basic Auth does, instead it uses JSON Web Token (JWT). So I need someone to issue a JWT for me, this actor is called an “Identity Provider” (IdP). The advantage of this solution is that the service itself does not need to store the username and password, this is all done by the IdP. The following image illustrates this in a simplified way.
JWT Structure
A JWT consists of a combination of a header, a payload and a signature. Each part has its own function, the combination of key and value within the header or payload is called a claim. The signature ensures the integrity of the data in the header and payload during transmission.
Enough theory, let’s have a look at a sample JWT (you can reproduce all the steps with this token)
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL2lkcC5jb2xsZnVzZS5jb20vIiwiaWF0IjoxNjgyNjgyOTk5LCJleHAiOjE3MTQyMTg5OTksImF1ZCI6Ind3dy5jb2xsZnVzZS5jb20iLCJzdWIiOiJqd3RAY29sbGZ1c2UuY29tIiwiR2l2ZW5OYW1lIjoiSGFydmV5IiwiU3VybmFtZSI6IlNwZWN0ZXIiLCJFbWFpbCI6ImhhcnZleS5zcGVjdGVyQGNvbGxmdXNlLmNvbSIsIlJvbGUiOlsiQWRtaW5pc3RyYXRvciIsIlByb2plY3QgQWRtaW5pc3RyYXRvciJdfQ.OvAqIRhwJKYq3xpnfL7Cm6vkz0y8jQm3SoNRdXbMil8
Didn’t we just talk about JSON? Correct, but in order for the JSON structure to be properly transmitted over the Internet, it is encoded using Base64url. Note that this is not an encryption and can be decoded back to its origin by anyone. The easiest way to decode it is to use online tools such as https://www.base64decode.org. If you are sure that there is a JWT behind the encoding (note: you can recognise a JWT by the first two letters “ey”, because they mirror the “{” character in Base64url, and this is always the beginning of a JWT), you can decode it directly using websites such as https://jwt.ms or https://jwt.io.
If we use one of these sites and decode the token, it should look like this
{
"typ": "JWT",
"alg": "HS256"
}.{
"iss": "https://idp.collfuse.com/",
"iat": 1682682999,
"exp": 1714218999,
"aud": "www.collfuse.com",
"sub": "[email protected]",
"GivenName": "Harvey",
"Surname": "Specter",
"Email": "[email protected]",
"Role": [
"Administrator",
"Project Administrator"
]
}.[Signature]
Now the header and payload are readable and the JSON structure is visible. Let’s first look at the header and the possible combinations.
JWT Header
{
"typ": "JWT",
"alg": "HS256"
}
The header can contain different claims depending on the IdP, but the most common and required by RFC are “typ” and “alg“, which stands for algorithm. The “alg” describes the method used to create the signature. The most common (but not definitive) methods are “HS256” and “RS256“. The difference is that “HS256” means that the signature was created using a passphaser known only to the issiuer, whereas “RS256” means that the signature was created using a certificate. The advantage of “RS256” is that the target system can verify the signature itself, whereas “HS256” cannot because the passphrase is not known. But let’s not go too deep yet, we’ll take a closer look at how to verify the signature later.
JWT Payload
{
"iss": "https://idp.collfuse.com/",
"iat": 1682682999,
"exp": 1714218999,
"aud": "www.collfuse.com",
"sub": "[email protected]",
"GivenName": "Harvey",
"Surname": "Specter",
"Email": "[email protected]",
"Role": [
"Administrator",
"Project Administrator"
]
}
The payload can contain almost any information, but the RFC also specifies which claims should be included. However, let’s focus on the most important ones, which are iss (issuer), iat (issued at), nbf (not before), exp (expiration time) and aud (audience).
- iss IdP which issued the token
- iat creation timestamp
- nbf timestamp for first use
- exp expiration timestamp
- aud target service
In the payload above you can also see the “Role” claim. This is a common way of obtaining the necessary rights in the target service. “Harvey Specter” would have access to the resources of “Project Administrator” and would be the administrator of the application (or the sub-areas assigned to this role) through the “Administrator” claim.
JWT Signature
Since you already know the header and payload, you may have noticed that it would be quite easy for an attacker to make changes to these parts of the token. They could simply replace the role “Project Administrator” with “Personal Administrator” to gain elevated privileges, or change the user ID from “Harvey Specter” to “James Bond” to read another user’s context. This would of course be less secure, hence the signature.
The signature is the control element of the whole construct, it can be used to check if claims have been added, removed or changed since issuance. In short, the integrity of the token can be verified using the signature.
The signature data consists of the base64web version of the header and the payload, concatenated and separated by a dot. Sounds more complicated than it is, here is an example in PowerShell of how to generate the signature for a HS256 token.
# token data
$jwt_header = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9"
$jwt_payload = "eyJpc3MiOiJodHRwczovL2lkcC5jb2xsZnVzZS5jb20vIiwiaWF0IjoxNjgyNjgyOTk5LCJleHAiOjE3MTQyMTg5OTksImF1ZCI6Ind3dy5jb2xsZnVzZS5jb20iLCJzdWIiOiJqd3RAY29sbGZ1c2UuY29tIiwiR2l2ZW5OYW1lIjoiSGFydmV5IiwiU3VybmFtZSI6IlNwZWN0ZXIiLCJFbWFpbCI6ImhhcnZleS5zcGVjdGVyQGNvbGxmdXNlLmNvbSIsIlJvbGUiOlsiQWRtaW5pc3RyYXRvciIsIlByb2plY3QgQWRtaW5pc3RyYXRvciJdfQ"
# signature
$sig_secret = "collfuse"
$sig_data = $header + "." + $payload
$hmac_sha256 = New-Object System.Security.Cryptography.HMACSHA256
$hmac_sha256.key = [Text.Encoding]::ASCII.GetBytes($secret)
$jwt_sig = $hmac_sha256.ComputeHash([Text.Encoding]::ASCII.GetBytes($sig_data))
$jwt_sig = [Convert]::ToBase64String($jwt_sig)
# jwt
$token = $sig_data + "." + $jwt_sig
$token
As you can see, in this example we create the signature with the super secret key “collfuse”. Finally, all the elements are put together and there you have it, the finished JWT. Pretty simple, isn’t it?
JWT Verification
Now you know that only RS256 tokens can be independently verified, HS256 tokens cannot because no one will give you their passphaser. So to verify an RS256 token, the application needs the public key of the certificate used to create the signature. This information should be made publicly available by the issuer of the token (no obligation). The application can retrieve this data as shown in the simplified image below.
- 1) The client sends the JWT as HTTP authentication header
- 2) The iss claim is extracted and the URL followed by “/.well-known/openid-configuration” is called
- 3) The IdP returns its openid configuration
- 4) the JSON Web Key Sets (jwks) endpoint from is extracted and called
- 5) The idP reports back its key set and the application checks the signature
- 6) If the signature is ok (in addition to other tests), the application returns the desired data
Conclusion
JWTs are quite powerful and very easy to implement. They meet the requirements of the new workflows in terms of the integrity and reliability of a login. However, it should also be noted that if the frameworks used to verify the tokens are not used correctly or are even designed incorrectly, the tokens will very quickly provide high-grade vulnerabilities.
It is always recommended to use frameworks or SDKs for token solutions rather than proprietary developments. This is not my opinion. If you know what you are doing, you can tailor it to your needs and build a secure solution that can withstand the attacks in the following posts.
Finally, here is a simple PHP code to verify your own issued HS256 tokens. The passphaser “collfuse” and the audiance “https://api.collfuse.com” have to be adapted.
function token_verification($access_token, $approle) {
# split auth header
list($token_type, $token_data) = explode(" ", $access_token, 2);
# verify if header is bearer
if (!strcasecmp($token_type, "Bearer") == 0) {
echo HTTP_Response(401); exit; }
# split to header, payload and signature
list($jwt_header, $jwt_payload, $jwt_signature) = explode(".", $token_data);
# decode body
$jwt_payload_data = json_decode (base64url_decode($jwt_payload));
$jwt_auth = $jwt_payload_data->{'auth'};
$jwt_iat = $jwt_payload_data->{'iat'};
$jwt_exp = $jwt_payload_data->{'exp'};
$jwt_roles = $jwt_payload_data->{'roles'};
# verify if jwt is expired
$jwt_exp_verify = strtotime('now');
if ($jwt_exp_verify > $jwt_exp) {
echo HTTP_Response(401); exit; }
# verify if auth is api.collfuse.com
if ($jwt_auth != 'https://api.collfuse.com') {
echo HTTP_Response(401); exit; }
# verify token signature
$jwt_data = "$jwt_header.$jwt_payload";
$jwt_signature_verify = base64url_encode(hash_hmac('sha256', $jwt_data, "collfuse""));
if ($jwt_signature_verify != $jwt_signature) {
echo HTTP_Response(401); exit; }
# verify if required app roles present
$app_roles = explode (";", $approle);
if (in_array("Administrator", $jwt_roles)) {
// return verification success
$success_code = array('status' => '200','reason' => 'token successfully veriefied');
return $success_code;
} else {
echo HTTP_Response(401); exit; }
}
That’s it so far, stay tuned and see you soon!
** midjourney string “A middle school student programming python coding language on a very thin desktop screen, 4k, clean illustration, beautiful colors, cinematic lighting –ar 1:2 –q 2”