Nancyでのフォーム認証

ナンシーフォーム認証を行うには、以下の流れで行います。

1, Nancy.Authentication.Formsパッケージのインストール
2, IUserMapper実装クラスの作成
3, ログイン、ログアウトルートの実装
4, フォーム認証の設定

IUserMapper実装クラスの作成

IUserMapperインターフェイス、IUserIdentityインターフェイスの実装クラスを作成します。前者はログイン処理で入力されたユーザーID・パスワードのマッピング処理を、後者はユーザー識別情報を表します。NancyではユーザーIDをGUIDで表します。(そのようなNancyの仕様みたい)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Nancy;
using Nancy.Authentication.Forms;
using Nancy.Security;

namespace NancyFormAuthSample
{
    public class SampleUserInfo : IUserIdentity
    {
        public IEnumerable<string> Claims
        {
            get;
            set;
        }

        public string UserName
        {
            get;
            set;
        }
    }

    public class UserDatabase : IUserMapper
    {
        //ユーザー情報ストア。サンプルの為、staticなメンバを使用している。
        private static List<Tuple<string, string, Guid>> users = new List<Tuple<string, string, Guid>>();

        public UserDatabase()
        {
            users.Add(new Tuple<string, string, Guid>("admin", "password", Guid.NewGuid()));
            users.Add(new Tuple<string, string, Guid>("user", "password", Guid.NewGuid()));
        }

        public IUserIdentity GetUserFromIdentifier(Guid identifier, NancyContext context)
        {
            var userRecord = users.Where(u => { return u.Item3 == identifier; }).FirstOrDefault();
            return userRecord == null
                ? null
                : new SampleUserInfo() { UserName = userRecord.Item1 };
        }

        //IUserMapperインターフェイスには含まれていないが、ここでユーザー認証処理を定義している。
        //戻り値として、ログイン入力値に対応するユーザーID値を返す。
        public static Guid? ValidateUser(string username, string password)
        {
            var userRecord = users.Where(u => { return u.Item1 == username && u.Item2 == password; }).FirstOrDefault();

            if (userRecord == null)
            {
                return null;
            }

            return userRecord.Item3;
        }
    }
}

ログイン、ログアウトルートの実装

以下サンプルソースの場合は、
/loginにPOSTリクエストし、ログインを行い、ログイン失敗したら/login?error=trueにリダイレクト。成功したら/secureへリダイレクトします。
/secureは認証済でないとアクセスできません。ログアウトするには/secure/logoutへアクセスします。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Nancy;
using Nancy.Authentication.Forms;
using Nancy.Extensions;
using Nancy.Security;

namespace NancyFormAuthSample
{
    public class MyModule : NancyModule
    {
        public IndexModule() : base("/login")
        {
            Get["/"] = _ =>
            {
                //クエリ文字列は、Context.Request.Queryで取得する
                if (Context.Request.Query.error != null)
                {
                    return Response.AsText("ユーザーIDまたはパスワードが不正です。", "text/plain; charset=UTF-8");
                }
                else
                {
                    return Response.AsText("ログインする際はPostリクエストしてください。", "text/plain; charset=UTF-8");
                }
            };

            Post["/"] = _ =>
            {
                var userGuid = UserDatabase.ValidateUser(Context.Request.Form.Username, Context.Request.Form.Password);

                if (userGuid == null)
                {
                    //リダイレクト先起点は、モジュールのルートパス起点ではないので注意すること。
                    return Context.GetRedirect("/login?error=true");
                }

                DateTime? expiry = null;
                if (Request.Form.RememberMe.HasValue)
                {
                    expiry = DateTime.Now.AddDays(7);
                }

                return Nancy.Authentication.Forms.ModuleExtensions.LoginAndRedirect(this, userGuid, expiry, "/secure");
            };
        }
    }
    
    public class SecureModule : NancyModule
    {
        public SecureModule()
            : base("/secure")
        {
            //このモジュールルートへのアクセスは認証済である必要あり。
            this.RequiresAuthentication();

            Get["/"] = _ =>
            {
                string msg = string.Format("{0}さん、セキュアなページです。", Context.CurrentUser.UserName);
                return Response.AsText(msg, "text/plain; charset=UTF-8");
            };

            Get["/logout"] = _ =>
            {
                return this.LogoutAndRedirect("/login");
            };
        }
    }
}

フォーム認証の設定

Bootstrapperのサブクラスを作成し、フォーム認証設定を行います。

using Nancy;
using Nancy.Authentication.Forms;
using Nancy.Bootstrapper;
using Nancy.TinyIoc;

namespace NancyFormAuthSample
{
    public class MyBootstrapper : DefaultNancyBootstrapper
    {
        protected override void ConfigureRequestContainer(TinyIoCContainer container, NancyContext context)
        {
            base.ConfigureRequestContainer(container, context);
            
            //TinyIoCContainerにIUserMapperの割り付けを登録
            container.Register<IUserMapper, UserDatabase>();
        }

        protected override void RequestStartup(TinyIoCContainer container, IPipelines pipelines, NancyContext context)
        {
            base.RequestStartup(container, pipelines, context);

            var formsAuthConfiguration = new FormsAuthenticationConfiguration()
            {
                RedirectUrl = "/login", //認証失敗時のリダイレクト先
                UserMapper = container.Resolve<IUserMapper>()
            };
            FormsAuthentication.Enable(pipelines, formsAuthConfiguration);  //フォーム認証の有効化
        }
    }
}