如何验证 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'];
}
我正在使用 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'];
}