rsaidentityserver3oauth-2.0

Validating access token with at_hash


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


Solution

  • 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" : "billd1600@gmail.com"
    }
    
    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.