Reputation: 13556
I'm using the Windows Live Connect javascript SDK to log in a user on a web page. In order to pass the identity assertion to my server, this SDK provides a signed JWT token as WL.getSession().authentication_token
. It appears to be a standard JWT, but I am unable to verify the signature.
What secret do I use? I have tried the client secret for my application from the Microsoft Account Dev Center, but this fails signature verification in both my JWT libraries and online JWT checkers (eg jwt.io).
Documentation for this token is haphazard. The primary documentation appears to be this. However, the code sample has been dropped in a migration and needs to be pulled out of github history; in any case, it merely says use the "application secret" without mentioning its origin.
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).
I have found an official microsoft video describing how to decode the token (see slide 15, or watch the video at 29:35). Also ambiguous as to where the secret comes from. Even worse, it references a SDK method that does not show up in the present SDK (LiveAuthClient.GetUserId()
).
I'm baffled. Yes, I know I can take the access_token and fetch the user id from the profile endpoint, but I need to avoid this extra API roundtrip. The JWT authentication_token is clearly present for exactly this purpose - how can I verify the content?
Upvotes: 1
Views: 407
Reputation: 51
PHP:
For decoding authentication_code part, here is a code snippet that you can refer to implement.
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'];
}
Upvotes: 0
Reputation: 13556
In addition, here are implementations in a couple more languages.
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);
}
}
}
Node:
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]);
Upvotes: 1
Reputation: 1178
You need the "JWTSig" as in this C# sample:
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;
}
}
Or this:
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.");
}
}
}
Upvotes: 0