如何验证 Windows Live Connect JWT authentication_token?

How to verify a Windows Live Connect JWT authentication_token?

我正在使用 Windows Live Connect javascript SDK 在网页上登录用户。为了将身份断言传递到我的服务器,此 SDK 提供了一个已签名的 JWT 令牌 WL.getSession().authentication_token。它似乎是一个标准的 JWT,但我无法验证签名。

我用什么秘密?我已尝试从 Microsoft 帐户开发中心为我的应用程序使用客户端密码,但这在我的 JWT 库和在线 JWT 检查器(例如 jwt.io)中都无法通过签名验证。

此令牌的文档是随意的。主要文档似乎是 this. However, the code sample has been dropped in a migration and needs to be pulled out of github history;无论如何,它只是说使用 "application secret" 而没有提及其来源。

This blog entry says I should go to http://appdev.microsoft.com/StorePortals, however, my app is not part of the windows store; it's a standard developer center application (https://account.live.com/developers/applications/index).

我找到了 official microsoft video describing how to decode the token (see slide 15,或在 29:35 观看视频。秘密从何而来也模棱两可。更糟糕的是,它引用了当前 SDK 中未显示的 SDK 方法 (LiveAuthClient.GetUserId())。

我很困惑。是的,我知道我可以使用 access_token 并从配置文件端点获取用户 ID,但我需要避免这种额外的 API 往返。 JWT authentication_token 显然就是为了这个目的而存在的——我如何验证内容?

您需要此 C# 示例中的 "JWTSig":

    public static byte[] EncodeSigningToken(string token)
    {
        try
        {

            var sha256 = new SHA256Managed();
            var secretBytes = StrToByteArray(token + "JWTSig");

            var signingKey = sha256.ComputeHash(secretBytes);

            return signingKey;
        }
        catch (Exception)
        {
            return null;
        }
    }

或者这样:

    private void ValidateSignature(string key)
    {
        // Derive signing key, Signing key = SHA256(secret + "JWTSig")
        byte[] bytes = UTF8Encoder.GetBytes(key + "JWTSig");
        byte[] signingKey = SHA256Provider.ComputeHash(bytes);

        // To Validate:
        // 
        // 1. Take the bytes of the UTF-8 representation of the JWT Claim
        //  Segment and calculate an HMAC SHA-256 MAC on them using the
        //  shared key.
        //
        // 2. Base64url encode the previously generated HMAC as defined in this
        //  document.
        //
        // 3. If the JWT Crypto Segment and the previously calculated value
        //  exactly match in a character by character, case sensitive
        //  comparison, then one has confirmation that the key was used to
        //  generate the HMAC on the JWT and that the contents of the JWT
        //  Claim Segment have not be tampered with.
        //
        // 4. If the validation fails, the token MUST be rejected.

        // UFT-8 representation of the JWT envelope.claim segment
        byte[] input = UTF8Encoder.GetBytes(this.envelopeTokenSegment + "." + this.claimsTokenSegment);

        // calculate an HMAC SHA-256 MAC
        using (HMACSHA256 hashProvider = new HMACSHA256(signingKey))
        {
            byte[] myHashValue = hashProvider.ComputeHash(input);

            // Base64 url encode the hash
            string base64urlEncodedHash = this.Base64UrlEncode(myHashValue);

            // Now compare the two has values
            if (base64urlEncodedHash != this.Signature)
            {
                throw new Exception("Signature does not match.");
            }
        }
    }

此外,这里还有更多语言的实现。

Java:

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.io.BaseEncoding;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

public class WLChecker {

    private final String clientSecret;

    public WLChecker(String clientSecret) {
        this.clientSecret = clientSecret;
    }

    /**
     * @throws GeneralSecurityException if the token is not perfect
     */
    public String check(String tokenString) throws IOException, GeneralSecurityException {
        final String[] parts = tokenString.split("\.");
        if (parts.length != 3)
            throw new GeneralSecurityException("Not a valid token");

        validate(parts[0], parts[1], parts[2]);

        JsonNode claims = new ObjectMapper().readTree(BaseEncoding.base64Url().decode(parts[1]));

        String uid = claims.path("uid").asText();
        if (uid == null || uid.length() == 0)
            throw new GeneralSecurityException("No uid in claims");

        return uid;
    }

    private void validate(String envelope, String claims, String sig) throws GeneralSecurityException {
        byte[] signingKey = sha256(getBytesUTF8(clientSecret + "JWTSig"));
        byte[] input = getBytesUTF8(envelope + "." + claims);

        Mac hmac = Mac.getInstance("HmacSHA256");
        hmac.init(new SecretKeySpec(signingKey, "HmacSHA256"));

        byte[] calculated = hmac.doFinal(input);

        if (!Arrays.equals(calculated, BaseEncoding.base64Url().decode(sig)))
            throw new GeneralSecurityException("Signature verification failed");
    }

    private byte[] getBytesUTF8(String s) {
        try {
            return s.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    private byte[] sha256(byte[] input) {
        try {
            return MessageDigest.getInstance("SHA-256").digest(input);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }
}

节点:

var crypto = require('crypto');

var token = "BLAH.BLAH.BLAH";
var parts = token.split(".");

var input = parts[0] + "." + parts[1];
var masterKey = "YOUR MASTER KEY";

var key = crypto.createHash('sha256').update(masterKey + "JWTSig").digest('binary');
var str = crypto.createHmac('sha256', key).update(input).digest('base64');

console.log(str);
console.log(parts[2]);

PHP:

解码authentication_code部分,这里有一个代码片段,您可以参考实现。

function jsonWebTokenBase64Decode($string)
{
    $string = str_replace('-', '+', $string);
    $string = str_replace('_', '/', $string);

    switch (strlen($string) % 4)
    {
        case 0: break;
        case 2: $string .= '=='; break;
        case 3: $string .= '='; break;
        default: throw createInvalidAuthenticationTokenException();
    }

    return base64_decode($string);
}

function jsonWebTokenBase64Encode($string)
{
    $string = base64_encode($string);
    $string = trim($string, '=');
    $string = str_replace('+', '-', $string);
    return str_replace('/', '_', $string);
}

function decodeAuthenticationToken($authenticationToken, $clientSecret)
{
    // Break the token into segments delimited by dots and verify there are three segments
    $segments = explode('.', $authenticationToken);
    if (count($segments) != 3)
    {
        throw createInvalidAuthenticationTokenException();
    }

    // Decode the segments to extract two JSON objects and the signature
    $envelope = json_decode(jsonWebTokenBase64Decode($segments[0]), true);
    $claims = json_decode(jsonWebTokenBase64Decode($segments[1]), true);
    $signature = $segments[2];

    // If the authentication token is expired, return false
    if ($claims['exp'] < time())
    {
        return false;
    }

    // Verify that the algorithm and token type are correct
    if ($envelope['alg'] != 'HS256')
    {
        throw createInvalidAuthenticationTokenException();
    }

    if ($envelope['typ'] != 'JWT')
    {
        throw createInvalidAuthenticationTokenException();
    }

    // Compute the signing key by hashing the client secret
    $encodedClientSecret = utf8_encode($clientSecret . 'JWTSig');
    $signingKey = hash('sha256', $encodedClientSecret, true);

    // Concatenate the first two segments of the token and perform an HMAC hash with the signing key
    $input = utf8_encode($segments[0] . '.' . $segments[1]);
    $hashValue = hash_hmac('sha256', $input, $signingKey, true);

    // Validate the token by base64 encoding the hash value and comparing it to the signature
    $encodedHashValue = jsonWebTokenBase64Encode($hashValue);
    if ($encodedHashValue != $signature)
    {
        throw createInvalidAuthenticationTokenException();
    }

    // If the token passes validation, return the user ID stored in the token
    return $claims['uid'];
}