2007-04-26

λ [.NET] livedoor Auth ASP.NET用認証ページ

Web.Config に authentication mode="Forms" を書いてやって、プログラムを Login.aspx として設置するだけというのを目指して作成した。 内部的には、livedoor Authが通ったら FormsAuthenticationTicket を作ってクッキーに書き込むことで Forms 認証を実現している。 通常のコンテンツページには全く影響がなく、Web.Configも至って普通なのでこれを用いたLivedoorID対応はかなり容易のはず。

要求するアクセス権が userhash の時は、ASP.NET的なユーザ名には userhash を設定し、 要求するアクセス権が id の時は、ASP.NET的なユーザ名は LivedoorID を設定する。

Web.Configの例。authdir/ 以下に認証したユーザしか利用できないページを置いておくというのを想定。

<?xml version="1.0"?>
<configuration>
 <appSettings/>
 <connectionStrings/>
 <system.web>
   <authentication mode="Forms" />
 </system.web>
 <location path="authdir">
   <system.web>
     <authorization>
       <deny users="?" />
     </authorization>
   </system.web>
 </location>
</configuration>

Login.aspx.cs:

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Xml;

public partial class Login : Page
{
    // 環境毎に変更する部分ここから
    private string アプリケーションキー = "????????????????????????????????";
    private string 秘密鍵 = "????????????????";
    private string 要求するアクセス権 = "id"; // userhash または id
    // 環境毎に変更する部分ここまで

    private static readonly DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
    private const int userdata最大文字数 = 255;
    private const int 認証クッキータイムアウト分 = 30;

    private static readonly Regex reReturnUrl =
        new Regex(@"[A-Za-z0-9_\.\/]+$", RegexOptions.Compiled | RegexOptions.Singleline);

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            if (Request.QueryString["app_key"] == アプリケーションキー)
            {
                string sig = GetSignature(Request.QueryString, 秘密鍵);
                if (sig == Request.QueryString["sig"])
                {
                    コールバック処理(Request.QueryString);
                }
            }
            else
            {
                ログインリダイレクト処理(); // コールバックでない場合はlivedoorAuthログインページに飛ぶ
            }
        }
    }

    private void ログインリダイレクト処理()
    {
        string userdata = Request.QueryString["ReturnURL"];
        if (!reReturnUrl.IsMatch(userdata)) userdata = ""; // 英数字アンダースコアピリオド以外の文字で構成された文字が含まれていたら DefaultUrl に変更する。

        // 文字数制限がエンコード後かどうかが確信できないので、エンコード後の長さで一応チェックしておく。
        string userdataUrlEncoded = HttpUtility.UrlEncode(userdata);
        if (userdataUrlEncoded.Length > userdata最大文字数) throw new ApplicationException("userdataの文字数がオーバーしています");

        NameValueCollection param = new NameValueCollection();
        param.Add("app_key", アプリケーションキー);
        param.Add("v", "1.0");
        param.Add("t", GetEpoch());
        param.Add("perms", 要求するアクセス権);
        param.Add("userdata", userdata);

        string redir = ログイン用URLの作成(param);
        Response.Redirect(redir);
    }

    private string ログイン用URLの作成(NameValueCollection param)
    {
        string sig = GetSignature(param, 秘密鍵);
        string t = param["t"];
        string userdata = param["userdata"];
        return
            String.Format("http://auth.livedoor.com/login/?app_key={0}&perms={1}&t={2}&v=1.0&userdata={3}&sig={4}",
                          アプリケーションキー, 要求するアクセス権, t, HttpUtility.UrlEncode(userdata), sig);
    }

    /// <summary>
    /// sigが正しいことを確認した後の処理
    /// </summary>
    private void コールバック処理(NameValueCollection param)
    {
        switch (要求するアクセス権)
        {
            case "userhash":
                // ユーザーを一意に識別する文字列である userhash をユーザ名として登録する
                RegisterPrincipalAndFormsCookie(param["userhash"]);
                break;
            case "id":
                Response.Cookies.Add(new HttpCookie("LivedoorUserhash", param["userhash"]));
                RegisterPrincipalAndFormsCookie(GetLivedoorID(param["token"]));
                break;
        }

        // userdata が利用可能であれば、そのURLをReturnURLとしてリダイレクトする
        string ReturnURL = param["userdata"];
        if (reReturnUrl.IsMatch(ReturnURL)) Response.Redirect(ReturnURL);
        Response.Redirect(FormsAuthentication.DefaultUrl);
    }

    /// <summary>
    /// LivedoorID をRPC経由で取得する
    /// </summary>
    /// <param name="token">トークン</param>
    /// <returns></returns>
    private string GetLivedoorID(string token)
    {
        string livedoorID;

        {
            NameValueCollection param = new NameValueCollection();
            param.Add("app_key", アプリケーションキー);
            param.Add("format", "xml");
            param.Add("v", "1.0");
            param.Add("t", GetEpoch());
            param.Add("token", token);
            param.Add("sig", GetSignature(param, 秘密鍵));

            string url = LivedoorID取得RPC用URLの作成();
            string errorcode;
            string message;
            HttpWebRequest wr = (HttpWebRequest) WebRequest.Create(url);
            wr.Method = "POST";
            wr.ContentType = "application/x-www-form-urlencoded";
            string postData = GetPostData(param);
            byte[] bytearr = Encoding.UTF8.GetBytes(postData);
            wr.ContentLength = bytearr.Length;
            Stream webRequestStream = wr.GetRequestStream();
            webRequestStream.Write(bytearr, 0, bytearr.Length);
            webRequestStream.Close();

            WebResponse res = wr.GetResponse();
            using (Stream st = res.GetResponseStream())
            {
                XmlReader xr = XmlTextReader.Create(st);
                xr.Read();
                xr.ReadStartElement("response");
                xr.ReadStartElement("error");
                errorcode = xr.ReadString();
                xr.ReadEndElement(); // error
                xr.ReadStartElement("message");
                message = xr.ReadString();
                xr.ReadEndElement(); // message
                xr.ReadStartElement("user");
                xr.ReadStartElement("livedoor_id");
                livedoorID = xr.ReadString();
                xr.ReadEndElement(); // livedoor_id
                xr.ReadEndElement(); // user
                xr.ReadEndElement(); // response
            }
            Trace.Write(
                String.Format("LivedoorID RPC result error:{0} message:{1} livedoor_id:{2}", errorcode, message,
                              livedoorID));
        }
        return livedoorID;
    }

    private static string GetPostData(NameValueCollection param)
    {
        StringBuilder s = new StringBuilder();
        foreach (string key in param.Keys)
        {
            s.Append(HttpUtility.UrlEncode(key));
            s.Append("=");
            s.Append(HttpUtility.UrlEncode(param[key]));
            s.Append("&");
        }
        if (s.Length > 0) s.Length = s.Length - 1; // 最後の&を除去
        return s.ToString();
    }

    /// <summary>
    /// 認証されたユーザIDとしてFormsクッキーに登録する
    /// 登録後ログイン処理前に利用していたURLにリダイレクトする
    /// </summary>
    /// <param name="username"></param>
    private void RegisterPrincipalAndFormsCookie(string username)
    {
        FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
            1,
            username,
            DateTime.Now,
            DateTime.Now.AddMinutes(認証クッキータイムアウト分),
            false /* isPersistent*/,
            "" /* userData */,
            FormsAuthentication.FormsCookiePath);

        string encTicket = FormsAuthentication.Encrypt(ticket);
        Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, encTicket));

        // LivedoorAuth だと一度 livedoor 側に移動するため、ReturnURL が保持されない
        // そのため一度 userdata に入れている。
        // Response.Redirect(FormsAuthentication.GetRedirectUrl(username, false /* createPersistentCookie */ ));
    }

    private static string LivedoorID取得RPC用URLの作成()
    {
        // RPC側はPOSTで送らなければならない
        return "http://auth.livedoor.com/rpc/auth";
    }

    private static string GetSignature(NameValueCollection パラメータ, string 秘密鍵)
    {
        HMACSHA1 sha1 = new HMACSHA1(Encoding.ASCII.GetBytes(秘密鍵));

        StringBuilder s = new StringBuilder();
        List<string> keylist = new List<string>();
        foreach (string key in パラメータ.Keys) keylist.Add(key);
        keylist.Sort();

        s.Length = 0; // StringBuilder初期化
        foreach (string key in keylist)
        {
            if (key == "sig")
                continue; // ここでは "sig" は無視する。コールバックURLで sig 付の NameValueCollection(QueryString) を渡すことを可能にするため。
            s.Append(key);
            s.Append(パラメータ[key]);
        }
        byte[] sha1bytes = sha1.ComputeHash(Encoding.ASCII.GetBytes(s.ToString()));

        s.Length = 0; // StringBuilder初期化
        foreach (byte b in sha1bytes)
        {
            s.Append(b.ToString("x2"));
        }
        return s.ToString();
    }

    /// <summary>
    /// エポック秒を文字列で返す
    /// </summary>
    /// <returns></returns>
    private static string GetEpoch()
    {
        return Convert.ToInt64(DateTime.Now.ToUniversalTime().Subtract(epoch).TotalSeconds).ToString();
    }
}

Login.aspx はLogin.aspx.csの中の処理が失敗した場合の表示を適当に書いておけばよい。処理がうまく動いている場合はリダイレクトされるので表示されない。

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Login.aspx.cs" Inherits="Login" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>livedoor Auth 認証処理ページ</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    この表示が出る場合は何らかの問題があります。
    </div>
    </form>
</body>
</html>
[]