How do I validate an access token using the at_hash claim of an id token?
Asked Answered
F

6

8

Say I have the following response from Google's OAuth2 /token endpoint after exchanging the code obtained from the /auth endpoint (using this example OAuth Playground request):

{
  "access_token": "ya29.eQETFbFOkAs8nWHcmYXKwEi0Zz46NfsrUU_KuQLOLTwWS40y6Fb99aVzEXC0U14m61lcPMIr1hEIBA", 
  "token_type": "Bearer", 
  "expires_in": 3600, 
  "refresh_token": "1/ZagesePFconRc9yQbPxw2m1CnXZ5MNnni91GHxuHm-A", 
  "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjJhODc0MjBlY2YxNGU5MzRmOWY5MDRhMDE0NzY4MTMyMDNiMzk5NGIifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTEwMTY5NDg0NDc0Mzg2Mjc2MzM0IiwiYXpwIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXRfaGFzaCI6ImFVQWtKRy11Nng0UlRXdUlMV3ktQ0EiLCJhdWQiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJpYXQiOjE0MzIwODI4NzgsImV4cCI6MTQzMjA4NjQ3OH0.xSwhf4KvEztFFhVj4YdgKFOC8aPEoLAAZcXDWIh6YBXpfjzfnwYhaQgsmCofzOl53yirpbj5h7Om5570yzlUziP5TYNIqrA3Nyaj60-ZyXY2JMIBWYYMr3SRyhXdW0Dp71tZ5IaxMFlS8fc0MhSx55ZNrCV-3qmkTLeTTY1_4Jc"
}

How do I hash the access token in order to compare it to the at_hash claim of the ID Token?

I can verify ID Tokens locally on the server to protect against client modification, and want to verify the Access Token was the one that was issued with the id token (implying that audience and subject match the ID token's).

Fabe answered 20/5, 2015 at 17:26 Comment(0)
F
19

The at_hash ID Token claim is defined by OpenID Connect as such:

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.

The c_hash ID Token claim for the hybrid flow is defined similarly, the same steps can be used to verify either.

Steps to generate an at_hash or c_hash from the token:

  1. Hash the ASCII representation of the token using the same alg as the ID Token itself, SHA-256 in Google's case.
  2. Truncate the hash to the first half of the raw hash value (importantly: not the string hex representation of the hash).
  3. Base64url encode (without padding) the truncated hash bytes.

Here's some sample code in python to create that hash, you'll need two libraries, pycrypto and the google-api-python-client (for the base64 encoding & id token comparison, you could potentially substitute with an alternative). You can install them with pip like so:

pip install pycrypto
pip install --upgrade google-api-python-client

Then, run python interactively, and try the following:

# Config: app's client id & tokens (in this case OAuth Playground's client id, and the tokens were extracted from the Token Endpoint response).
client_id = "407408718192.apps.googleusercontent.com"
id_token_string = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjcwZjZjNDI2NzkyNWIzMzEzNmExZDFjZmVlNGViYzU3YjI0OWU1Y2IifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiYXRfaGFzaCI6Iml5VkFfTnNtY2JJMDFHcFJDQVJaOEEiLCJhdWQiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTAxNjk0ODQ0NzQzODYyNzYzMzQiLCJhenAiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJpYXQiOjE0NjcyMTg1NzMsImV4cCI6MTQ2NzIyMjE3M30.e4hJJYeUaFVwJ9OC8LBnmOjwZln_E2-isEUJtb-Um7vt3GDZnBZkHdCokAPBL4OW3DXBNPk9iY0QL2P5Gpb-nX_s-PZKOIES8CE0i2DmGahCZgJY_Y3V2qwiP1fTEQjcUmHEG2e7OdCn6siSZveFQ0W7SiSbbSeJVLws9aoHROo_UXy8CVjaU5KinROG6m6igqCxFoskIWRzAynfx70xMadY4UdS8kbKK_v5id0_Rdg_gYlF1ND0lsPM9vdm3jOifQEAAkjHr-RuSDWlX4Bs4cQtEkeQkN6--MWhoqAshJITuGSazVIiDkVUNNBIXmB_dp9TO6ZjeQEEfeGCs6axKA"
access_token = "ya29.Ci8QA5eGBdBglK59FXdqXIR5KnbMJs-swx6Alk6_AV_6YPkjhxdO1e0Hqxi-8NB3Ww"

# Verifies & parses id token.
idtoken = oauth2client.client.verify_id_token(id_token_string, client_id)

# Token to hash & expected hash value (replace with code & c_hash to verify code).
token_to_hash = access_token
token_hash_expected = idtoken["at_hash"]

# Step 1. hashes the access token using SHA-256 (Google uses `RS256` as the ID Token `alg`).
hash = hashlib.sha256()
hash.update(token_to_hash)
digest = hash.digest()   # this returns the hash digest bytes (not a hex string)

# Step 2. truncates the hash digest to the first half.
digest_truncated = digest[:(len(digest)/2)]

# Step 3. base64url encodes the truncated hash digest bytes.
token_hash_computed = oauth2client.crypt._urlsafe_b64encode(digest_truncated)

# Compares computed to expected, outputs result.
str("Computed at_hash: %s" % token_hash_computed)
str(token_hash_computed == token_hash_expected)

To try this sample with a fresh ID Token from your own account, create a request using the OAuth Playground with the profile scope (or use this one), exchange the code for refresh and access tokens, and copy the response into token_response_http_body in the sample above (remove the linebreaks).

Fabe answered 20/5, 2015 at 17:26 Comment(2)
I know this is old, but it was really helpful. Thank you William. May I add re Python 3 and encoding issues you do this in one line hashlib.sha256(code.encode('utf-8')) .Coshow
also, I get a floating point in Python 3, rightly or wrongly I added int digest[:(int(len(digest) / 2))] - Same outcome :)Coshow
H
3

C# solution, though I'm not sure if it works in all cases:

using System.Linq;
using System.Security.Cryptography;
using System.Text;

    static readonly char[] padding = { '=' };

    private static string CreateGoogleAtHash(string accessToken)
    {
        using (SHA256 sha256Hash = SHA256.Create())
        {
            byte[] bytes = sha256Hash.ComputeHash(Encoding.ASCII.GetBytes(accessToken));
            byte[] firstHalf = bytes.Take(bytes.Length / 2).ToArray();

            return System.Convert.ToBase64String(firstHalf).TrimEnd(padding).Replace('+', '-').Replace('/', '_');
        }
    }
Houseroom answered 22/10, 2020 at 14:39 Comment(1)
Very clean c# example - the Replace('+', '-') was especially helpful!Sacrilege
T
2

PHP solution:

$accessToken = 'xxx';
$idToken = 'yyy';
$client = new Google_Client();

$verification = $client->verifyIdToken($idToken);

$hash = hash('sha256', $accessToken);
$hash = substr($hash, 0, 32);
$hash = hex2bin($hash);
$hash = base64_encode($hash);
$hash = rtrim($hash, '=');
$hash = str_replace('/', '_', $hash);
$hash = str_replace('+', '-', $hash);

if ($hash === $verification['at_hash']) {
    // access token is valid
}

Google_Client available here: https://packagist.org/packages/google/apiclient

Teide answered 12/9, 2018 at 15:17 Comment(0)
B
2

I am going to give an answer based on OpenID Connect Core specification (read here). Looking at section 3.2.2.9, a client can validate an access token that was given by an authorization server with an ID Token.

The steps are as follows:

  1. Hash the octets of the ASCII representation of the access_token with the hash algorithm specified in JWA for the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is RS256, the hash algorithm used is SHA-256.
  2. Take the left-most half of the hash and base64url encode it.
  3. The value of at_hash in the ID Token MUST match the value produced in the previous step.

Step 1 requires the client to know what algorithm was used to sign the ID Token. It can be done by decoding the ID Token and checking the Header section for the alg property. Let's say the alg is equal to RS256 then, the hash algorithm used to create at_hash is SHA-256. If it was RS384 then the hash algorithm is SHA-384, and so on, you get the point.

Step 2 requires halving the hashed value, and take the left half to apply base64url encoding.

Step 3 then expects the at_hash value in the ID Token to be equal to the hash operations done in step 1 & 2. If it's not equal, then the access token wasn't issued with the specified ID Token.

A PHP implementation would roughly be like this:

public function verifyToken($id_token, $access_token)
{
    $header = $this->decodeJWT($id_token, 0);
    $claims = $this->decodeJWT($id_token, 1);

    return $this->createAtHash($access_token, $header['alg']) === $claims['at_hash'];
}

public function decodeJWT($jwt, $section = 0) 
{    
    $parts = explode(".", $jwt);

    return json_decode(base64url_decode($parts[$section]));
}

public function createAtHash($access_token, $alg)
{
    // maps HS256 and RS256 to sha256, etc.
    $hash_algorithm = 'sha' . substr($alg, 2);
    $hash = hash($hash_algorithm, $access_token, true);
    $at_hash = substr($hash, 0, strlen($hash) / 2);

    return $this->urlSafeB64Encode($at_hash);
}

public function urlSafeB64Encode($data)
{
    $b64 = base64_encode($data);
    $b64 = str_replace(array('+', '/', "\r", "\n", '='),
            array('-', '_'),
            $b64);

    return $b64;
}

Call verifyToken passing your ID Token and access token. It will return true if the hash matches, and false on the contrary.

Bhili answered 25/5, 2020 at 18:39 Comment(0)
K
2

Basic Java solution:

private static final String acccesToken = "rvArgQKPbBDJkeTHwoIAOQVkV8J0_i8PhrRKyLDaKkk.iY6nzJoIb2dRXBoqHAa3Yb6gkHveTXbnM6PGtmoKXvo";

public static void main(String[] args) throws NoSuchAlgorithmException {
    MessageDigest md = MessageDigest.getInstance("SHA-256");
    byte[] asciiValue = acccesToken.getBytes(StandardCharsets.US_ASCII);
    byte[] encodedHash = md.digest(asciiValue);
    byte[] halfOfEncodedHash = Arrays.copyOf(encodedHash, (encodedHash.length / 2));
    System.out.println("at_hash generated from access-token: " + Base64.getUrlEncoder().withoutPadding().encodeToString(halfOfEncodedHash));
}
Kef answered 20/7, 2020 at 4:55 Comment(0)
U
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 (see verifyHashClaim): https://github.com/hashicorp/cap/blob/v0.3.1/oidc/id_token.go#L95

Unsure answered 11/7, 2023 at 14:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.