Azureの小ネタ (改)

~Azureネタを中心に、色々とその他の技術的なことなどを~

いまさら AppFabric ACS を弄ぶ その2

こんばんはstatemachineです。いつもご愛読ありがとうございます。前回(2010/11/11)から、だいぶ時間が空いてしまい、このままフェードアウトしようかと思ったのですが、一部方面からのリクエストにより続きを書くことにしました。例の通りあまりよく理解していないので、用語など不正確な記述がありますのであしからず。

前回のおさらい

詳細は、前回記事 いまさら AppFabric ACS を弄ぶ その1 - Azureの小ネタ をご覧いただくとして、前回はAppFabric ACSからTokenを受け取ったところで終わっていたので、今回はそのTokenが正しいか承認する部分を実装してみたいと思います。

システム図的には以下の範囲。

サービスの作成

本来ならば、Webサーバでも立ち上げてTokenの検証をするのですが、その受け渡し部分が面倒なので、純粋にTokenありきでどのように検証するかの部分だけ実装してみます。このソースは、ACSサンプル内のTokenValidator.cs そのものですのであしからず。

Token検証の手順

検証手順としては以下のとおり。

  1. Tokenの末尾の署名の確認
  2. 有効期限の確認
  3. Issuer URLの確認
  4. Service URLの確認
        public bool Validate(string token)
        {
            if (!this.IsHMACValid(token, this.trustedSigningKey))
            {
                return false;
            }
            Console.WriteLine ("HMAC is valid.");
            if (this.IsExpired(token))
            {
                return false;
            }
            Console.WriteLine("Not expired.");

            if (!this.IsIssuerTrusted(token))
            {
                return false;
            }
            Console.WriteLine ("Issuer is trusted.");

            if (!this.IsAudienceTrusted(token))
            {
                return false;
            }
            Console.WriteLine ("Audience is trusted.");

            return true;
        }
Token

前回受け取ったTokenをデーコドすると以下。URLなどは、さらにデコードどする必要がありますが、&で区切られた個々がクレーム。

mode=admin&Issuer=https%3a%2f%2fxxxxx.accesscontrol.windows.net%2f&Audience=http%3a%2f%2flocalhost%2fappfabric&ExpiresOn=1294950141&HMACSHA256=rXnyZF686JryIhlj5DW3ns7iKic%2fec9LjOQ4P7XNKNg%3d

ばらばらにすると下表。基本的にこれらを検証していくわけです。

mode admin 独自に作ったクレーム、アプリで解釈
Issuer https%3a%2f%2fxxxxx.accesscontrol.windows.net%2f 発行者
Audience Audience=http%3a%2f%2flocalhost%2fappfabric サービス先
ExpiresOn 1294950141 期限
HMACSHA256 rXnyZF686JryIhlj5DW3ns7iKic%2fec9LjOQ4P7XNKNg%3d 署名
署名の確認

Tokenは、AppFabric ACSで発行されたことを証明する署名が末尾についてます。以下のHMACSHA256=あとの文字列です。

mode=admin&Issuer=https%3a%2f%2fxxxxx.accesscontrol.windows.net%2f&Audience=http%3a%2f%2flocalhost%2fappfabric&ExpiresOn=1294950141&HMACSHA256=rXnyZF686JryIhlj5DW3ns7iKic%2fec9LjOQ4P7XNKNg%3d

これを以下のメソッドで検証します。第二引数のsha256HMACKeyは、AppFabricで取得したToken Policy Keyです。サービス側は、AppFabricによって発行されたToken Policy Keyを持っていることによって信頼性を得ているわけで、そこに何らかのネットワークのつながりがあるわけではありません。このKeyを元に正しいACSで発行されたものか検証するわけです。
この検証で、Tokenが改ざんされていないことは確認できているので、あとはそれぞれのクレームがサービス側で受け入れ可能なものか単純にチェックしていくだけです。

        private bool IsHMACValid(string swt, byte[] sha256HMACKey)
        {
            string[] swtWithSignature = swt.Split(new string[] { "&" + this.hmacSHA256Label + "=" }, StringSplitOptions.None);

            if ((swtWithSignature == null) || (swtWithSignature.Length != 2))
            {
                return false;
            }

            HMACSHA256 hmac = new HMACSHA256(sha256HMACKey);

            byte[] locallyGeneratedSignatureInBytes = hmac.ComputeHash(Encoding.ASCII.GetBytes(swtWithSignature[0]));

            string locallyGeneratedSignature = HttpUtility.UrlEncode(Convert.ToBase64String(locallyGeneratedSignatureInBytes));

            return locallyGeneratedSignature == swtWithSignature[1];
        }
Issuerの確認

発行者を確認します。発行したAppFabric ACSのURLが入っているので、これがサービス側で了解しているものかチェックします。

mode=admin&Issuer=https%3a%2f%2fxxxxx.accesscontrol.windows.net%2f&Audience=http%3a%2f%2flocalhost%2fappfabric&ExpiresOn=1294950141&HMACSHA256=rXnyZF686JryIhlj5DW3ns7iKic%2fec9LjOQ4P7XNKNg%3d

コード自体は特に難しいものではありません。

        private bool IsIssuerTrusted(string token)
        {
            Dictionary<string, string> tokenValues = this.GetNameValues(token);

            string issuerName;

            tokenValues.TryGetValue(this.issuerLabel, out issuerName);

            if (!string.IsNullOrEmpty(issuerName))
            {
                if (issuerName.Equals(this.trustedTokenIssuer))
                {
                    return true;
                }
            }

            return false;
        }
Service URLの確認

Audienceとなっていますが、これは下図からも分かるようにApplies Toの値です。自分のサービス向けに発行されているのか検証することになります。(なぜにAudience?)

        private bool IsAudienceTrusted(string token)
        {
            Dictionary<string, string> tokenValues = this.GetNameValues(token);

            string audienceValue;

            tokenValues.TryGetValue(this.audienceLabel, out audienceValue);
            if (!string.IsNullOrEmpty(audienceValue))
            {
                Uri audienceValueUri = new Uri(audienceValue);
                if (audienceValueUri.Equals(this.trustedAudienceValue))
                {
                    return true;
                }
            }
最終的に

期限のチェックについては割愛しましたが、ここまでおこなうとTokenが正しいか確認できます。あとは、mode=adminといったクレームを、サービス側でどう理解するかというアプリの問題になってくるはずです。

まとめ

結局は、Tokenが正しいものか、TokenPolicyKeyを使って検証し、あとは自力でゴリゴリとやるしかないわけですかね。WIFを使ったりすると、クラスライブラリ化されているんでしょうか?

最後に全体ソースと、実行結果を付けておきます。何となく分かったような雰囲気になって頂けたら幸いです。

-- response --
wrap_access_token=mode%3dadmin%26Issuer%3dhttps%253a%252f%252fcrossbar.accesscontrol.windows.net%252f%26Audience%3dhttp%253a%252f%252flocalhost%252fappfabric%26ExpiresOn%3d1294986751%26HMACSHA256%3daxfWutakM0ruMjXnlgXMD3Sa51Jq9gxlqKQAndiPyo4%253d&wrap_access_token_expires_in=28800
-- token --
mode%3dadmin%26Issuer%3dhttps%253a%252f%252fcrossbar.accesscontrol.windows.net%252f%26Audience%3dhttp%253a%252f%252flocalhost%252fappfabric%26ExpiresOn%3d1294986751%26HMACSHA256%3daxfWutakM0ruMjXnlgXMD3Sa51Jq9gxlqKQAndiPyo4%253d
-- Authorization --
Validate : mode=admin&Issuer=https%3a%2f%2fcrossbar.accesscontrol.windows.net%2f&Audience=http%3a%2f%2flocalhost%2fappfabric&ExpiresOn=1294986751&HMACSHA256=axfWutakM0ruMjXnlgXMD3Sa51Jq9gxlqKQAndiPyo4%3d
HMAC is valid.
Not expired.
Issuer is trusted.
Audience : http://localhost/appfabric
Audience is trusted.
True
void Main()
{
    string token = "ACSで発行されたToken";
    string tokenPolicyKey = "TokenPolicyKeyを入れてね";
    var tv = new TokenValidator("https://xxxxx.accesscontrol.windows.net/", "http://localhost/appfabric", Convert.FromBase64String(tokenPolicyKey));
    var s = tv.Validate(HttpUtility.UrlDecode(token));
    Console.WriteLine (s);
}

    public class TokenValidator
    {
        private string issuerLabel = "Issuer";
        private string expiresLabel = "ExpiresOn";
        private string audienceLabel = "Audience";
        private string hmacSHA256Label = "HMACSHA256";

        private byte[] trustedSigningKey;
        private string trustedTokenIssuer;
        private Uri trustedAudienceValue;

        public TokenValidator(string issuerName, string trustedAudienceValue, byte[] trustedSigningKey)
        {
            this.trustedSigningKey = trustedSigningKey;
            this.trustedTokenIssuer = issuerName.ToLowerInvariant();
            this.trustedAudienceValue = new Uri(trustedAudienceValue);
        }

        public bool Validate(string token)
        {
            Console.WriteLine("Validate : " + token);
            if (!this.IsHMACValid(token, this.trustedSigningKey))
            {
                return false;
            }
            Console.WriteLine ("HMAC is valid.");
            if (this.IsExpired(token))
            {
                return false;
            }
            Console.WriteLine("Not expired.");

            if (!this.IsIssuerTrusted(token))
            {
                return false;
            }
            Console.WriteLine ("Issuer is trusted.");

            if (!this.IsAudienceTrusted(token))
            {
                return false;
            }
            Console.WriteLine ("Audience is trusted.");

            return true;
        }

        public Dictionary<string, string> GetNameValues(string token)
        {
            if (string.IsNullOrEmpty(token))
            {
                throw new ArgumentException();
            }

            return
                token
                .Split('&')
                .Aggregate(
                new Dictionary<string, string>(),
                (dict, rawNameValue) =>
                {
                    if (rawNameValue == string.Empty)
                    {
                        return dict;
                    }

                    string[] nameValue = rawNameValue.Split('=');

                    if (nameValue.Length != 2)
                    {
                        throw new ArgumentException("Invalid formEncodedstring - contains a name/value pair missing an = character");
                    }

                    if (dict.ContainsKey(nameValue[0]) == true)
                    {
                        throw new ArgumentException("Repeated name/value pair in form");
                    }
                    // Console.WriteLine (nameValue[0]  + " ; " + nameValue[1]);
                    dict.Add(HttpUtility.UrlDecode(nameValue[0]), HttpUtility.UrlDecode(nameValue[1]));
                    return dict;
                });
        }

        private static ulong GenerateTimeStamp()
        {
            // Default implementation of epoch time
            TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
            return Convert.ToUInt64(ts.TotalSeconds);
        }

        private bool IsAudienceTrusted(string token)
        {
            Dictionary<string, string> tokenValues = this.GetNameValues(token);

            string audienceValue;

            tokenValues.TryGetValue(this.audienceLabel, out audienceValue);
            Console.WriteLine ("Audience : " + audienceValue);
            if (!string.IsNullOrEmpty(audienceValue))
            {
                Uri audienceValueUri = new Uri(audienceValue);
                if (audienceValueUri.Equals(this.trustedAudienceValue))
                {
                    return true;
                }
            }

            return false;
        }

        private bool IsIssuerTrusted(string token)
        {
            Dictionary<string, string> tokenValues = this.GetNameValues(token);

            string issuerName;

            tokenValues.TryGetValue(this.issuerLabel, out issuerName);

            if (!string.IsNullOrEmpty(issuerName))
            {
                if (issuerName.Equals(this.trustedTokenIssuer))
                {
                    return true;
                }
            }

            return false;
        }

        private bool IsHMACValid(string swt, byte[] sha256HMACKey)
        {
            string[] swtWithSignature = swt.Split(new string[] { "&" + this.hmacSHA256Label + "=" }, StringSplitOptions.None);

            if ((swtWithSignature == null) || (swtWithSignature.Length != 2))
            {
                return false;
            }

            HMACSHA256 hmac = new HMACSHA256(sha256HMACKey);

            byte[] locallyGeneratedSignatureInBytes = hmac.ComputeHash(Encoding.ASCII.GetBytes(swtWithSignature[0]));

            string locallyGeneratedSignature = HttpUtility.UrlEncode(Convert.ToBase64String(locallyGeneratedSignatureInBytes));

            return locallyGeneratedSignature == swtWithSignature[1];
        }

        private bool IsExpired(string swt)
        {
            try
            {
                Dictionary<string, string> nameValues = this.GetNameValues(swt);
                string expiresOnValue = nameValues[this.expiresLabel];
                ulong expiresOn = Convert.ToUInt64(expiresOnValue);
                ulong currentTime = Convert.ToUInt64(GenerateTimeStamp());

                if (currentTime > expiresOn)
                {
                    return true;
                }

                return false;
            }
            catch (KeyNotFoundException)
            {
                throw new ArgumentException();
            }
        }
    }