DHJJ DHJJ [Hatsune's Journal Japan]

ASP.NETでRDBMSやテキストファイルを使って認証する

BASIC認証

通常、ASP.NETで動作するインターネット上のWEBアプリにおいてBASIC認証を行いときは、IIS認証として「基本認証」、ASP.NET認証としては「Windows」を指定して、ユーザIDとパスワードの組み合わせはWindowsユーザとして登録しておかなければなりません。

しかし、この方法では不特定多数のユーザに対してユーザ認証を行おうと考えたときに、Windowsユーザとして不特定多数のユーザが登録される事になり、この点を好まないお客様も多いと思います。

そこで、IIS認証とASP.NET認証をバイパスしてWEBアプリでBASIC認証を実装することで、RDBMSやテキストファイル(アクセス権には充分に注意が必要です)を使ってBASIC認証を行う方法をご紹介します。

web.config

authentication要素としてNoneを指定してASP.NETの認証系をバイパスします。そして、authorizationのdeny要素として「users="?"」を指定してフォルダ配下丸ごとユーザ認証が必要な状態にします。

オマケとしてlocation要素を指定してerrフォルダだけ認証しなくてもOKなように設定しています。ここにWEBアプリ共通のエラー画面などを用意してcustomErrors要素のdefaultRedirectとして指定しておくと認証エラー時にもエラー画面として使用できます。これがないとエラー画面ですら認証が必要になり堂々巡りになってしまいます。

<? xml version="1.0" ?>
<configuration>
  <appSettings />
  <connectionStrings />
  <!--  -->
  <location path="err">
    <system.web>
      <authorization>
        <allow users="*" />
      </authorization>
    </system.web>
  </location>
  <!--  -->
  <system.web>
    <compilation explicit="true" strict="false" debug="true" />
      <pages>
        <namespaces>
          <clear />
            <add namespace="system" />
            <add namespace="system.collections" />
            <add namespace="system.collections.specialized" />
            <add namespace="system.configuration" />
            <add namespace="system.text" />
            <add namespace="system.text.regularexpressions" />
            <add namespace="system.web" />
            <add namespace="system.web.caching" />
            <add namespace="system.web.sessionstate" />
            <add namespace="system.web.security" />
            <add namespace="system.web.profile" />
            <add namespace="system.web.ui" />
            <add namespace="system.web.ui.webcontrols" />
            <add namespace="system.web.ui.webcontrols.webparts" />
            <add namespace="system.web.ui.htmlcontrols" />
        </namespaces>
      </pages>
    <authentication mode="None" />
    <authorization>
      <deny users="?" />
    </authorization>
    <globalization uiCulture="ja" culture="ja-jp" requestEncoding="utf-8" responseEncoding="utf-8" fileEncoding="utf-8" />
  </system.web>
</configuration>

Global.asax

web.configの編集が完了したら、プロジェクトに[新しい項目の追加]でGlobal.asaxファイルを追加して認証をフックしてBASIC認証を行う部分を記述します。

<%@ Application Language="VB" %>
<SCRIPT runat="server">
  ''' <summary>
  ''' BASIC認証用のヘッダを付与
  ''' </summary>
  ''' <param name="sender"></param>
  ''' <param name="e"></param>
  ''' <remarks>401エラー時にWWW-Authenticateヘッダを付与</remarks>
  Protected Sub Application_EndRequest(ByVal sender As Object, ByVal e As System.EventArgs)
    Dim appl As HttpApplication = CType(sender, HttpApplication)
    
    If appl.Response.StatusCode = 401 Then
      appl.Response.AppendHeader("WWW-Authenticate", "Basic Realm Hatsue-Check")
    End If
  End Sub
  ''' <summary>
  ''' BASIC認証を行う。
  ''' </summary>
  ''' <param name="sender"></param>
  ''' <param name="e"></param>
  ''' <remarks></remarks>
  Protected Sub Application_AuthenticateRequest(ByVal sender As Object, ByVal e As System.EventArgs)
    Dim appl As HttpApplication = CType(sender, HttpApplication)
    Dim authBase64 As String = appl.Request.Headers("Authorization")
    
    If Not (authBase64 Is Nothing) AndAlso authBase64.Length > 5 AndAlso authBase64.IndexOf("Basic", 0) = 0 Then
      Dim auth As String = New ASCIIEncoding().GetString(Convert.FromBase64String(authBase64.Substring(6).Trim))
      Dim uid() As String = auth.Split(":")
      If uid(0) = uid(1) Then
        Dim MyIdentity As New System.Security.Principal.GenericIdentity(uid(0))
        Dim MyStringArray As String() = {"BUILTIN\Users"}
        appl.Context.User = New System.Security.Principal.GenericPrincipal(MyIdentity, MyStringArray)
      End If
    End If
  End Sub
</SCRIPT>

大まかなプログラムの流れとしては次のようになります。

  1. 認証前であれば401エラーなので、Application_EndRequestプロシージャにてWWW-Authenticateヘッダを付与して、ブラウザがBASIC認証のダイアログを表示するように返信
  2. BASIC認証ダイアログでの入力値はAuthorizationヘッダにBASE64でエンコードされて設定されてくるので、Application_AuthenticateRequestプロシージャで値を取得
  3. 取得した値はBASE64でデコードしてSplit関数でIDとパスワードに分離
  4. 独自方法でIDとパスワードの正当性を評価(今回はID=パスワード)
  5. System.Security.Principal.GenericIdentityに入力されたIDを設定
  6. System.Security.Principal.GenericPrincipalにて、BUILTIN\Usersに擬装し、Context.Userに設定

以上の方法により、Windowsユーザとして登録しなくてもBASIC認証が可能になります。

フォーム認証

ASP.NETにおいてBASIC認証は基本的にはIIS側でのみ認証を行う前提であり、前述のようにWEBアプリ側でBASIC認証を実現しようとするとコードの記述量も増えます。

また、そもそもBASIC認証はIISやASP.NETに限らずIDやパスワードが平文でブラウザからWEBサーバへ送信されるため盗聴の危険性があり、きちんとしたWEBアプリにしたてあげるためにはSSL通信を実現してhttpsな通信路の確立が必須です。これは社内だから社外だからではなく流れるデータにより決まることであり、例え社内であっても人事情報や顧客情報のように秘匿性の高いものについてはhttpsの利用を考慮しなければなりません。

確かに、Apacheなどで.htaccessファイルだけでBASIC認証を気軽に設定できたかも知れませんがASP.NETではWindowsネットワーク以外を考慮したとたんに実装しづらくなりますし、実装したとしても特に固有のメリット(ログインダイアログが出せるくらい?)もないのですから、ASP.NETを使うのであればForm認証による認証も検討してみるといいでしょう。

web.config

ASP.NETで動作するWEBアプリにおいてForm認証を行いときは、IIS認証として「認証なし」、ASP.NET認証としては「Form認証」と認証用aspxファイルの指定を行います。

<? xml version="1.0" ?>
<configuration>
  <appSettings />
  <connectionStrings />
  <!--  -->
  <location path="err">
    <system.web>
      <authorization>
        <allow users="*" />
      </authorization>
    </system.web>
  </location>
  <!--  -->
  <system.web>
    <compilation explicit="true" strict="false" debug="true" />
      <pages>
        <namespaces>
          <clear />
            <add namespace="system" />
            <add namespace="system.collections" />
            <add namespace="system.collections.specialized" />
            <add namespace="system.configuration" />
            <add namespace="system.text" />
            <add namespace="system.text.regularexpressions" />
            <add namespace="system.web" />
            <add namespace="system.web.caching" />
            <add namespace="system.web.sessionstate" />
            <add namespace="system.web.security" />
            <add namespace="system.web.profile" />
            <add namespace="system.web.ui" />
            <add namespace="system.web.ui.webcontrols" />
            <add namespace="system.web.ui.webcontrols.webparts" />
            <add namespace="system.web.ui.htmlcontrols" />
        </namespaces>
      </pages>
    <authentication mode="Forms"/>
      <forms loginUrl="~/Login.aspx" defaultUrl="~/Default.aspx" protection="All" path="/" timeout="30" />
    <authentication/>
    <authorization>
      <deny users="?" />
    </authorization>
    <globalization uiCulture="ja" culture="ja-jp" requestEncoding="utf-8" responseEncoding="utf-8" fileEncoding="utf-8" />
  </system.web>
</configuration>

Login.aspx

Partial Class Login
    Inherits System.Web.UI.Page

    Protected Sub OK_Button_Click(ByVal sender As Object, _
                                  ByVal e As System.EventArgs) Handles OK_Button.Click
        Dim userID As String = Me.UserID_TextBox.Text.Trim.ToUpper
        Dim password As String = Me.Password_TextBox.Text.Trim.ToUpper

        If userID = password Then
            FormsAuthentication.RedirectFromLoginPage(userID, True)
        Else
            Me.Message_Label.Text = "ユーザIDまたはパスワードに誤りがあります。"
        End If
    End Sub
End Class

大まかなプログラムの流れとしては次のようになります。

  1. web.configの設定により認証前の場合は、Login.aspxにリダイレクト
  2. ログインボタンのクリックによりOK_Button.Clickイベントが発生
  3. 独自方法でIDとパスワードの正当性を評価(今回はID=パスワード)
  4. FormsAuthentication.RedirectFromLoginPageメソッドにより、ログイン前に指定していたURLに自動リダイレクト

認証前の動きをweb.config側で制御しているので、BASIC認証のときに比べるとプログラムコードはすっきりとまとまります。

credentialsを使ったフォーム認証

ASP.NETのフォーム認証では、IDとパスワードの組み合わせをweb.configの中に明記することもできます。

この方法を使えば、あたかも.htaccessで指定するような気軽さでIDとパスワードが管理できます。

web.config

<? xml version="1.0" ?>
    <authentication mode="Forms"/>
      <forms loginUrl="~/Login.aspx" defaultUrl="~/Default.aspx" protection="All" path="/" timeout="30">
        <credentials passwordFormat="Clear">
          <user name="hatsune" password="miku" />
          <user name="user" password="hoge" />
        </credentials>
      </forms>
    <authentication/>

この例では説明の都合上、credentials要素でpasswordFormat="Clear"を指定しているので、user要素のnameとpasswordは平文で設定しています。

もし、業務で使う場合にはセキュリティを考慮して、passwordFormat="Clear"以外を指定して暗号化して設定するか、web.config全体を暗号化してください。

Login.aspx

Partial Class Login
    Inherits System.Web.UI.Page

    Protected Sub OK_Button_Click(ByVal sender As Object, _
                                  ByVal e As System.EventArgs) Handles OK_Button.Click
        Dim userID As String = Me.UserID_TextBox.Text.Trim.ToUpper
        Dim password As String = Me.Password_TextBox.Text.Trim.ToUpper

        If FormsAuthentication.Authenticate(userID, password) Then
            FormsAuthentication.RedirectFromLoginPage(userID, True)
        Else
            Me.Message_Label.Text = "ユーザIDまたはパスワードに誤りがあります。"
        End If
    End Sub
End Class

web.configでIDとパスワードを指定しているときには、FormsAuthentication.Authenticateメソッドのパラメタに画面で入力されたIDとパスワードを指定する事で設定値の組み合わせとの比較が行えます。メソッドの実行結果がTrueであれば存在する組み合わせなので、FormsAuthentication.RedirectFromLoginPageメソッドによりログイン前に指定していたURLに自動リダイレクトします。

HashPasswordForStoringInConfigFileについて

credentials要素のPasswordFormatにはSHAまたはMD5が指定できます。

credentials要素のPasswordFormatにMD5と指定されたときに用いられるのはMD5です。 MD5 (Message Digest Algorithm 5) の使用目的もSHA1と同様ですが、暗号技術評価プロジェクト (CRYPTREC:Cryptography Research and Evaluation Committees) の電子政府推奨暗号リストからははずされています。また、2007年にはIPAより脆弱性が報告されています。

出典:http://www.ipa.go.jp/security/vuln/documents/2007/JVN_19445002.html

credentials要素のPasswordFormatにSHAと指定されたときに用いられるのはSHA1です。 SHA1 (Secure Hash Algorithm 1) は、1995年に米国政府標準ハッシュ関数として採用されたハッシュ関数でデジタル署名などに使われています。8バイト以上の平文から160bitのハッシュ値を計算するのですが、特徴としては不可逆変換のためハッシュ値から平文を再現する事ができないため擬似的な暗号文としてハッシュ値を使う事もできる点です。

SHA1ハッシュ値はバイナリ値なので計算値をそのままweb.configに記述できません。 credentials要素のPasswordFormatにSHAを指定したときにuser要素のPassowrdに指定するSHA1値を求める方法は、HashPasswordForStoringInConfigFileメソッドを使う方法です。

Partial Class HashPasswordForStoringInConfigFile
    Inherits System.Web.UI.Page

    Protected Sub CipherText_Button_Click(ByVal sender As Object, _
                                          ByVal e As System.EventArgs) _
                                          Handles CipherText_Button.Click
        Me.CipherText_TextBox.Text = _
            FormsAuthentication.HashPasswordForStoringInConfigFile( _
                Me.PlainText_TextBox.Text.ToLower.Trim, "SHA1")
    End Sub
End Class

サンプルサイト

なお、SHA1はMD5よりも攻撃に強くMD5に対する攻撃方法が発見された後もSHA1に対する方法は見つかっていませんでしたが、2005年にはIPAより安全性に関する注意が発表されています。今後はSHA2などより安全性の高い方法の実装が望まれますが、より良い解決方法は簡単だからという理由でどのような形であれweb.configにパスワードを記述するのをやめRDBMSなど安全性の高い場所に記述する事だと思います。

出典:http://www.ipa.go.jp/security/enc/CRYPTREC/fy17/cryptrec20050420_report01.html

COPYRIGHT (C) 2008 初音玲 All rights reserved. / Running .NET Framework 4.0.30319.42000