13

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

danijels
  • 5,211
  • 4
  • 26
  • 36

5 Answers5

10

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.

Harry
  • 11,298
  • 1
  • 29
  • 43
  • Once, I am done with this much of work, I get a valid accessToken. But in case of WebAPI token based authentication - Individual user account, server sends an access token when login with username, password, and grant_type. How do I set this accessToken in server as one issued while logging. – Abhishek Jaiswal Jul 04 '18 at 05:55
3

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.

Community
  • 1
  • 1
leastprivilege
  • 18,196
  • 1
  • 34
  • 50
  • Yes, I did read the spec and yet I couldn't make it work. I try hashing the access token value here http://www.xorbin.com/tools/sha256-hash-calculator then base64 encoding it here http://www.xorbin.com/tools/base64-encoder-and-decoder. The end result is not even close to being right – danijels Mar 29 '16 at 08:15
  • 1
    Here's a JS-based implementation: https://github.com/IdentityModel/oidc-client-js/blob/dev/src/ResponseValidator.js#L248 – Brock Allen Apr 15 '16 at 12:27
2

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.

lgaud
  • 2,430
  • 20
  • 30
  • Thanks, but I tried different online tools too. I'd like to understand why there is a difference and surely it should be easy to find an online tool that does the same thing. – danijels Apr 07 '16 at 08:03
0

For Perl, the code above need to be augmented like this:

#!/usr/bin/env perl
use strict;
use warnings;
use MIME::Base64;
use Digest::SHA;
my $access_token = "SOMETHING"; 
my $digest = Digest::SHA::sha256( $access_token );
my $first_16_bytes = substr( $digest, 0, 16 );
print MIME::Base64::encode_base64url( $first_16_bytes );

Then it is working standard-compliant indeed.

Make sure you upgrade your MIME::Base64 module to the latest version.

0

Golang solution

func verifyAtHash(accessToken string, atHash string) bool {
    h := sha256.New() // for RS256, ES256, PS256
    h.Write([]byte(accessToken)) // hash documents that Write never return an error
    sum := h.Sum(nil)[:h.Size()/2] // left-most 128 bits
    atHashFromAccessToken := base64.RawURLEncoding.EncodeToString(sum)
    return atHashFromAccessToken == atHash
}

more complete solution here with different signing algorithm (see verifyHashClaim): https://github.com/hashicorp/cap/blob/v0.3.1/oidc/id_token.go#L95

Big_Boulard
  • 799
  • 1
  • 13
  • 28