WebSurfer's Home

Filter by APML

ASP.NET Core Identity の SecurityStamp

by WebSurfer 8. November 2024 18:06

ASP.NET Core Identity の SecurityStamp についていろいろ調べましたので、調べたことを備忘録として書いておきます。

どういう機能かを簡単に書くと、複数のユーザーが同じアカウントで同時にログインしている場合に、あるユーザーがパスワードの変更など重要なセキュリティに関わる変更を行った場合、他のログインユーザーの次回のアクセス時にサインアウトさせるというもので、デフォルトで有効に設定されています。

ASP.NET Core Identity が使うデータベースの AspNetUsers テーブルの SecuriryStamp フィールドと、認証クッキーに含まれる SecuriryStamp の値を使って上記の機能を実現しています。

下の画像はデータベースの AspNetUsers テーブルの SecuriryStamp 列の各ユーザーの値を SQL Server Management Studio で見たものです。

AspNetUsers テーブルの SecurityStamp 列

ユーザーがログインすると認証クッキーが発行されますが、その際データベースの SecurityStamp の値を Claim として認証クッキーに含めます。下の画像の赤枠部分を見てください。上の画像のデータベースの SecurityStamp の値と同じになっています。

認証クッキーに含まれる SecurityStamp

一旦ユーザーがログインに成功すると、それ以降はそのユーザーがサイトにアクセスしてくるたび認証クッキーが送信され、送られてきた認証クッキーをチェックして有効であればログインが継続されるという仕組みになっています。

ログインしたユーザーは自分のアカウントの設定を変更する管理画面にアクセスできます。下の画面は自分のパスワードを変更するページです。

パスワード変更画面

上の画面でパスワードの変更に成功すると、同時にデータベースの SecuriryStamp の値が変更されます。

パスワードを変更した際、ユーザーには認証クッキーが再発行されます。それにより、変更後のデータベースの SecuriryStamp の値と再発行された認証クッキーに含まれる SecuriryStamp の値は同じになります。

一方、同じアカウントでログインしている他のユーザーには認証クッキーは再発行されませんので、認証クッキーに含まれる SecuriryStamp の値は古いまま、すなわちデータベースと認証クッキーの SecurityStamp とは異なった値になります。

なので、あるユーザーによるパスワード変更後、他のユーザーがアクセスして来た時にデータベースと認証クッキーの SecuriryStamp の値を比較すれば異なっているので、そのユーザーをサインアウトさせることが可能です。

ただし、比較するにはデータベースにクエリを投げてデータベースの SecuriryStamp の値を取得してくる必要があります。それをユーザーがアクセスしてくるたびに行うのはサーバーの負担が大きいという問題があります。それゆえインターバル(デフォルトで 30 分)を設けています。

例えば 10,000 人のユーザーが同時にログインしていて、一人当たり 1 分に 5 回アクセスしてくるとします。インターバルを設けないでアクセスのたび毎回 SecurityStamp の比較を行うとすると、1 分間に 50,000 回データベースにクエリを投げることになります。

インターバルを 1 分にすると、ユーザー当たり 1 分に 5 回のリクエストの内 4 回は検証しないので 1/5 に、すなわち全体では 10,000 回/分に減ります。

さらに、インターバルを 30 分に増やすと 1/150 に、すなわち全体では 333 回/分に減ります。その程度であれば問題ないであろうということでデフォルトが 30 分になっているようです。

ただし、インターバルが短いほどセキュリティは高くなるはずなので、ユーザーが少なくアクセスの少ないサイトであれば短くした方が良さそうです。

Program.cs に以下のコードを追加することによりインターバルを変更できます。

// インターバルは FromMinutes(m) の m で設定。下のコードは 30 分
builder.Services.Configure<SecurityStampValidatorOptions>(options =>
        options.ValidationInterval = TimeSpan.FromMinutes(30));

詳しくは Microsoft のドキュメント「ISecurityStampValidator とすべてからのサインアウト」に書いてありますので見てください。


以上で分かった気になっていたのですが、よく考えてみると「チェックを 30 分のインターバルで行うとして、そのタイミングと、ユーザーがアクセスするタイミングは当然異なるはずだが、そこはどうしているのか?」が疑問です。そのあたりをさらに調べてみました。

上に紹介したMicrosoft のドキュメントには "Identity の既定の実装では、SecurityStampValidator がメインのアプリケーション cookie と 2 要素 cookie に登録されます。検証コントロールは、各 cookie の OnValidatePrincipal イベントにフックして Identity を呼び出し、ユーザーのセキュリティ スタンプ クレームが cookie に格納されているものから変更されていないことを確認します" と書いてあります。

具体的には、「メインのアプリケーション cookie」の方でいうと、そのために以下のオプション設定がデフォルトでされているということのようです。

builder.Services.ConfigureApplicationCookie(options =>
{
    // cookie 設定

    options.Events = new CookieAuthenticationEvents
    {
        OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
    };
});

上のコードで行っているのは以下の通りです。

CookieAuthenticationOptionsEvents プロパティCookieAuthenticationEvents (Allows subscribing to events raised during cookie authentication) を設定。

CookieAuthenticationEvents の OnValidatePrincipal プロパティ (Invoked to validate the principal) に SecurityStampValidator クラスValidatePrincipalAsync 静的メソッド (Validates a principal against a user's stored security stamp) を設定

ということで、

  1. ユーザーが認証クッキーを持ってアクセスしてくる
  2. cookie authentication プロセスが行われる
  3. CookieAuthenticationEvents が種々イベントをサブスクライブしている
  4. ValidatePrincipal イベントが発生すると ValidatePrincipalAsync が invoke される
  5. cookie と DB の SecurityStamp が異なっているとユーザーをサインアウトさせる

・・・というプロセスになるようです。

ValidatePrincipal イベントが発生するたび ValidatePrincipalAsync が無条件に呼ばれるようですが、その際上のステップ 5 を行うか否かにインターバルが関係しています。

そのあたりの詳細はググって調べてヒットするドキュメントでは分からなかったので Copilot に聞いてみたところ以下の回答でした。要するにミドルウェアが良しなにやっているとのことです。

  1. Initial Login: When the user logs in, an authentication ticket with the security stamp is issued and stored in the authentication cookie.
  2. Request Handling: When a request comes in, the authentication middleware reads the cookie and the authentication ticket.
  3. Interval Check: The middleware checks if the time since the last validation exceeds the ValidationInterval. This check is done based on the current time and the internally tracked last validation timestamp.
  4. Update: If the validation occurs, the middleware updates its internal record of the last validation time.

AI の回答は間違っていることもあるので、裏を取��ため SecurityStampValidator.cs の ValidatePrincipalAsync メソッドが呼び出す ValidateAsync メソッドのコードを調べてました。Copilot の言っていることに間違いはなさそうです。

参考に SecurityStampValidator.cs の ValidateAsync メソッドのコードをコピーして以下に載せておきます

public virtual async Task ValidateAsync(CookieValidatePrincipalContext context)
{
    var currentUtc = TimeProvider.GetUtcNow();
    var issuedUtc = context.Properties.IssuedUtc;
 
    // Only validate if enough time has elapsed
    var validate = (issuedUtc == null);
    if (issuedUtc != null)
    {
        var timeElapsed = currentUtc.Subtract(issuedUtc.Value);
        validate = timeElapsed > Options.ValidationInterval;
    }
    if (validate)
    {
        var user = await VerifySecurityStamp(context.Principal);
        if (user != null)
        {
            await SecurityStampVerified(user, context);
        }
        else
        {
            Logger.LogDebug(EventIds.SecurityStampValidationFailed, 
                  "Security stamp validation failed, rejecting cookie.");
            context.RejectPrincipal();
            await SignInManager.SignOutAsync();
            await SignInManager.Context.SignOutAsync(
                          IdentityConstants.TwoFactorRememberMeScheme);
        }
    }
}

その他気が付いたことも書いておきます。

上に紹介した Microsoft のドキュメントに書いてありますが、データベースの SecurityStamp を変更するには userManager.UpdateSecurityStampAsync(user) を呼び出します。

上に書いた管理画面でのパスワード変更にはそのメソッドが実装されているようで、パスワード変更操作でデータベースの SecurityStamp が変更されます。

ユーザーにアサインされるロールが変更された場合もデータベースの SecurityStamp を変更した方が良さそうですが、先の記事「ASP.NET Identity のロール管理 (CORE)」に書いた自作のメソッド EditRoleAssignment の AddToRoleAsync, RemoveFromRoleAsync ではデータベースの SecurityStamp は変わりません。SecurityStamp を変更するには userManager.UpdateSecurityStampAsync(user) の呼び出しを追加で実装する必要があります。

また、Microsoft のドキュメントに書いてあった sign out everywhere については、Logout.cshtml.cs に以下のように追加すれば実現できます。

public async Task<IActionResult> OnPost(string returnUrl = null)
{
    // ログアウトで SecurityStamp を再生成するため追加
    var user = await _userManager.GetUserAsync(User);
    await _userManager.UpdateSecurityStampAsync(user);

    await _signInManager.SignOutAsync();
    _logger.LogInformation("User logged out.");
    if (returnUrl != null)
    {
        return LocalRedirect(returnUrl);
    }
    else
    {
        // This needs to be a redirect so that the browser performs a new
        // request and the identity for the user gets updated.
        return RedirectToPage();
    }
}

もう一つ気になっていることがあります。それはデフォルトで有効になっている SlidingExpiration の影響です。これが働くと有効期間が延長された認証クッキーが再発行されますが、それによる SecurityStamp への影響が不明です。調べる気力がなくなったのでまだ未確認ですが、分かったら追記します。

Tags: , ,

Authentication

認証チケットの期限切れをユーザーに通知

by WebSurfer 11. May 2024 16:15

ASP.NET Core Identity を使ったユーザー認証システムで認証チケットの有効期限が切れたとき、下の画像のようにユーザーにその旨通知する方法を書きます。

認証チケット有効期限切れの通知

先の記事「ASP.NET Core Identity タイムアウト判定」で書いたこととほぼ同じで、違うのは先の記事ではミドルウェアを使っていたのを Login ページで行うようにし、ターゲットフレームワークを .NET Core 3.1 から .NET 8.0 に変更した点です。

Visual Studio 202 のテンプレートを使って ASP.NET Core Web アプリのプロジェクトを作る際、認証を「個別のユーザーアカウント」に設定すると ASP.NET Core Identity を利用したクッキーベースのユーザー認証が実装されます。

ユーザーがログインに成功すると、サーバーからは応答ヘッダの Set-Cookie に認証チケットを入れて認証クッキーとしてクライアント(ブラウザ)に送信します。

その後は、ブラウザは要求の都度サーバーに認証クッキーを送信するので認証状態が継続されるという仕組みになっています。

認証チケットには有効期限があります (デフォルトで 5 分、SlidingExpiration で延長あり)。一旦認証を受けたユーザーがログオフせずブラウザを立ち上げたまま長時間席を外すなどして、タイムアウトに設定した時間を超えてアクセスしなかった場合を考えてください。

タイムアウトとなった後でユーザーが席に戻ってきて再度どこかのページを要求したとすると、要求したページが匿名アクセスを許可してなければ、ログインページにリダイレクトされます。

その際、ユーザーに認証チケットが期限切れとなっていることを知らせるためには、どのようにできるかということを書きます。

一旦認証を受けたが、認証チケットが期限切れになっているというのは、Web サーバー側では以下の条件で判定できるはずです。

  1. 要求 HTTP ヘッダーに認証クッキーが含まれる。
  2. 認証クッキーの中の認証チケットが期限切れ。

一旦認証クッキーの発行を受ければ、ブラウザを閉じない限り要求の都度サーバーにクッキーを送り続けます。(下の「注 1」に書きましたが、[Remember me?]チェックボックスにチェックを入れて認証を受けた場合は話が違ってくるので注意してください)

Web サーバーが認証クッキーを取得できれば、それを復号して認証チケットを取得し、チケットの中の有効期限の日時の情報を取得できます。それで上記の 2 つの条件を確認できます。

ログインページで条件を確認して、認証チケットが期限切れになっていた場合はこの記事の一番上の画像のようにメッセージを表示してみました。

以下がログインページのコードです。スキャフォールディング機能を利用して自動生成した Login.cshtml.cs, Login.cshtml のコードに手を加えています。(スキャフォールディング方法の詳細は別の記事「ASP.NET Core MVC プロジェクトに Identity 実装」を見てください)

Login.cshtml.cs

コメントで「2024/5/11 追加」としたコードを自動生成された Login.cshtml.cs に追加しています。

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable

using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authentication;
// ・・・中略・・・

namespace MvcNet8App2.Areas.Identity.Pages.Account
{
    public class LoginModel : PageModel
    {
        private readonly SignInManager<ApplicationUser> _signInManager;
        private readonly ILogger<LoginModel> _logger;

        // 2024/5/11 追加(下の「注 2」参照)
        private readonly CookieAuthenticationOptions _options;

        public LoginModel(SignInManager<ApplicationUser> signInManager, 
                          ILogger<LoginModel> logger,

                          // 2024/5/11 追加(下の「注 2」参照)
                          IOptions<CookieAuthenticationOptions> options)
        {
            _signInManager = signInManager;
            _logger = logger;

            // 2024/5/11 追加(下の「注 2」参照)
            _options = options.Value;
        }

        // ・・・中略・・・

        public async Task OnGetAsync(string returnUrl = null)
        {
            // 2024/5/11 追加・・・
            // .AspNetCore.Identity.Application はデフォルトのクッキー名
            string cookie = Request.Cookies[".AspNetCore.Identity.Application"];
            if (!string.IsNullOrEmpty(cookie))
            {
                IDataProtectionProvider provider = 
                                      _options.DataProtectionProvider;

                // 下の「注 3」参照
                IDataProtector protector = provider.CreateProtector(
                    // CookieAuthenticationMiddleware のフルネーム
                    "Microsoft.AspNetCore.Authentication.Cookies." +
                    "CookieAuthenticationMiddleware",
                    // クッキー名 .AspNetCore.Identity.Application から
                    // .AspNetCore. を除去した文字列
                    "Identity.Application",
                    // .NET Framework 版は "v1"、Core 版は "v2"
                    "v2");

                // 認証クッキーから暗号化された認証チケットを復号
                TicketDataFormat format = new TicketDataFormat(protector);

                // 下の「注 3, 4」参照
                AuthenticationTicket authTicket = format.Unprotect(cookie);

                // ユーザー名を取得
                ClaimsPrincipal principal = authTicket.Principal;
                IIdentity identity = principal.Identity;
                string userName = identity.Name!;

                // 認証チケットの有効期限の日時を取得
                AuthenticationProperties property = authTicket.Properties;
                DateTimeOffset? expiersUtc = property.ExpiresUtc;

                // expiresUtc と現在の時刻を比較して期限切れか否かを判定
                if (expiersUtc.Value < DateTimeOffset.UtcNow)

                {
                    ViewData["AuthTicket"] = $"{userName} さん、"+
                      $"認証チケットの有効期限 {expiersUtc.Value} が切れています。";
                }
            }
            // ・・・追加はここまで

            // ・・・中略・・・
        }

        public async Task<IActionResult> OnPostAsync(string returnUrl = null)
        {
            
            // ・・・中略・・・

        }
    }
}

Login.cshtml

認証ケットが期限切れの場合は cshtml から ViewData で情報をもらって期限切れのメッセージを表示するための一行を追加したのみです。

@page
@model LoginModel

@{
    ViewData["Title"] = "Log in";
}

<h1>@ViewData["Title"]</h1>

@* 2024/5/11 以下の一行を追加 *@
<p><font color=red>@ViewData["AuthTicket"]</font></p>

<div class="row">
    <div class="col-md-4">
        <section>

・・・中略・・・

</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

注 1 : デフォルトのログインページには[Remember me?]チェックボックスがあって、これにチェックを入れて認証を受けると Set-Cookie ヘッダに expires 属性が追加され認証クッキーが HDD / SSD に保存されます。expires 属性に指定される期限は認証チケットの有効期限と同じになります。期限を過ぎるとブラウザ側で認証クッキーを削除してしまいますので、条件 1 の「要求 HTTP ヘッダーに認証クッキーが含まれる」ことの判定はできなくなります。

注 2 : DI 機能により CookieAuthenticationOptions オブジェクトを取得します。それをベースに復号に必要な TicketDataFormat オブジェクトを取得し、認証クッキーから認証チケットを復号しています。

Visual Studio で「個別のユーザーアカウント」を認証に選んで作成したプロジェクトまたはスキャフォールディング機能を使って ASP.NET Core Identity を追加したプロジェクトでは以下のコードが Program.cs 含まれるはずです。その場合、コンストラクタの引数に IOptions<CookieAuthenticationOptions> options を含めるだけで DI 機能が働きます。

builder.Services.AddDefaultIdentity<ApplicationUser>(options => 
    options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>()

注 3 : IDataProtectionProvider.CreateProtector メソッドの引数ですが、これには暗号化するときに使用した値 (暗号化を特定の目的にロックするためのものらしい)と同じものを設定する必要があります。それが CookieAuthenticationMiddleware の名前、認証クッキー名、ASP.NET Identity のバージョンになるようです。

暗号化するときに使用した値と、上の CreateProtector メソッドの引数に指定したものが違うと復号に失敗し、上のコードで authTicket が null になり、次の行の authTicket.Principal で NullReferenceException がスローされます。

注 4 : 別の記事「ASP.NET Core でのデータ保護キーの管理」で書きましたがデータ保護キーがリサイクルで失われることがあります。その場合は復号に失敗し、上のコードで authTicket が null になり、次の行の authTicket.Principal で NullReferenceException がスローされます。

リサイクルで失われるようなことは無くても、データ保護キーの有効期限はデフォルトで 90 日だそうですので、有効期限を過ぎると復号に失敗すると思われます (未検証・未確認です)。

Tags: , , , ,

Authentication

ASP.NET Core Identity をスキャフォールディングで実装

by WebSurfer 14. February 2024 14:00

Visual Studio Community 2022 のテンプレート「ASP.NET Core Web アプリ (Model-View-Controller)」を使って認証なしで作成した .NET 8.0 の ASP.NET Core MVC プロジェクトに、ASP.NET Core Identity を実装する方法を書きます。

認証なしで ASP.NET Core MVC プロジェクト作成

上の画像で[認証の種類(A)]を「個別のアカウント」に設定してプロジェクトを作成するとユーザー認証に必要な ASP.NET Core Identity が実装されますが、Razor クラス ライブラリ (RCL) として実装されるためソースコードはプロジェクトに含まれません。なので、例えばログイン・登録ページを書き換えたい場合は、スキャフォールディング機能を利用してログイン・登録ページのソースコードを生成し、それに手を加えるということになります。

書き換えるまでは必要なくても単純に日本語化したいページはログイン・登録ページ以外にも多々あるので、この際[認証の種類(A)]を「なし」に設定してプロジェクトを作成し、それにスキャフォールディング機能を利用して ASP.NET Core Identity 関係のソースコードを一式すべて実装する方が良さそうです。

基本的なことは Microsoft のドキュメント「ASP.NET Core プロジェクトでの Identity のスキャフォールディング」の「MVC プロジェクトに既存の認可なしで Identity をスキャフォールディングする」のセクションに書いてありますが、それだけでは情報不足だと思いますので、以下に画像を加えて詳しく書いておきます。

(1) Microsoft.VisualStudio.Web.CodeGeneration.Design

NuGet から Microsoft.VisualStudio.Web.CodeGeneration.Design をインストールします。 この操作をスキップしても、この後のステップで自動的に追加されますが、 自分でバージョンを確認してからインストールするのが良さそうです。

(2) ASP.NET Core Identity のソースコード実装

スキャフォールディング機能を利用して ASP.NET Core Identity のソースコードを実装します。 具体的な方法は以下の通りです。

  1. ソリューションエクスプローラーでプロジェクトノードを右クリックすると表示されるメニューで[追加(D)]⇒[新規スキャフォールディングアイテム(F)...]を選択。 「新規スキャフォールディングアイテムの追加」ダイアログが表示されるので ID を選択して[追加]ボタンをクリック。

    新規スキャフォールディングアイテムの追加
  2. 「ID の追加」ダイアログが表示されるので[すべてのファイルをオーバーライド]にチェックを入れます。

    ID の追加
  3. DbContext クラス名は、上の画像の + のアイコンをクリックして出てくる「データコンテキストの追加」ダイアログでは、 例えばプロジェクト名が MvcNet8App2 の場合、下の画像のように MvcNet8App2.Data.MvcNet8App2Context というプロジェクト名が反映された名前になります。 この MvcNet8App2Context を ApplicationDbContext に変更します。

    データコンテキストの追加

    スキャフォールディング機能で自動生成される ASP.NET Core Identity 関係のコードで ApplicationDbContext という名前が使われていて、ApplicationDbContext でないとエラーになるというケースが過去にありました。 改善されているかもしれませんが(未確認です)、変更した方が無難だと思います。名前の変更は必ず「データコンテキストの追加」ダイアログ上で行う必要がありますので注意してください。
  4. 上のステップで名前を変更後[追加]ボタンをクリックすると結果が以下のように反映されます。 この時点でデーターベースプロバイダーには SQL Server が選択され、この後の操作で Program.cs に SQL Server を使う設定がなされます。

    DbContext クラス
  5. ユーザークラス名は、上の画像の + のアイコンをクリックして出てくる「ユーザークラスの追加」ダイアログでは MvcNet8App2User というプロジェクト名が反映された名前になります。 これを ApplicationUser に変更し、さらに、DbContext クラスと同じフォルダ Data にユーザークラスのファイルが作られるよう、同じ名前空間 MvcNet8App2.Data を付与します。

    ユーザークラスの追加
  6. 上のステップで名前を変更後[追加]ボタンをクリックすると結果が以下のように反映されます。確認して[追加]ボタンをクリックします。

    ユーザークラス
  7. 成功すると、プロジェクトルート直下に Areas フォルダが生成され、その中に ASP.NET Core Identity 関係のファイルが一式生成されます。 加えて、appsettings.json ファイルに接続文字列が追加され、Views/Shared フォルダに _LoginPartial.cshtml が追加され、 Program.cs に ApplicationDbContext と ApplicationUser のインスタンスを DI によって取得できるようにするためのコードが追加されます。

(3) ソリューションのリビルド

ソリューションをリビルドし、エラー無く完了することを確認してください。

ちなみに、前のバージョン .NET 7.0 の時は Pages\Account\Logout.cshtml と Pages\Account\Manage\_Layout.cshtml に NULL 許容参照型に対応していないコードが含まれていて警告が出ましたが、.NET 8.0 では警告が出ないように改善されていました。バージョンアップで進歩しているようです。

(4) レイアウトページの修正

スキャフォールディングで自動生成された Areas/Identity/Page/Account/Manage/ フォルダの _Layout.cshtml のコードで Layout = "/Areas/Identity/Pages/_Layout.cshtml"; を Layout = "/Views/Shared/_Layout.cshtml"; に変更します。

(5) LoginPartial を追加

Views/Shared/_Layout.cshtml に <partial name="_LoginPartial" /> を追加します。

LoginPartial を追加

これによりページの右上に下の画像の赤枠部分のように登録・ログイン・ログアウト操作のためにリンクが表示されるようになります。

LoginPartial の表示

(6) Program.cs の修正

Program.cs に builder.Services.AddRazorPages(); と app.MapRazorPages(); を追加します。 これがないとメニューバーの上のステップ (5) 画像の Register, Login をクリックしても Razor Page で作られた Register, Login に遷移しません。

(7) Add-Migration の実行

パッケージマネージャーコンソールから Add-Migration CreateIdentitySchema を実行します。CreateIdentitySchema という名前は任意に付けられます。

成功すると Migrations フォルダが生成され、その中に 20240115003149_CreateIdentitySchema.cs と ApplicationDbContextModelSnapshot.cs が生成されます。20240115003149 は Add-Migration を実行した日時、CreateIdentitySchema は上のコマンドで指定した名前です。

(8) Update-Database の実行

次に Update-Database を実行し、Entity Framework Code First の機能を利用してデータベース / テーブルを生成します。成功すると、appsettings.json に指定した接続文字列の Database に設定された名前のデータベースと必要なテーブル一式が生成されます。

データベース / テーブル

(9) ユーザー登録

以上で完了です。アプリを起動し Register ページでユーザー登録すれば上のステップ (8) で作成したデータベース / テーブルに反映されます。

Email Confirmation がデフォルトで有効になっており、手順に表示される Register confirmation ページのリンク "Click here to confirm your account" をクリックしないとアカウントが有効にならないので注意してください。

Register confirmation ページ

Tags: , , ,

Authentication

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。ブログ2はそれ以外の日々の出来事などのトピックスになっています。

Calendar

<<  January 2025  >>
MoTuWeThFrSaSu
303112345
6789101112
13141516171819
20212223242526
272829303112
3456789

View posts in large calendar