DotNetOpenAuthのOpenId認証アダプタ作成

※8/25 OpenId用語訂正(RP⇒OP)


asp.net mvcアプリで、DotNetOpenAuthを使用したOpenId認証を行う際、
DotNetOpenAuthのサンプルコードのようにControllerで認証関係処理をゴリ書きするのが嫌だったので、その辺の処理をクラスに分離してみました。
FactoryクラスでCookie使ってたり、RP OP毎の(とりあえずはGoogleとYahoo)AuthAdapterクラス処理をちゃんと実装する必要はありますが、
ひと通り動くようになってるので公開しておきます。その他RP OPとして、はてなとライブドアで動作確認済です。


ログインページ(一部抜粋)

@{  
  ViewBag.Title = "ログイン";

  var returnUrl = @Url.Content("~/Home");
  if (ViewBag.ReturnUrl != null)
  {
    returnUrl = @Html.Encode(ViewBag.ReturnUrl as string);
  }
}

@if (!string.IsNullOrEmpty(ViewBag.Message))
{
  <h3 class="error">@ViewBag.Message</h3>
}

<form action="@Url.Content("~/OpenId/Authenticate?ReturnUrl=" + @returnUrl)" method="post">
  <span>Id</span>
  <input id="OpenIdIdentifier" name="OpenIdIdentifier" type="text" value="" />
  <br />
  <input type="submit" value="認証" />
</form>

ログインコントローラー

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Security;
using AuthAdapter;

namespace OpenIdLoginMVC3Sample.Controllers
{
  public class OpenIdController : Controller
  {
    private EasyAuthAdapterFactory adapterFactory;

    //
    // GET: /OpenId/

    public ActionResult Index()
    {
      ViewBag.ReturnUrl = "~/Home";
      return View("Login");
    }

    public ActionResult Logout()
    {
      FormsAuthentication.SignOut();
      return Redirect("~/Home");
    }

    public ActionResult Login()
    {
      return View();
    }

    [ValidateInput(false)]
    public ActionResult Authenticate(string ReturnUrl)
    {
      adapterFactory = new EasyAuthAdapterFactory(this);
      adapterFactory.Initialize(Request["OpenIdIdentifier"]);

      var adapter = adapterFactory.Create();
      var response = adapter.GetResponse();

      switch(response.Status)
      {
        case AuthStatus.NotYet:
          try
          {
            string identifier = Request["OpenIdIdentifier"];
            return adapter.CreateRequestAndRedirectingResponse(identifier);
          }
          catch (Exception ex)
          {
            ViewBag.Message = ex.Message;
            return View("Login");
          }

        case AuthStatus.Authenticated:
          FormsAuthentication.SetAuthCookie(response.NickName, false);
          if (!string.IsNullOrEmpty(ReturnUrl))
          {
            return Redirect(ReturnUrl);
          }
          else
          {
            return RedirectToAction("Index", "Home");
          }
        
        case AuthStatus.Canceled:
          ViewBag.Message = "認証がキャンセルされました。";
          return View("Login");

        case AuthStatus.Failed:
          ViewBag.Message = "認証に失敗しました。";
          return View("Login");

        default:
          return new EmptyResult();
      }  
    }
  }
}

認証アダプタ定義

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OpenId;
using DotNetOpenAuth.OpenId.RelyingParty;
using DotNetOpenAuth.OpenId.Extensions;
using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
using DotNetOpenAuth.OpenId.Extensions.AttributeExchange;
using System.Text.RegularExpressions;

namespace AuthAdapter
{
  /// <summary>
  /// 認証状態
  /// </summary>
  public enum AuthStatus
  {
    /// <summary>
    /// 未認証
    /// </summary>
    NotYet,
    /// <summary>
    /// 認証済み
    /// </summary>
    Authenticated,
    /// <summary>
    /// キャンセルされた
    /// </summary>
    Canceled,
    /// <summary>
    /// 失敗
    /// </summary>
    Failed
  }

  /// <summary>
  /// 認証結果
  /// </summary>
  public class AuthResult
  {
    private EasyAuthAdapter adapter;
    private IAuthenticationResponse lowResponse = null;
    private string nickname = null;

    public AuthResult(EasyAuthAdapter adapter)
    {
      this.adapter = adapter;
    }

    public AuthStatus Status
    {
      get{
        lowResponse = lowResponse == null ? adapter.GetLowResponse() : lowResponse;

        if(lowResponse == null)
          return AuthStatus.NotYet;
        if(lowResponse.Status == AuthenticationStatus.Canceled)
          return AuthStatus.Canceled;
        if (lowResponse.Status == AuthenticationStatus.Authenticated)
          return AuthStatus.Authenticated;
        return AuthStatus.Failed;
      }
    }

    public string NickName
    {
      get{
        if(nickname == null)
           nickname = adapter.GetNickName(lowResponse);
        return nickname;
      }
    }
  }

  /// <summary>
  /// 簡単認証アダプタファクトリ
  /// </summary>
  public interface IEasyAuthAdapterFactory
  {
    void Initialize(string adapterid);
    EasyAuthAdapter Create();
  }

  /// <summary>
  /// ファクトリ実装
  /// </summary>
  public class EasyAuthAdapterFactory : IEasyAuthAdapterFactory
  {
    //Cookieを使用してEasyAuthAdapterを生成する。

    private Controller controller;
    private string adapterId = null;

    public EasyAuthAdapterFactory(Controller controller)
    {
      this.controller = controller;
    }

    public void Initialize(string adapterid)
    {
      var cookie = controller.Request.Cookies["adptid"];

      if (cookie != null)
        adapterId = cookie.Value;
      if (adapterid != null)
        adapterId = this.adapterId != adapterid ? adapterid : this.adapterId;
      
      controller.Response.Cookies.Add(new HttpCookie("adptid", adapterId));
    }

    public EasyAuthAdapter Create()
    {
      if (adapterId == null)
        return null;

      var uri = new Uri(adapterId);

      if (Regex.IsMatch(uri.Host, "google"))
        return new EasyAuthForGoogle();
      if (Regex.IsMatch(uri.Host, "yahoo"))
        return new EasyAuthForYahoo();
      else
        return new EasyAuthForOther();
    }
  }

  /// <summary>
  /// 簡単認証アダプタ
  /// </summary>
  public class EasyAuthAdapter
  {
    protected OpenIdRelyingParty openid;

    public EasyAuthAdapter()
    {
      this.openid = new OpenIdRelyingParty();
    }

    public virtual bool IsTarget(string domainOrUrl)
    {
      return true;
    }

    public virtual ActionResult CreateRequestAndRedirectingResponse(string identifier)
    {
      return null;
    }
  
    public AuthResult GetResponse()
    {
      return new AuthResult(this);
    }

    internal IAuthenticationResponse GetLowResponse()
    {
      return openid.GetResponse();
    }

    internal string GetNickName(IAuthenticationResponse lowres)
    {
      if (lowres == null)
        return null;
      return CreateNickName(lowres);
    }

    protected virtual string CreateNickName(IAuthenticationResponse lowres)
    {
      return null;
    }
  }

  /// <summary>
  /// グーグル用
  /// </summary>
  public class EasyAuthForGoogle : EasyAuthAdapter
  {
    public EasyAuthForGoogle()
      : base()
    {
    
    }

    public override ActionResult CreateRequestAndRedirectingResponse(string identifier)
    {
      Identifier id;
      if(!Identifier.TryParse(identifier, out id))
        return null;

      try
      {
        //Todo:OP毎の実装
        var req = openid.CreateRequest(id);
        var freq = new FetchRequest();
        freq.Attributes.Add(new AttributeRequest("http://axschema.org/namePerson/friendly", true, 1));
        freq.Attributes.Add(new AttributeRequest("http://axschema.org/contact/country/home", true, 1));
        freq.Attributes.Add(new AttributeRequest("http://axschema.org/namePerson/first", true, 1));
        freq.Attributes.Add(new AttributeRequest("http://axschema.org/pref/language", true, 1));
        freq.Attributes.Add(new AttributeRequest("http://axschema.org/namePerson/last", true, 1));
        req.AddExtension(freq);

        return req.RedirectingResponse.AsActionResult();
      }
      catch (ProtocolException ex)
      {
        throw new Exception(ex.Message);
      }
    }

    protected override string CreateNickName(IAuthenticationResponse lowres)
    {
      //Todo:OP毎の実装
      var fres = lowres.GetExtension(typeof(FetchResponse)) as FetchResponse;
      var nickname = fres != null ? fres.Attributes[0].Values[0] : lowres.FriendlyIdentifierForDisplay;

      return nickname;
    }
  }

  /// <summary>
  /// ヤフー用
  /// </summary>
  public class EasyAuthForYahoo : EasyAuthAdapter
  {
    public EasyAuthForYahoo()
      : base()
    {
    
    }

    public override ActionResult CreateRequestAndRedirectingResponse(string identifier)
    {
      Identifier id;
      if (!Identifier.TryParse(identifier, out id))
        return null;

      try
      {
        //Todo:OP毎の実装
        var req = openid.CreateRequest(id);
        var freq = new FetchRequest();
        freq.Attributes.Add(new AttributeRequest("http://axschema.org/namePerson/friendly", true, 1));
        freq.Attributes.Add(new AttributeRequest("http://axschema.org/contact/country/home", true, 1));
        freq.Attributes.Add(new AttributeRequest("http://axschema.org/namePerson/first", true, 1));
        freq.Attributes.Add(new AttributeRequest("http://axschema.org/pref/language", true, 1));
        freq.Attributes.Add(new AttributeRequest("http://axschema.org/namePerson/last", true, 1));
        req.AddExtension(freq);

        return req.RedirectingResponse.AsActionResult();
      }
      catch (ProtocolException ex)
      {
        throw new Exception(ex.Message);
      }
    }

    protected override string CreateNickName(IAuthenticationResponse lowres)
    {
      //Todo:OP毎の実装
      var fres = lowres.GetExtension(typeof(FetchResponse)) as FetchResponse;
      var nickname = fres != null ? fres.Attributes[0].Values[0] : lowres.FriendlyIdentifierForDisplay;

      return nickname;
    }
  }

  /// <summary>
  /// そのほか
  /// </summary>
  public class EasyAuthForOther : EasyAuthAdapter
  {
    public EasyAuthForOther()
      : base()
    {
    
    }

    public override ActionResult CreateRequestAndRedirectingResponse(string identifier)
    {
      Identifier id;
      if (!Identifier.TryParse(identifier, out id))
        return null;

      try
      {
        var req = openid.CreateRequest(id);
        return req.RedirectingResponse.AsActionResult();
      }
      catch (ProtocolException ex)
      {
        throw new Exception(ex.Message);
      }
    }

    protected override string CreateNickName(IAuthenticationResponse lowres)
    {
      var nickname = lowres.FriendlyIdentifierForDisplay;
      int stIdx = nickname.LastIndexOf("/") + 1;
      return nickname.Substring(stIdx);
    }
  }
}