ASP.NET Core Authentication Endpoints: Why I Built AuthEndpoints
---
title: ASP.NET Core Authentication Endpoints: Why I Built AuthEndpoints
published: true
description: ASP.NET Core wires up auth middleware but no login, register, refresh or 2FA endpoints. AuthEndpoints 4 adds them with cookies, JWT and passkeys.
canonical_url: madeyoga.harten.id
tags: dotnet, aspnetcore, authentication, csharp
cover_image: dev-to-uploads.s3.us-east-2.amazonaws.com
Use a ratio of 100:42 for best results.
published_at: 2026-10-21 16:04 +0000
---
AddAuthentication. AddAuthorization. AddCookie. AddJwtBearer. UseAuthentication. UseAuthorization.
In 2022 I started using ASP.NET Core for new projects. I came from Django, where you create a project and auth is already there: a user model, login, logout, password reset, and an admin. However, in aspnetcore I wrote those six lines, ran the app, and had nowhere to log in. There were no aspnetcore authentication endpoints, only the plumbing for them.
The problem: no endpoints
It took me a while to understand why. I read the aspnetcore source and a lot of blog samples. The Add* and Use* calls register the auth services and middleware. They don't give you a single endpoint. Register, login, refresh, 2FA and password reset are all yours to write. To write them, you first have to learn UserManager, SignInManager, token providers and authentication schemes. That's a lot for a beginner. ASP.NET Core is unopinionated on purpose, and auth is where you feel it most.
So I wrote the endpoints once and turned them into a library. I started AuthEndpoints in April 2022. I've used it ever since in every client project, including POS systems, CRMs and internal apps.
!David Fowler forked AuthEndpoints
That July the repo got a fork from David Fowler, one of the engineers behind aspnetcore. Stars started coming in. When I looked into why, I found out it wasn't just me. A month earlier someone had opened dotnet/aspnetcore#42158, asking for simple auth options in the SPA templates. The thread grew into a long back-and-forth discussions.
!ASP.NET Core auth discussions
The opening post quoted developers saying "I'm learning authentication by trial-and-error" and called it frightening. A consultant wrote that a team new to .NET couldn't get auth working for a month and gave up for a while. Others pointed to Laravel, where this part just works. David Fowler was replying in that thread too. At the time, aspnetcore had no built-in auth endpoints at all. MapIdentityApi didn't come until .NET 8, in November 2023.
The more I learned about auth (while building client projects), the more I saw how opinionated it is. Cookies or tokens. Passwords, email codes, passkeys, social logins, oauth, oidc, step ups, two factor authentication. Every app picks a different mix depending on their needs. I also think maybe that's why aspnetcore left the endpoints out. A framework that stays unopinionated can't pick one for you. Even with MapIdentityApi, it is, in my opinion an opinionated solution that can't fit on every apps out there.
That gave me the idea for AuthEndpoints: a set of minimal API endpoints where you pick the ones you need. However, the execution was really bad. I didn't have the experience yet, so the early versions gave you no real choice. You got one opinionated way of doing auth, password auth, and that was it.
Later, AI tools helped me reshape the idea and the code, so each piece stands on its own. You pick your sign-in method, your session type and the extras you want, and map only those. The idea became composable authentication.
AuthEndpoints 4 is the result. The version I want other developers to try. It is free and MIT licensed. The main goal is to simplify aspnetcore authentication. Whether you're new to aspnetcore or have used it for years, please give it a try!
AuthEndpoints 4: an ASP.NET Core auth library on top of Identity
!AuthEndpoints
AuthEndpoints is an aspnetcore auth library. It doesn't replace aspnetcore Identity. Identity still owns your users, password hashing and lockout. AuthEndpoints maps the HTTP endpoints on top of it. Opinionated recipes ready for different auth types, while remain flexible, allowing users to compose their own auth stack.
Version 4.0.0 shipped on 15 October 2026. Two patch releases followed, and 4.0.2 is the current one on NuGet. It targets .NET 10 and uses EF Core for the user store.
The big change in 4 is a new entry point for new projects, AddPasswordAuth and MapPasswordAuth. It is a Password auth recipe (or I call it facade). One pair of calls gives you password sign-up, sign-in, recovery, email confirmation, account management and one session, all under /auth. Cookie is the default session. Jwt is the other one.
Quick start
This comes from the quick start in the docs. You need the .NET 10 SDK.
dotnet new web -n AuthDemo
cd AuthDemo
dotnet add package AuthEndpoints
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
Then Program.cs:
using AuthEndpoints;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(o => o.UseSqlite("Data Source=app.db"));
builder.Services.AddPasswordAuth<AppUser, AppDbContext>();
builder.Services.AddTransient<IEmailSender<AppUser>, ConsoleEmailSender>();
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
scope.ServiceProvider.GetRequiredService<AppDbContext>().Database.EnsureCreated();
}
app.UseAuthEndpoints();
app.MapPasswordAuth<AppUser>();
app.MapGet("/me", (System.Security.Claims.ClaimsPrincipal user) => new { email = user.Identity!.Name })
.RequireAuthorization();
app.Run();
public class AppUser : IdentityUser { }
public class AppDbContext(DbContextOptions<AppDbContext> options) : IdentityDbContext<AppUser>(options) { }
public class ConsoleEmailSender(ILogger<ConsoleEmailSender> logger) : IEmailSender<AppUser>
{
public Task SendConfirmationLinkAsync(AppUser user, string email, string confirmationLink)
{
logger.LogInformation("Confirm {Email}: {Link}", email, confirmationLink);
return Task.CompletedTask;
}
public Task SendPasswordResetLinkAsync(AppUser user, string email, string resetLink)
{
logger.LogInformation("Reset link for {Email}: {Link}", email, resetLink);
return Task.CompletedTask;
}
public Task SendPasswordResetCodeAsync(AppUser user, string email, string resetCode)
{
logger.LogInformation("Reset code for {Email}: {Code}", email, resetCode);
return Task.CompletedTask;
}
}
The quick start also has a ConsoleEmailSender that writes confirmation and reset links to the log. I left it out here to keep the sample short. You need some IEmailSender<AppUser> for the app to send those links.
Three calls do the work. AddPasswordAuth registers Identity, the password endpoints, account management and the cookie session. UseAuthEndpoints adds authentication, authorization, rate limiting and antiforgery middleware. MapPasswordAuth maps the routes under /auth.
Run it, and you have POST /auth/register, POST /auth/login, POST /auth/logout, GET /auth/confirmEmail, POST /auth/forgotPassword, POST /auth/resetPassword, /auth/manage/2fa and more. Accounts need a confirmed email before they can log in. That's the default.
What you get
Sessions. Cookie sessions for browser apps. JWT with a rotating refresh token for clients that need a bearer token. Identity bearer tokens for mobile apps, on the 3.x facade. In 4, the JWT refresh token can also come back in the JSON body for native apps and CLIs that have no cookie. Cookie transport stays the default.
2FA. Authenticator codes and recovery codes through /auth/manage/2fa.
Passkeys. Set Passkeys.ServerDomain and the password facade maps passkey sign-in and passkey management under /auth/passkeys. Users can add, rename and remove them.
Step-up (ReAuth). Before a sensitive change, the user proves who they are again with a password, an authenticator code, a recovery code, a passkey, or an email code. You can require it on your own routes too.
Other sign-in options. Emailed one-time codes, plus GitHub and Google sign-in (currently plain oauth flow, not oidc).
Protection. Rate limits, CSRF checks on cookie routes that change state, lockout, and checks at startup. Every Map* call throws at startup when its Add* call is missing, so a broken setup fails before the first request.
Modules. If the facade maps more than you want, you can compose smaller modules and map only the routes you need on your own prefixes.
If you already use the 3.x AddAuthEndpoints and MapAuthEndpoints, they still work in 4. Some setups need a database migration, and a few smaller APIs changed. Read the upgrade guide before you bump the version.
AuthEndpoints vs MapIdentityApi
If you're looking for a MapIdentityApi alternative, this is the honest comparison in dotnet10.
MapIdentityApi maps ten routes: register, login, refresh, confirm email, resend confirmation, forgot password, reset password, 2FA, and get or update account info. Login returns Identity bearer tokens by default, or a cookie if you pass useCookies. Microsoft's docs say those tokens are not standard JWTs. It's a good start, and both libraries use the same Identity user store.
Here is what it doesn't do:
- It maps no logout route. The Microsoft docs tell you to write your own.
- It has no passkey endpoints.
- It maps no CSRF token route for cookie clients.
- It has no step-up check for host endpoints.
- You can't leave out routes like /register. It maps all ten.
- Rate limiting, antiforgery and CORS are yours to wire up.
The docs have a full comparison with the routes side by side.
Try it
I built AuthEndpoints to simplify authentication in ASP.NET Core web apis. Four years later it runs in every project I ship.
- Docs: authendpoints.harten.id
- Quick start: authendpoints.harten.id
- NuGet: nuget.org
- GitHub: madeyoga/AuthEndpoints
If something breaks or a doc is unclear, please open an issue and let me know!
2 reactions · 2 comments
Mentions
Ran an experiment that ran for a day that resulted in ~90 PRs to improve the performance of asp.net Core: aspnetcore#70219 Fun facts: - These were using Github's new cloud agents support that shipped this week at GHU. - Opus 5.5 is very good - ~$230 in tokens. #dotnet #aspnetcore
CIをローカルPCで再現実行したらPCが悲鳴を上げたので、原因を全部調べた話 個人開発で、動画サイト(ASP.NET Core + React + PostgreSQL)を作っています。品質は「本番並み」を目標にしていて1、合流の前にはビルド・lint・型・全テスト・e2e・秘密情報の走査をすべて流し、全部通ったものだけをマージする決まりにしています。 ところが、別の検証で GitHub Actions の無料枠を使い切ってしまいました。そこで CI と同じ手順を手元で 1 回で流すスクリプト(以下、ローカル CI)を作り、合流の前に必ず流すことにしました。この記事は、そのローカル CI を自宅の PC で回し続けたら PC が重くなり、原因を測って直していった記録です。 ! 開発の多くは Claude Code に任せていて、モデルごとに役割を分けています。 Claude Opus 5.5: 全体の取りまとめとレビュー Claude Sonnet 5.5: 機能ごとの実装(何本か並行で動かす) Claude Fable 5.1: 原因の見立ての検証 この記事の調査も Opus と Sonnet が行い、出てきた原因の見立てを Fable に確かめさせています。 環境 自宅の PC OS Windows(Docker Desktop・WSL2) CPU Core i7-9700(8 コア 8 スレッド) メモリ 16GB(Docker Desktop に回せるのは 7.7GB) ディスク SATA の SSD 512GB テストの多くは、本物の PostgreSQL・S3 互換のストレージ(Garage)・ffmpeg を Docker で動かして確かめます。結合テストは Testcontainers、e2e は Playwright です。ローカル CI を 1 回流すと、PC の上で多くのコンテナと、数百のアプリのホストが動きます。 何が起きたか 仕事では速い Mac を使っていて、そこで Claude Code に任せているのと同じくらいの量の作業を、自宅の PC にも並べていました。開発の作業を 3 本並べ、ローカル CI が 2 本重なって流れることもありました。自宅の PC でどのくらいの負荷まで耐えられるかを測る、という意味もありました。 結果、動いている間は PC がすごく重くなり、ほかの作業ができなくなりました。性能の差の分はしょうがない。ただ、それとは別に無駄が無いかを調べることにしました。 調べると、起きていたのは次の 2 つでした。 メモリが足りない。 会社の PC より性能の低い PC に、同じくらいの量の作業を並べたのが原因でした。ただ、テストのプロセスが捨てたはずのメモリを抱えたままになる無駄も重なっていました(後の「3. テストのホストが解放されない」)。 空きメモリが数十 MB まで落ちた(普段のローカル CI の回で最小 68MB。バックエンドのテストだけを計測の道具を付けて流した調査の回では 34MB) バックエンドのテストのプロセスが 11〜16GB を握っていた ローカル CI は 1 回 1 時間くらいかかっていた覚えがある(時間の記録は残っていない。記録の残っている 10/9 の 6 回は、1 回 20〜30 分ほどで、6 回ともどこかのテストが落ちて流し直していた)。毎回違うテストが時間切れで落ちた ローカル CI が 2 本重なった回は、どちらの負荷で落ちたのか分けられず、その結果は当てにならなかった ディスクの空きが無くなる。 ローカル CI の最中に C ドライブの空きが 0 バイトになり、Docker も WSL も応答しなくなりました。こちらは、もともと空きが 30GB 足らずだった C ドライブに、テストのたびに作る DB を消さずに、1 回のローカル CI で約 8GB ずつ書いていたのが原因でした(後の「2. ローカル CI を重ねない・テストの DB を消す」)。 どこに時間がかかっているか いちばん重かった頃の、手順ごとの時間の記録は残っていません。残っているのは、10/9 のバックエンドのテストが 10 分 52 秒〜18 分 47 秒かかっていた、という記録です。そこでこの節では、改善の途中で取ったバックエンドのテストの内訳と、直した後の今のローカル CI(約 19 分)の手順ごとの時間を使って、何が重いのかを見ていきます。 結論から書くと、次の 3 つが分かりました。 今のローカル CI でも、時間の約半分はバックエンドのテストが占めている そのバックエンドのテストは、8 コアのうち半分も使えていなかった。並列で流せる部分は、いちばん長いテストのまとまり 1 つで決まり、ffmpeg を使うテストは単独で 3 分以上かかっていた テスト 1 件ごとの準備では、パスワードのハッシュの計算が重く、しかも 1 件の中で何度も繰り返されていた もう 1 つ、時間の表には出てこない問題として、メモリの使い方がありました(後の「3. テストのホストが解放されない」)。 それぞれ、測った値で見ていきます。まず、直した後の今のローカル CI の、手順ごとの時間です。 手順 時間 バックエンドのテスト(xUnit。1,752 件。Docker の PostgreSQL・ffmpeg を使う) 9 分 28 秒 e2e(Playwright。173 件。テストの実行だけ。DB などの起動は除く) 約 3 分 24 秒 フロントエンドのテスト(Vitest。1,822 件) 2 分 48 秒 そのほか(ビルド・型・lint・脆弱な依存・秘密の走査、e2e の環境の起動など) 約 3 分 合計 約 19 分 19 分のうち約 9 分半がバックエンドのテストです。速くするなら、まずここを見ることになります。 そのバックエンドのテストを、さらに割りました(改善の途中の回。1,339 件・8 分 9 秒)。 段階 時間 見えたこと 並列の部分 276 秒 8 コアのうち平均 3.8 しか使えていない。最も長い 1 つのテストのまとまり(xUnit の collection。まとまりの中のテストは順番に動く)の時間で決まる ffmpeg を Docker で動かすテスト(単独) 196 秒 ほかと重ねずに流している(理由は後の「2.」) そのほか(単独) 6 秒 並列の部分は、いちばん長いまとまりが終わるのを待つ形なので、ほかのまとまりを速くしても縮みません。縮めるには、テスト 1 件ごとの重さを減らす必要があります。 そこで、テスト 1 件ごとの準備の時間を、ほかの負荷が無い状態で 30 回ずつ測りました(中央値)。 処理 時間 テスト専用の DB をひな形からコピーする 25 ms アプリのホストを立ち上げる(DB の作成を含む) 129 ms パスワードのハッシュを 1 回計算する(PBKDF2・10 万回) 54 ms ffmpeg の docker run を 1 回起動する 約 330 ms パスワードのハッシュ 1 回(54 ms)は、DB のコピー(25 ms)の倍かかります。テストの準備では 1 件ごとに会員を 3〜6 人作るので、この計算が 1 件の中で 3〜6 回繰り返されていました。 直していった順番 1. 並行の作業を減らす 作業ごとにテスト用のコンテナと画面のビルドが動くので、8 コア・16GB で 3 本は多すぎました。同時に動かすのを 2 本にしました。 2. ローカル CI を重ねない・テストの DB を消す ローカル CI が 2 本同時に流れていたので、ローカル CI のスクリプト自身が最初に鍵(フォルダ)を取り、一度に 1 本しか流れないようにしました。2 本目は 1 本目が終わるまで待つことになりますが、負荷の山は 1 本分になり、取り合いで落ちて流し直すことも無くなります。 git worktree で作業ツリーが分かれていても同じ鍵を見るよう、置き場所はリポジトリの本体の側にする GATE_LOCK="$(cd "$(git rev-parse --git-common-dir)/.." && pwd)/.claude/gate.lock" mkdir -p "$(dirname "$GATE_LOCK")" mkdir は「無ければ作る」を 1 回の操作で行うので、2 本が同時に来ても片方だけが取れる if ! mkdir "$GATE_LOCK" 2>/dev/null; then echo "別のローカル CI が動いています" >&2 cat "$GATE_LOCK/owner" >&2 # 持ち主(PID・開始時刻・作業ツリー)を表示する exit 1 fi (持ち主を $GATE_LOCK/owner に書き、失敗・中断でも EXIT の trap で owner と鍵のフォルダを消す) テストは 1 件ごとに専用の DB を作りますが、消していませんでした。1 回のローカル CI で約 8GB を書き込み、C ドライブを使い切っていたので、終わったら消すようにしました。 // 作る: マイグレーション済みの「ひな形」からコピーする(1 個 25ms) command.CommandText = $"CREATE DATABASE \"{name}\" TEMPLATE \"{TemplateName}\""; // 消す: 接続が残っていても強制的に切る。後片付けなので、失敗してもテストは落とさず警告にする command.CommandText = $"DROP DATABASE IF EXISTS \"{name}\" WITH (FORCE)"; ffmpeg を Docker で動かすテストは、ほかと重なると起動が遅れて落ちていたので、ほかと重ねずに流すようにしました。これでその揺れは止まりましたが、テストの時間は約 7 分延びました。 ! テストが毎回違う所で落ちる原因について、メイン(Opus)が最初に立てた見立ては「DB を消していないので、ディスクの I/O が詰まってテストが遅れている」でした。Fable に検証させると、DB の作成は 1 回のローカル CI で合計約 40 秒・平均約 18MB/s の書き込みで、SSD を詰まらせる量ではありませんでした。DB を消す直しが主に効くのは容量で、テストが遅れる主な原因ではありませんでした。落ちていたテストの多くは、テスト自身の同期の競合でした。 3. テストのホストが解放されない(ASP.NET Core の既知の不具合) テストは 1 件ごとにアプリのホスト(WebApplicationFactory)を立ち上げて捨てます。計測の道具を足して調べると、869 個のホストが、捨てた後も 1 つ残らずメモリに残り、合計 16GB に達していました。 原因は ASP.NET Core の既知の不具合でした。回数制限のミドルウェア(RateLimitingMiddleware)が、ポリシーの有無に関係なく内部に限定器(レート制限器。PartitionedRateLimiter。100ms のタイマー付き)を作り、それを破棄しません。タイマーが static のキューから生き続け、ホスト全体を参照し続けます。 dotnet/aspnetcore #66434 RateLimitingMiddleware never disposes PartitionedRateLimiter causing memory leak(2026-10-26 時点で未修正。修正の PR #66435 も未マージ) 本番は 1 プロセスに 1 ホストなので影響しません。テストのように何百もホストを作ると溜まります。 テストの側で、起動時に組み上がったパイプラインを受け取り、自分のホストの限定器を捕まえて、DI コンテナの破棄と一緒に止めるようにしました。 // 起動時に、パイプラインの先頭に「後ろ全部」を受け取る印を置く。リクエストごとの処理は増えない internal sealed class RateLimiterCaptureFilter(RateLimiterReleaser releaser) : IStartupFilter { public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next) { return app => { app.Use(rest => { releaser.Capture(rest); // パイプラインをたどって RateLimitingMiddleware の _endpointLimiter を捕まえる return rest; }); next(app); }; } } RateLimiterReleaser は DI のシングルトンで、捕まえた限定器をコンテナの破棄のときに Dispose します。中身はリフレクションで private のフィールドをたどるので、上流が直ったら外す前提です。「この仕組みを入れないとホストが残る」ことを確かめるテストも付けておき、上流が直ってそのテストが落ちたら外す合図にしています。 直したことで、捨てたホストが残らなくなりました。下の表は、原因を調べるために、計測の道具を付けてバックエンドのテストだけを流した回(調査の回)の値です。 項目 直す前の調査の回 直した後の調査の回 捨てた後も残るホスト 869 2 残るメモリ 16.06GB 247MB 空きメモリの最小 34MB 2,854MB バックエンドのテスト 17 分 22 秒 7 分 39 秒 時間の効果は、調査の回とは別に、普段のローカル CI(全部の手順を流す)の回で比べました。どちらの列も、その 1 回のローカル CI の中で測った値です。 項目 直す前のローカル CI の回 直した後のローカル CI の回 ローカル CI の全体 約 20 分 約 16 分 バックエンドのテスト 10 分 33 秒 7 分 29 秒 空きメモリの最小 68MB 3,282MB 直す前は、メモリが尽きて、Windows がメモリの中身をディスクに退避させながら動いていた(ページング)分、遅くなっていました。どれだけ遅くなるかは回ごとにぶれます(バックエンドのテストは、普段のローカル CI の回で 10 分 33 秒〜18 分 47 秒、計測の道具を付けた調査の回で 17 分 22 秒)。調査の回は道具を付けている分も重く、差の理由までは切り分けていません。 ! このメモリの滞留も、原因を突き止めるまでに、見立てを 3 回外しています(同時に生きているホストが多すぎる・GC が回収を後回しにしている・ワーキングセットの急落をゴミの回収と読んだ)。最後は、GC のヒープ・Private Bytes・生きているホストの数を測って、参照の経路をたどって決めました。 4. テストの準備で、同じ重い計算を毎回していた テストの準備では、1 件ごとに同じパスワードの会員を 3〜6 人作り、そのたびにパスワードのハッシュ(既定の PasswordHasher。PBKDF2・10 万回。1 回 54ms)を計算し直していました。テスト 1 件あたり 0.16〜0.32 秒が、毎回同じ結果を作るためだけに使われていました。 同じパスワードのハッシュは、テストのプロセスの中で 1 回だけ作って再利用するようにしました。 // キーはパスワードだけ(PasswordHasher<TUser> は user を使わない) private static readonly ConcurrentDictionary<string, string> HashCache = new(StringComparer.Ordinal); user.PasswordHash = HashCache.GetOrAdd(password, static (p, u) => Hasher.HashPassword(u, p), user); ログインの照合は本物の計算のままなので、確かめている中身は変わりません。同じパスワードの会員どうしでハッシュ(ソルト)が同じになりますが、これはテストの種データだけの話です。再利用しても会員ごとに自分のパスワードでしかログインできないことは、テストで確かめました。 壁時計は 446 秒 → 391 秒、テストのプロセスの CPU 時間は 637 秒 → 465 秒になりました。ただ、前後は 1 回ずつしか測っていません。壁時計の差(54 秒)は、同じコードを 2 回測ったときの差(43 秒)と同じくらいしかなく、CPU 時間は前を 1 回しか測っていないのでぶれが分かりません。効果の大きさは確かめきれていません。効果の根拠は仕組み(54ms の計算が 2 人目以降で無くなる)です。 5. e2e がたまに時間切れで落ちる(localhost の落とし穴) 最近、e2e が毎回違う所で時間切れになりました。調べると、e2e のアプリが DB に localhost でつないでいたのが原因でした。 Docker の公開は 127.0.0.1:5432(IPv4)だけ。IPv6 の ::1:5432 には誰も待ち受けていない この PC では、localhost を引くと ::1 が先に返る この PC では、待ち受けの無いポートへの接続が拒否されるまで約 2 秒かかる(.NET で測って 2,031〜2,133ms。127.0.0.1 の空いているポートでも同じだったので、::1 に限らず、この環境の性質。原因は確かめていない) Npgsql は接続の時間切れを、接続先の数で等分して順に試す。時間切れを 2 秒にしていたので、新しい接続のたびに約 1 秒を ::1 で失い、残りで間に合わないと失敗する const postgres = { // compose は postgres を 127.0.0.1 にだけ公開する。localhost だと ::1 を先に試して、拒否を待つ間に時間を失う host: '127.0.0.1', // ... }; 接続先を 127.0.0.1 にすると、比べた e2e の 1 回で、DB の接続の失敗は 0 件になり、ログインの遅い側(95 パーセンタイル)は 1,087ms → 165ms になりました。 結果 いちばん上の行(記憶)を除き、どれも普段のローカル CI(全部の手順を流す)の回の記録の値です。バックエンドのテストは、そのローカル CI の中の 1 手順の時間です。 時点 ローカル CI の全体 バックエンドのテスト いちばん重かった頃(作業を 3 本並べていた頃) 1 時間くらい(記憶。記録なし) 記録なし 改善の前(10/9) 20〜30 分ほど(6 回で 2 時間 10 分。6 回とも失敗) 10 分 52 秒〜18 分 47 秒 ホストの解放の前(10/10) 約 20 分 10 分 33 秒 ホストの解放の後(10/10) 約 16 分 7 分 29 秒 ハッシュの再利用の後(10/12) 約 15 分 6 分 34 秒 今(10/23) 約 19 分 9 分 28 秒 今は、テストの数がバックエンド 1,340 → 1,752 件、フロントエンド 1,627 → 1,822 件に増えた中で、約 19 分で全部通っています。 分かったこと 重さの原因は、性能の低い PC に作業を並べすぎたことと、捨てたはずのものが残り続けること(ASP.NET Core の既知の不具合で、テストが捨てたアプリのホストがメモリに残る・テストの DB を消し忘れて残る)の重なりでした。作業を減らし、ローカル CI を重ねないようにし、残るものを片付けると、ローカル CI は 1 時間くらいかかっていた覚えのある頃から、15〜20 分で安定して通るようになりました 毎回同じ結果を作り直していないかも見るべき所でした。テストの準備で、同じパスワードのハッシュを何度も計算していました。結果が同じになる重い処理は、作り直さずに 1 回だけ作って再利用することが大切です それでも残る差は、PC の性能の差です。8 スレッド・16GB では、Docker とテストとブラウザが取り合い、時間を測るテストがたまに揺れます 原因の見立ては外れる。テストが落ちる原因の最初の推測は、別のモデルに検証させたら大部分が誤りでした。推測のまま直していたら、効かない直しに時間を使うところでした。直す前に測り、見立てを確かめるのが近道です おわりに 自宅の PC で、測っては直してを繰り返した記録です。手元で CI を回している方や、同じように PC が重くなって困っている方、どなたかの参考になれば嬉しいです。 ご一読ありがとうございました。 おまけ: Windows + Docker Desktop で決めていること イメージのビルドの中で dotnet restore や pnpm install をしない。 セキュリティソフトが HTTPS を検査して独自の証明書を差し込むので、コンテナの中から取りに行くと証明書の誤りになります。検証を切る代わりに、ホストでビルドした成果物を COPY で入れています(例外は ffmpeg の apt だけ。apt は HTTP でも、パッケージの署名を確かめてから入れます) AI のエージェントから Docker Desktop を起動しない。 Claude のデスクトップアプリ(MSIX パッケージ)から起動したプロセスは AppData の下が仮想化され、Docker が自分のソケットを片付けられず、次の起動で落ちました。Docker は自分でスタートメニューから起動します 固まったら、残ったプロセスを止めて WSL を終わらせる。 画面は「Engine running」なのに命令が返らない、Quit しても中のプロセスが残って起動し直せない、ということがあります。残ったプロセスを止め、wsl --terminate docker-desktop を流してから起動し直します。スリープの後は、docker info がすぐ返るかを確かめてから使います ローカル CI を流す前に、ディスクの空きとテストのコンテナの残骸を見る。 WSL2 の仮想ディスクは自動では縮まず、空きが尽きると Docker も WSL も応答しなくなります 公開は 127.0.0.1 の必要なポートだけ。 アプリのイメージは、基底のイメージをダイジェストで固定し、root でない利用者で動かす 脚注 自分の好きな SNS を自分で作り、本番の運用に耐えられる品質まで仕上げる試みです。作りながらたまった知識はノウハウとして残し、学習にもしています。 ↩︎
@tunguz Because of the recent remote code execution exploit? github.com/dotnet/aspnetcore/security/advisories/…
@garamlover あまり騒がれてなさそうですが、ASP .NET Coreも割と致命的な脆弱性出ているような… github.com/dotnet/aspnetcore/security/advisories
ASP.NET Core Authentication Endpoints: Why I Built AuthEndpoints --- title: ASP.NET Core Authentication Endpoints: Why I Built AuthEndpoints published: true description: ASP.NET Core wires up auth middleware but no login, register, refresh or 2FA endpoints. AuthEndpoints 4 adds them with cookies, JWT and passkeys. canonical_url: madeyoga.harten.id tags: dotnet, aspnetcore, authentication, csharp cover_image: dev-to-uploads.s3.us-east-2.amazonaws.com Use a ratio of 100:42 for best results. published_at: 2026-10-21 16:04 +0000 --- AddAuthentication. AddAuthorization. AddCookie. AddJwtBearer. UseAuthentication. UseAuthorization. In 2022 I started using ASP.NET Core for new projects. I came from Django, where you create a project and auth is already there: a user model, login, logout, password reset, and an admin. However, in aspnetcore I wrote those six lines, ran the app, and had nowhere to log in. There were no aspnetcore authentication endpoints, only the plumbing for them. The problem: no endpoints It took me a while to understand why. I read the aspnetcore source and a lot of blog samples. The Add* and Use* calls register the auth services and middleware. They don't give you a single endpoint. Register, login, refresh, 2FA and password reset are all yours to write. To write them, you first have to learn UserManager, SignInManager, token providers and authentication schemes. That's a lot for a beginner. ASP.NET Core is unopinionated on purpose, and auth is where you feel it most. So I wrote the endpoints once and turned them into a library. I started AuthEndpoints in April 2022. I've used it ever since in every client project, including POS systems, CRMs and internal apps. !David Fowler forked AuthEndpoints That July the repo got a fork from David Fowler, one of the engineers behind aspnetcore. Stars started coming in. When I looked into why, I found out it wasn't just me. A month earlier someone had opened dotnet/aspnetcore#42158, asking for simple auth options in the SPA templates. The thread grew into a long back-and-forth discussions. !ASP.NET Core auth discussions The opening post quoted developers saying "I'm learning authentication by trial-and-error" and called it frightening. A consultant wrote that a team new to .NET couldn't get auth working for a month and gave up for a while. Others pointed to Laravel, where this part just works. David Fowler was replying in that thread too. At the time, aspnetcore had no built-in auth endpoints at all. MapIdentityApi didn't come until .NET 8, in November 2023. The more I learned about auth (while building client projects), the more I saw how opinionated it is. Cookies or tokens. Passwords, email codes, passkeys, social logins, oauth, oidc, step ups, two factor authentication. Every app picks a different mix depending on their needs. I also think maybe that's why aspnetcore left the endpoints out. A framework that stays unopinionated can't pick one for you. Even with MapIdentityApi, it is, in my opinion an opinionated solution that can't fit on every apps out there. That gave me the idea for AuthEndpoints: a set of minimal API endpoints where you pick the ones you need. However, the execution was really bad. I didn't have the experience yet, so the early versions gave you no real choice. You got one opinionated way of doing auth, password auth, and that was it. Later, AI tools helped me reshape the idea and the code, so each piece stands on its own. You pick your sign-in method, your session type and the extras you want, and map only those. The idea became composable authentication. AuthEndpoints 4 is the result. The version I want other developers to try. It is free and MIT licensed. The main goal is to simplify aspnetcore authentication. Whether you're new to aspnetcore or have used it for years, please give it a try! AuthEndpoints 4: an ASP.NET Core auth library on top of Identity !AuthEndpoints AuthEndpoints is an aspnetcore auth library. It doesn't replace aspnetcore Identity. Identity still owns your users, password hashing and lockout. AuthEndpoints maps the HTTP endpoints on top of it. Opinionated recipes ready for different auth types, while remain flexible, allowing users to compose their own auth stack. Version 4.0.0 shipped on 15 October 2026. Two patch releases followed, and 4.0.2 is the current one on NuGet. It targets .NET 10 and uses EF Core for the user store. The big change in 4 is a new entry point for new projects, AddPasswordAuth and MapPasswordAuth. It is a Password auth recipe (or I call it facade). One pair of calls gives you password sign-up, sign-in, recovery, email confirmation, account management and one session, all under /auth. Cookie is the default session. Jwt is the other one. Quick start This comes from the quick start in the docs. You need the .NET 10 SDK. dotnet new web -n AuthDemo cd AuthDemo dotnet add package AuthEndpoints dotnet add package Microsoft.EntityFrameworkCore.Sqlite Then Program.cs: using AuthEndpoints; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext<AppDbContext>(o => o.UseSqlite("Data Source=app.db")); builder.Services.AddPasswordAuth<AppUser, AppDbContext>(); builder.Services.AddTransient<IEmailSender<AppUser>, ConsoleEmailSender>(); var app = builder.Build(); using (var scope = app.Services.CreateScope()) { scope.ServiceProvider.GetRequiredService<AppDbContext>().Database.EnsureCreated(); } app.UseAuthEndpoints(); app.MapPasswordAuth<AppUser>(); app.MapGet("/me", (System.Security.Claims.ClaimsPrincipal user) => new { email = user.Identity!.Name }) .RequireAuthorization(); app.Run(); public class AppUser : IdentityUser { } public class AppDbContext(DbContextOptions<AppDbContext> options) : IdentityDbContext<AppUser>(options) { } public class ConsoleEmailSender(ILogger<ConsoleEmailSender> logger) : IEmailSender<AppUser> { public Task SendConfirmationLinkAsync(AppUser user, string email, string confirmationLink) { logger.LogInformation("Confirm {Email}: {Link}", email, confirmationLink); return Task.CompletedTask; } public Task SendPasswordResetLinkAsync(AppUser user, string email, string resetLink) { logger.LogInformation("Reset link for {Email}: {Link}", email, resetLink); return Task.CompletedTask; } public Task SendPasswordResetCodeAsync(AppUser user, string email, string resetCode) { logger.LogInformation("Reset code for {Email}: {Code}", email, resetCode); return Task.CompletedTask; } } The quick start also has a ConsoleEmailSender that writes confirmation and reset links to the log. I left it out here to keep the sample short. You need some IEmailSender<AppUser> for the app to send those links. Three calls do the work. AddPasswordAuth registers Identity, the password endpoints, account management and the cookie session. UseAuthEndpoints adds authentication, authorization, rate limiting and antiforgery middleware. MapPasswordAuth maps the routes under /auth. Run it, and you have POST /auth/register, POST /auth/login, POST /auth/logout, GET /auth/confirmEmail, POST /auth/forgotPassword, POST /auth/resetPassword, /auth/manage/2fa and more. Accounts need a confirmed email before they can log in. That's the default. What you get Sessions. Cookie sessions for browser apps. JWT with a rotating refresh token for clients that need a bearer token. Identity bearer tokens for mobile apps, on the 3.x facade. In 4, the JWT refresh token can also come back in the JSON body for native apps and CLIs that have no cookie. Cookie transport stays the default. 2FA. Authenticator codes and recovery codes through /auth/manage/2fa. Passkeys. Set Passkeys.ServerDomain and the password facade maps passkey sign-in and passkey management under /auth/passkeys. Users can add, rename and remove them. Step-up (ReAuth). Before a sensitive change, the user proves who they are again with a password, an authenticator code, a recovery code, a passkey, or an email code. You can require it on your own routes too. Other sign-in options. Emailed one-time codes, plus GitHub and Google sign-in (currently plain oauth flow, not oidc). Protection. Rate limits, CSRF checks on cookie routes that change state, lockout, and checks at startup. Every Map* call throws at startup when its Add* call is missing, so a broken setup fails before the first request. Modules. If the facade maps more than you want, you can compose smaller modules and map only the routes you need on your own prefixes. If you already use the 3.x AddAuthEndpoints and MapAuthEndpoints, they still work in 4. Some setups need a database migration, and a few smaller APIs changed. Read the upgrade guide before you bump the version. AuthEndpoints vs MapIdentityApi If you're looking for a MapIdentityApi alternative, this is the honest comparison in dotnet10. MapIdentityApi maps ten routes: register, login, refresh, confirm email, resend confirmation, forgot password, reset password, 2FA, and get or update account info. Login returns Identity bearer tokens by default, or a cookie if you pass useCookies. Microsoft's docs say those tokens are not standard JWTs. It's a good start, and both libraries use the same Identity user store. Here is what it doesn't do: - It maps no logout route. The Microsoft docs tell you to write your own. - It has no passkey endpoints. - It maps no CSRF token route for cookie clients. - It has no step-up check for host endpoints. - You can't leave out routes like /register. It maps all ten. - Rate limiting, antiforgery and CORS are yours to wire up. The docs have a full comparison with the routes side by side. Try it I built AuthEndpoints to simplify authentication in ASP.NET Core web apis. Four years later it runs in every project I ship. - Docs: authendpoints.harten.id - Quick start: authendpoints.harten.id - NuGet: nuget.org - GitHub: madeyoga/AuthEndpoints If something breaks or a doc is unclear, please open an issue and let me know!
@cholf5 醒醒,快醒醒! 祖传的漏洞也不少: github.com/dotnet/aspnetcore/security/advisories
I felt it was time to make another contribution to the ASP.NET Core repository! 🚀 github.com/dotnet/aspnetcore/blob/2dd4d9d70ca0c59… #aspnetcore
I felt it was time to make another contribution to the asp.net Core repository! 🚀 github.com/dotnet/aspnetcore/blob/2dd4d9d70ca0c59… #aspnetcore t.co