I'm trying to validate access tokens against at_hash. Token header is like this
{
"typ": "JWT",
"alg": "RS256",
"x5t": "MclQ7Vmu-1e5_rvdSfBShLe82eY",
"kid": "MclQ7Vmu-1e5_rvdSfBShLe82eY"
}
How do I get from my access token to the Base64 encoded at_hash claim value that is in the id token? Is there an online tool that could help me with this? Is SHA256 hash calculator not a correct tool for this?
Thanks
at_hash OPTIONAL. Access Token hash value. Its value is the base64url encoding of the left-most half of the hash of the octets of the ASCII representation of the access_token value, where the hash algorithm used is the hash algorithm used in the alg Header Parameter of the ID Token's JOSE Header.
There are two ways to verify a token: locally or remotely with Okta. The token is signed with a JSON Web Key (JWK) using the RS256 algorithm. To validate the signature, Okta provides your application with a public key that can be used.
Token validation is an important part of modern app development. By validating tokens, you can protect your app or APIs from unauthorized users. IBM Cloud® App ID uses access and identity tokens to ensure that a user or app is authenticated before they are granted access.
Is SHA256 hash calculator not a correct tool for this?
It's not working because you need to be use binary data for one of the steps and almost all the web tools are expecting some sort of text as input and generating text as output. The online tools are not suitable for this. I'll write a tool so you can see how it's done.
How do I get from my access token to the Base64 encoded at_hash claim value that is in the id token?
This is my first ever C# program iteration 2 :) so if it's ugly it's because I've never used it before. The explanation after this will explain how to compute an at_hash token, including why we need the decode_base64
.
using System;
using System.Security.Cryptography;
using System.Collections.Generic;
using System.Text;
namespace AtHash
{
class AtHash
{
private const String access_token = "ya29.eQGmYe6H3fP_d65AY0pOMCFikA0f4hzVZGmTPPyv7k_l6HzlEIpFXnXGZjcMhkyyuqSMtN_RTGJ-xg";
private const String id1 = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImUxMWQ1N2QxZmY0ODA0YjMxYzA1MWI3MWY2ZDVlNWExZmQyOTdjZjgifQ";
private const String id2 = "eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTEwMTY5NDg0NDc0Mzg2Mjc2MzM0IiwiYXpwIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiZW1haWwiOiJiaWxsZDE2MDBAZ21haWwuY29tIiwiYXRfaGFzaCI6ImxPdEkwQlJvdTBaNExQdFF1RThjQ3ciLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXVkIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiaWF0IjoxNDMyMTQyMjIyLCJleHAiOjE0MzIxNDU4MjJ9";
private byte[] decode_base64(String str) {
List<byte> l = new List<Byte>(Encoding.Default.GetBytes(str));
while (l.Count % 4 != 0 ){
l.Add(Convert.ToByte('='));
}
return Convert.FromBase64String(Encoding.Default.GetString(l.ToArray()));
}
public String sha256_at_hash(String access_token) {
SHA256Managed hashstring = new SHA256Managed();
byte[] bytes = Encoding.Default.GetBytes(access_token);
byte[] hash = hashstring.ComputeHash(bytes);
Byte[] sixteen_bytes = new Byte[16];
Array.Copy(hash, sixteen_bytes, 16);
return Convert.ToBase64String(sixteen_bytes).Trim('=');
}
public static void Main (string[] args) {
AtHash ah = new AtHash();
byte[] id1_str = ah.decode_base64 (id1);
byte[] id2_str = ah.decode_base64 (id2);
Console.WriteLine(Encoding.Default.GetString(id1_str));
Console.WriteLine(Encoding.Default.GetString(id2_str));
Console.WriteLine ("\n\tat_hash value == " + ah.sha256_at_hash(access_token));
}
}
}
Output of this program (formatting mine)
{
"alg":"RS256",
"kid":"e11d57d1ff4804b31c051b71f6d5e5a1fd297cf8"
}
{
"exp" : 1432145822,
"iat" : 1432142222,
"azp" : "407408718192.apps.googleusercontent.com",
"aud" : "407408718192.apps.googleusercontent.com",
"email_verified" : true,
"iss" : "accounts.google.com",
"at_hash" : "lOtI0BRou0Z4LPtQuE8cCw",
"sub" : "110169484474386276334",
"email" : "[email protected]"
}
at_hash value == lOtI0BRou0Z4LPtQuE8cCw
This is how to verify an at_hash
value. You can skip the google part if you want to use the data I've used but if you want to test it on new data you can get it at Google...
Get Access Token from Googles O2Auth Playground
Go here
https://developers.google.com/oauthplayground/
Don't select anything, near the bottom of the page there's an input box. Type in openid
and hit Authorize APIs
, click the id you want to use and select allow
. Select Exchange authorization code for tokens
. If successful you'll get something resembling the following.
{
"access_token": "ya29.eQGmYe6H3fP_d65AY0pOMCFikA0f4hzVZGmTPPyv7k_l6HzlEIpFXnXGZjcMhkyyuqSMtN_RTGJ-xg",
"token_type": "Bearer", "expires_in": 3600,
"refresh_token": "1/r5RRN6oRChjLtY5Y_T3lrqOy7n7QZJDQUVm8ZI1xGdoMEudVrK5jSpoR30zcRFq6",
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImUxMWQ1N2QxZmY0ODA0YjMxYzA1MWI3MWY2ZDVlNWExZmQyOTdjZjgifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTEwMTY5NDg0NDc0Mzg2Mjc2MzM0IiwiYXpwIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiZW1haWwiOiJiaWxsZDE2MDBAZ21haWwuY29tIiwiYXRfaGFzaCI6ImxPdEkwQlJvdTBaNExQdFF1RThjQ3ciLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXVkIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiaWF0IjoxNDMyMTQyMjIyLCJleHAiOjE0MzIxNDU4MjJ9.jtnP4Ffw2bPjfxRAEvHI8j88YBI4OJrw2BU7AQUCP2AUOKRC5pxwVn3vRomGTKiuMbnHqMyMiVSQZWTjAgjQrmaANxTEA68UMKh3dtu63hh4LHkGJly2hFcIKwbHxMWPDRO9nv8LxAUeCF5ccMgFNXhu-i-CeVtrMOsjCq6j5Qc"
}
The id_token is in three parts separated using a period .
. The first two parts are base64 encoded. I'm ignoring the third part of the id_token. We need to base64 decode both. Note, I'm using Perl to avoid having to pad the base64 strings ie Perl handles it for us.
The first part which you already know gives us the algorithm we need to use.
perl -MMIME::Base64 -e 'print decode_base64("eyJhbGciOiJSUzI1NiIsImtpZCI6ImUxMWQ1N2QxZmY0ODA0YjMxYzA1MWI3MWY2ZDVlNWExZmQyOTdjZjgifQ")'
{
"alg":"RS256",
"kid":"e11d57d1ff4804b31c051b71f6d5e5a1fd297cf8"
}
The second part gives is the at_hash
value.
perl -MMIME::Base64 -e 'print decode_base64("eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTEwMTY5NDg0NDc0Mzg2Mjc2MzM0IiwiYXpwIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiZW1haWwiOiJiaWxsZDE2MDBAZ21haWwuY29tIiwiYXRfaGFzaCI6ImxPdEkwQlJvdTBaNExQdFF1RThjQ3ciLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXVkIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiaWF0IjoxNDMyMTQyMjIyLCJleHAiOjE0MzIxNDU4MjJ9")'
{
"iss":"accounts.google.com",
........
"at_hash":"lOtI0BRou0Z4LPtQuE8cCw",
........
"exp":1432145822
}
Now we know what the at_hash
value is we can use the access_token
to verify that they're the same... The following Perl program does this.
#!/usr/bin/env perl
use strict;
use warnings;
use MIME::Base64;
use Digest::SHA qw(sha256);
my $data = "ya29.eQGmYe6H3fP_d65AY0pOMCFikA0f4hzVZGmTPPyv7k_l6HzlEIpFXnXGZjcMhkyyuqSMtN_RTGJ-xg";
my $digest = sha256($data);
my $first_16_bytes = substr($digest,0,16);
print encode_base64($first_16_bytes);
This program can be run as follows
perl sha256.pl
lOtI0BRou0Z4LPtQuE8cCw==
Note we got at_hash
but why are they not the same..., they are in fact the same it's just one of them is missing the padding. The =
signs are added until the following is true.
(strlen($base64_string) % 4 == 0)
In our case
strlen("lOtI0BRou0Z4LPtQuE8cCw") == 22
so we got two ==
added to the result :). The reason they're not in the token is because the people who wrote the spec don't believe passing unnecessary bytes over a network is a good idea if they can be added at the other end.
It's described exactly in the spec:
https://openid.net/specs/openid-connect-core-1_0.html
3.1.3.6. ID Token
at_hash OPTIONAL. Access Token hash value. Its value is the base64url encoding of the left-most half of the hash of the octets of the ASCII representation of the access_token value, where the hash algorithm used is the hash algorithm used in the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is RS256, hash the access_token value with SHA-256, then take the left-most 128 bits and base64url encode them. The at_hash value is a case sensitive string.
I ran into a bit of a similar issue in generating client secrets.
Looking at the HashExtensions class that IdentityServer uses was helpful; in my case I wasn't getting the bytes with UTF8 encoding. I suspect that online tool you linked is taking a different approach to encoding the byte array to strings.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With