如何使用 ID 令牌的 at_hash 声明来验证访问令牌?

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

假设我在交换从 /auth 端点 (using this example OAuth Playground request) 获得的代码后从 Google 的 OAuth2 /token 端点收到以下响应:

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

如何对访问令牌进行哈希处理以便将其与 ID 令牌的 at_hash 声明进行比较?

我可以在服务器上本地验证 ID 令牌以防止客户端修改,并且想要验证访问令牌是否是与 ID 令牌一起颁发的(这意味着观众和主题与 ID 令牌相匹配)。

OpenID Connect 的 at_hash ID 令牌声明是 defined

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.

混合流的c_hashID Token声明为defined similarly,验证步骤相同。

从令牌生成 at_hashc_hash 的步骤:

  1. 使用与 ID 令牌本身相同的 alg 对令牌的 ASCII 表示进行哈希处理,在 Google 的情况下使用 SHA-256。
  2. T运行将哈希值归为原始哈希值的前半部分 (重要的是:不是哈希的字符串十六进制表示)。
  3. Base64url 编码(无填充)t运行cated 哈希字节。

下面是 python 中的一些示例代码来创建该哈希,您需要两个库,pycryptogoogle-api-python-client(对于 base64 编码和 ID 令牌比较,您可能会用替代品替代)。你可以像这样用 pip 安装它们:

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

然后,运行 python 交互式地尝试以下操作:

# 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)

要使用您自己帐户中的新 ID 令牌试用此示例,请使用 OAuth Playground with the profile scope (or use this one 创建请求),交换刷新和访问令牌的代码,并将响应复制到 token_response_http_body在上面的示例中(删除换行符)。

PHP 解决方案:

$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 可在此处获取:https://packagist.org/packages/google/apiclient

我将根据 OpenID Connect 核心规范 (read here) 给出答案。查看第 3.2.2.9 节,客户端可以使用 ID 令牌验证授权服务器提供的访问令牌。

步骤如下:

  1. 将 access_token 的 ASCII 表示的八位字节散列为 JWA 中为 alg Header 参数指定的哈希算法 ID Token 的 JOSE Header。例如,如果算法是 RS256,则 使用的散列算法是 SHA-256。
  2. 取 left-most 一半的散列和 base64url 对其进行编码。
  3. ID 令牌中 at_hash 的值必须与上一步生成的值匹配。

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.

一个PHP的实现大概是这样的:

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;
}

调用 verifyToken 传递您的 ID 令牌和访问令牌。如果哈希匹配,它将 return 为真,否则为假。

基本Java解决方案:

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));
}

C# 解决方案,但我不确定它是否适用于所有情况:

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('/', '_');
        }
    }