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にあるものに合わせます。
以下コードの説明。
名前空間などは省略してあります。
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 }
);
}
}
この部分はデフォルトで用意されたままです。
public class Global : HttpApplication
{
void Application_Start(object sender, EventArgs e)
{
AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);
}
}
これも用意されたまま使っています。
public class User : IUser<string>
{
public string Id => UserName;
public string UserName { get; set; }
}
アプリ内でユーザーを表すクラスです。
あまり利用しないので最低限の機能だけつけました。
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対策なので認証とは関係ありません。
[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 CA
"B13EC36903F8BF4701D498261A0802EF63642BC3" // DigiCert High Assurance EV Root CA
})
});
}
}
ConsumerKey とConsumerSecret はTwitterからもらったものを記入してください。
BackchannelCertificateValidator に設定してある英数字の羅列は、認証方法の設定を登録しているようです。
普通はなくても動くようなのですが、必要とされる条件がわかりませんでした。
localhostになっているのが怪しいかもしれません。
[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; }
}
モデル。最小限の機能です。
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);
}
}
コントローラー。
適当にメッセージを設定します。
ここでログインユーザーの名前は利用できます。
@model Web.Models.Message
@{
ViewBag.Title = "Index";
}
@Model.Value
ビュー。上記のHomeControllerで設定したメッセージを表示します。
@modelにあるのはモデルの名前空間込みのクラス名です。
@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>© @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が用意したままで、かつたぶん認証と関係ないので省略します。