Validating access token with at_hash
Asked Answered
T

5

14

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

Turdine answered 22/3, 2016 at 11:1 Comment(1)
Is the answer I posted any use, My C# isn't that sharp :)Hartford
H
11

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.

Hartford answered 19/4, 2016 at 4:5 Comment(1)
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.Funerary
A
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.

Airway answered 22/3, 2016 at 18:16 Comment(2)
Yes, I did read the spec and yet I couldn't make it work. I try hashing the access token value here xorbin.com/tools/sha256-hash-calculator then base64 encoding it here xorbin.com/tools/base64-encoder-and-decoder. The end result is not even close to being rightTurdine
Here's a JS-based implementation: github.com/IdentityModel/oidc-client-js/blob/dev/src/…Corsica
R
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.

Raisin answered 6/4, 2016 at 17:36 Comment(1)
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.Turdine
R
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.

Repro answered 1/2, 2019 at 16:41 Comment(0)
E
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

Encephalon answered 11/7, 2023 at 16:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.