LoginSignup
10

More than 5 years have passed since last update.

ASP.NET Identityを利用してTwitter認証を実装する最低限のコード

Last updated at Posted at 2016-05-11

ASP.NETを学習し始めたので、
とりあえず小さなWebアプリを作ってみようと思いました。

最初にログイン機能を作らないと何もできないので、
手軽にTwitter認証ができるということなので、
ASP.NET Identityを利用して作ることにしました。

ググった方法でやってみると、確かに簡単なのですが、
Visual Studioが自動で書いてくれた大量のコードに数行追加するだけだから簡単、
という話なのでちょっと。

下手すると作る予定のWebアプリより多機能そうなユーザー管理機能をデフォルトで用意されてしまうと、
なんというか邪魔です。

なので、Twitter認証を実現する最低限の実装を調べてみました。
多少調査に手間取ったので、結果を書いておきます。

深い理解しているわけでもないのに認証関連で最小限のコードを目指したので、
セキュリティ的に重要なコードが抜けている可能性があります。
気付いた方がいたら指摘ください。

使ったのは
Visual Studio Community 2015
ASP.NET MVCのバージョンは5.2.3でした。

NuGetでMicrosoft.AspNet.Identity.Owinを入れておく必要があるようです。

ASP.NETのプロジェクトを、EnptyにMVCのチェックをつけて作成します。

Twitter Developpersへのアプリの登録の説明はあちこちにあるので省略。
Websiteは開発用に
http://127.0.0.1:60003/
とかで。
60003の部分はポート番号なので、プロジェクトのプロパティのWebタグのプロジェクトのURLにあるものに合わせます。

以下コードの説明。
名前空間などは省略してあります。

/App_Start/RouteConfig.cs
public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
    }
}

この部分はデフォルトで用意されたままです。

/Global.asax
public class Global : HttpApplication
{
    void Application_Start(object sender, EventArgs e)
    {
        AreaRegistration.RegisterAllAreas();
        RouteConfig.RegisterRoutes(RouteTable.Routes);            
    }
}

これも用意されたまま使っています。

/Models/User.cs
    public class User : IUser<string>
    {
        public string Id => UserName;

        public string UserName { get; set; }
    }

アプリ内でユーザーを表すクラスです。
あまり利用しないので最低限の機能だけつけました。

/Models/UserStore.cs
public class UserStore : IUserStore<User>
{
    static List<User> cache = new List<User>();

    public void Dispose()
    {
    }

    public Task<User> FindByNameAsync(string userName)
    {
        return Task.FromResult(
            (from user in cache
                where user.UserName == userName
                select user)
            .SingleOrDefault());
    }

    public Task CreateAsync(User user)
    {
        if (!cache.Any(already => already.Id == user.Id))
        {
            cache.Add(user);
        }
        return Task.CompletedTask;
    }

    public Task DeleteAsync(User user) { throw new NotImplementedException(); }

    public Task<User> FindByIdAsync(string userId) { throw new NotImplementedException(); }

    public Task UpdateAsync(User user) { throw new NotImplementedException(); }
}

アプリ内でユーザーを管理するクラス。
これもできるだけ最低限の機能で済ませました。
ユーザーはstatic変数に突っ込んであるだけなので、アプリ再起動したら消えますが、
ユーザー情報をほとんど利用しないならこれでなんとかなります。
普通はDBに保存とかしたほうが良いでしょう。

Task.FromResultとかTask.CompletedTaskはあまり見かけない機能かもしれませんが、
これは単にasyncメソッドのWarning対策なので認証とは関係ありません。

/Startup.cs
[assembly: OwinStartup(typeof(Startup))]
public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.CreatePerOwinContext<UserManager<User>>(() => new UserManager<User>(new UserStore()));
        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/Home/Index"),
            Provider = new CookieAuthenticationProvider
            {
                OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<UserManager<User>, User>(
                    TimeSpan.FromMinutes(30),
                    async (manager, user) => await manager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie))
            }
        });
        app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

        app.UseTwitterAuthentication(
            new TwitterAuthenticationOptions
            {
                ConsumerKey = "TwitterからもらったConsumer Key (API Key)をここに書き込んでください。",
                ConsumerSecret = "TwitterからもらったConsumer Secret (API Secret)をここに書き込んでください。",
                BackchannelCertificateValidator =
                new CertificateSubjectKeyIdentifierValidator(
                    new[]
                    {
                        "A5EF0B11CEC04103A34A659048B21CE0572D7D47", // VeriSign Class 3 Secure Server CA - G2
                        "0D445C165344C1827E1D20AB25F40163D8BE79A5", // VeriSign Class 3 Secure Server CA - G3
                        "7FD365A7C2DDECBBF03009F34339FA02AF333133", // VeriSign Class 3 Public Primary Certification Authority - G5
                        "39A55D933676616E73A761DFA16A7E59CDE66FAD", // Symantec Class 3 Secure Server CA - G4
                        "4eb6d578499b1ccf5f581ead56be3d9b6744a5e5", // VeriSign Class 3 Primary CA - G5
                        "5168FF90AF0207753CCCD9656462A212B859723B", // DigiCert SHA2 High Assurance Server C‎A 
                        "B13EC36903F8BF4701D498261A0802EF63642BC3" // DigiCert High Assurance EV Root CA
                    })
            });
    }
}

ConsumerKey とConsumerSecret はTwitterからもらったものを記入してください。

BackchannelCertificateValidator に設定してある英数字の羅列は、認証方法の設定を登録しているようです。
普通はなくても動くようなのですが、必要とされる条件がわかりませんでした。
localhostになっているのが怪しいかもしれません。

/Controllers/AccountController.cs
[Authorize]
public class AccountController : Controller
{
    public ActionResult LogOff()
    {
        HttpContext.GetOwinContext().Authentication.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
        return RedirectToAction("Index", "Home");
    }

    [AllowAnonymous]
    public ActionResult ExternalLogin()
    {
        return new ChallengeResult("Twitter", Url.Action("ExternalLoginCallback", "Account"));
    }

    [AllowAnonymous]
    public async Task<ActionResult> ExternalLoginCallback()
    {
        var loginInfo = await HttpContext.GetOwinContext().Authentication.GetExternalLoginInfoAsync();

        if (loginInfo.ExternalIdentity.IsAuthenticated)
        {
            var manager = HttpContext.GetOwinContext().GetUserManager<UserManager<User>>();
            var user = new User { UserName = loginInfo.DefaultUserName};
            await manager.CreateAsync(user);
            var identity = manager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);
            HttpContext.GetOwinContext().Authentication.SignIn(identity);
        }
        return RedirectToAction("Index", "Home");
    }

    internal class ChallengeResult : HttpUnauthorizedResult
    {

        public ChallengeResult(string provider, string redirectUri)
        {
            LoginProvider = provider;
            RedirectUri = redirectUri;
        }

        public string LoginProvider { get; set; }
        public string RedirectUri { get; set; }

        public override void ExecuteResult(ControllerContext context)
        {
            context.HttpContext.GetOwinContext().Authentication.Challenge(
                new AuthenticationProperties { RedirectUri = RedirectUri },
                LoginProvider);
        }
    }
}

認証用のコントローラーです。

認証用のコードはここまでで、
あとはこれを利用する最小限のMVCとなります。

public class Message
{
    public string Value { get; set; }
}

モデル。最小限の機能です。

/Controllers/HomeController.cs
public class HomeController : Controller
{

    public ActionResult Index()
    {
        var model = new Message();
        if (Request.IsAuthenticated)
        {
            model.Value = $"ようこそ{User.Identity.Name}さん。";
        }
        else
        {
            model.Value = "ログインしてください。";
        }
        return View(model);
    }
}

コントローラー。
適当にメッセージを設定します。
ここでログインユーザーの名前は利用できます。

/Views/Home/Index.cshtml
@model Web.Models.Message
@{
    ViewBag.Title = "Index";
}

@Model.Value

ビュー。上記のHomeControllerで設定したメッセージを表示します。
@modelにあるのはモデルの名前空間込みのクラス名です。

/Views/Shared/_Layout.cshtml
@using Microsoft.Owin.Security
@using System.Web
@using Microsoft.AspNet.Identity
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - </title>
    <link href="~/Content/Site.css" rel="stylesheet" type="text/css" />
    <link href="~/Content/bootstrap.min.css" rel="stylesheet" type="text/css" />
    <script src="~/Scripts/modernizr-2.6.2.js"></script>
</head>
<body>
    <div class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                @Html.ActionLink("ホーム", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })

            </div>
            <div class="nav navbar-nav navbar-right">
                @if (Request.IsAuthenticated)
                {

                    <a href=@Url.Action("LogOff", "Account") class="btn btn-link">
                        <img src="~/Images/TwitterLogo.png" />
                        ログアウト
                    </a>
                }
                else
                {
                    <a href=@Url.Action("ExternalLogin", "Account") class="btn btn-link">
                        <img src="~/Images/TwitterLogo.png" />
                        ログイン
                    </a>
                }
            </div>
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav"></ul>
            </div>
        </div>
    </div>

    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; @DateTime.Now.Year - Marimo</p>
        </footer>
    </div>

    <script src="~/Scripts/jquery-1.10.2.min.js"></script>
    <script src="~/Scripts/bootstrap.min.js"></script>
</body>
</html>

ここだけは最低限ではないです。
最低限にすると見た目がたいへんしょぼくなるので。

ログインとログアウトのリンクはここに書いてあります。
真ん中あたりですね。
ログイン済みかどうかで切り替えています。

ほかいろいろ、MVCで必要なファイルはありますが、
Visual Studioが用意したままで、かつたぶん認証と関係ないので省略します。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10