このページでは、C#とASP.NET Coreを使用してPush! Passkeyを実装します。最初は自社DBや既存のログイン処理へ接続せず、APIキーと固定のテストユーザーだけで登録・認証を確認します。
動作確認後に、ログイン中のユーザー取得、登録完了処理、認証成功後のログイン処理を自社サイトに合わせて変更します。最初から全処理を変更せず、「API接続の確認」と「自社システムへの組み込み」を分けることで、問題の発生箇所を判断しやすくします。
C#のWeb開発ではASP.NET Coreが標準的な選択肢です。同じページにMVC、Razor Pages、Minimal APIの完成コードを混在させるとコピー対象が分かりにくくなるため、本ページはASP.NET Core MVCのController方式に絞ります。
既存サイトがRazor PagesやMinimal APIで構築されている場合も、Push! Passkeyへ送信する項目と処理順序は同じです。ルーティング、セッション、JSON応答、CSRF対策を利用中の構成へ読み替えてください。
このサンプルは.NET 8以降、ASP.NET Core MVCを想定しています。Push! Passkey APIへの通信には、ASP.NET Core標準のIHttpClientFactoryとSystem.Text.Jsonを使用します。
dotnet --version dotnet --info
新しいテスト用プロジェクトを作成する場合は、次のコマンドを実行します。
dotnet new mvc -n PushPasskeySample cd PushPasskeySample
Push! Passkey管理サイトで、テストに使用するWebサイトのドメインを登録し、APIキーを発行しておいてください。登録ドメインとブラウザで開くドメインは一致させます。
PushPasskeySample/ ├── Controllers/ │ └── PushPasskeyController.cs ├── Views/ │ └── Home/ │ └── PushPasskeyTest.cshtml ├── Program.cs ├── appsettings.json └── PushPasskeySample.csproj
接続試験では処理の流れを追いやすくするため、API通信と4つのactionの処理を1つのControllerへまとめます。本番導入時は、自社プロジェクトの規約に合わせてController、Service、DTO、Optionsへ分割できます。
{
"PushPasskey": {
"ApiKey": "",
"ApiBaseUrl": "https://auth.jintec.com",
"TestMode": true,
"TestUserKey": "test-user-001",
"RequestTimeoutSeconds": 15
},
"AllowedHosts": "*"
}# Windows PowerShell $env:PushPasskey__ApiKey="発行されたAPIキー" # Linux / macOS export PushPasskey__ApiKey="発行されたAPIキー"
ASP.NET Coreの階層設定を環境変数で指定する場合、区切りには二重アンダースコアを使用します。APIキーをソースコードやGit管理対象のappsettings.jsonへ直接記載しないでください。ローカル開発ではUser Secretsも使用できます。
dotnet user-secrets init dotnet user-secrets set "PushPasskey:ApiKey" "発行されたAPIキー"
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(options =>
{
options.Cookie.Name = "pushpasskey_sample_session";
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.Lax;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.IdleTimeout = TimeSpan.FromMinutes(30);
});
builder.Services.AddHttpClient("PushPasskey", client =>
{
var baseUrl = builder.Configuration["PushPasskey:ApiBaseUrl"]
?? "https://auth.jintec.com";
var timeout = builder.Configuration
.GetValue("PushPasskey:RequestTimeoutSeconds", 15);
client.BaseAddress = new Uri(baseUrl);
client.Timeout = TimeSpan.FromSeconds(timeout);
client.DefaultRequestHeaders.Accept.ParseAdd("application/json");
});
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseSession();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();ローカルHTTPだけで試験する場合は、CookieのSecurePolicyを開発時だけSameAsRequestへ変更できます。本番では必ずHTTPSとAlwaysを使用してください。
最初の接続試験では、名前空間以外を変更せずに使用します。regist_startでは固定のテストユーザーを使用し、auth_startでは現在の仕様に従いuser_keyを送信しません。
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
namespace PushPasskeySample.Controllers;
[ApiController]
[Route("pushpasskey")]
public sealed class PushPasskeyController : ControllerBase
{
private static readonly HashSet<string> AllowedActions =
new(StringComparer.Ordinal)
{
"regist_start",
"auth_start",
"regist_result",
"auth_result"
};
private readonly IConfiguration configuration;
private readonly IHttpClientFactory httpClientFactory;
private readonly ILogger<PushPasskeyController> logger;
public PushPasskeyController(
IConfiguration configuration,
IHttpClientFactory httpClientFactory,
ILogger<PushPasskeyController> logger)
{
this.configuration = configuration;
this.httpClientFactory = httpClientFactory;
this.logger = logger;
}
[HttpPost("endpoint")]
[Consumes("application/json")]
[Produces("application/json")]
public async Task<IActionResult> EndpointAsync(
[FromBody] Dictionary<string, JsonElement> input,
CancellationToken cancellationToken)
{
try
{
VerifySameOrigin();
string action = GetString(input, "action");
if (!AllowedActions.Contains(action))
{
return JsonError(400, "invalid_action",
"処理の指定が正しくありません。");
}
string apiKey =
configuration["PushPasskey:ApiKey"]?.Trim() ?? "";
if (apiKey == "")
{
throw new InvalidOperationException(
"PushPasskey:ApiKey is not configured.");
}
var apiBody = new Dictionary<string, object?>
{
["api_key"] = apiKey
};
string apiPath;
if (action == "regist_start")
{
string userKey = GetRegistrationUserKey();
string returnPageUrl =
GetString(input, "return_page_url");
apiPath = "/regist_url";
apiBody["user_key"] = userKey;
if (returnPageUrl != "")
{
apiBody["return_page_url"] = returnPageUrl;
}
}
else if (action == "auth_start")
{
string returnPageUrl =
GetString(input, "return_page_url");
apiPath = "/auth_url";
// 認証開始時はuser_keyを送信しない。
if (returnPageUrl != "")
{
apiBody["return_page_url"] = returnPageUrl;
}
}
else
{
string requestId = GetString(input, "request_id");
if (requestId == "")
{
return JsonError(400, "request_id_required",
"request_idがありません。");
}
apiPath = action == "regist_result"
? "/regist_result"
: "/auth_result";
apiBody["request_id"] = requestId;
}
using HttpClient client =
httpClientFactory.CreateClient("PushPasskey");
using HttpResponseMessage apiResponse =
await client.PostAsJsonAsync(
apiPath, apiBody, cancellationToken);
string raw = await apiResponse.Content
.ReadAsStringAsync(cancellationToken);
JsonDocument json;
try
{
json = JsonDocument.Parse(raw);
}
catch (JsonException ex)
{
logger.LogError(ex,
"Push! Passkey API returned non-JSON. HTTP {Status}",
(int)apiResponse.StatusCode);
return JsonError(502, "api_invalid_json",
"Push! Passkey APIの応答がJSONではありません。");
}
using (json)
{
var result = JsonSerializer.Deserialize<Dictionary<string, object?>>(
json.RootElement.GetRawText()) ?? new();
if (!apiResponse.IsSuccessStatusCode)
{
return StatusCode((int)apiResponse.StatusCode, result);
}
if (action == "regist_result")
{
AfterRegistrationSuccessIfNeeded(json.RootElement);
}
else if (action == "auth_result")
{
await AfterAuthenticationSuccessIfNeededAsync(
json.RootElement, cancellationToken);
}
return Ok(result);
}
}
catch (OperationCanceledException)
{
return JsonError(504, "api_timeout",
"APIとの通信がタイムアウトしました。");
}
catch (Exception ex)
{
logger.LogError(ex, "Push! Passkey endpoint failed.");
return JsonError(500, "internal_error",
"サーバー処理中にエラーが発生しました。");
}
}
private string GetRegistrationUserKey()
{
bool testMode =
configuration.GetValue("PushPasskey:TestMode", true);
if (testMode)
{
return configuration["PushPasskey:TestUserKey"]?.Trim()
?? "test-user-001";
}
// 本番では、既存ログインユーザーの外部連携用IDを返す。
// 数値IDであっても必ず文字列へ変換する。
int? userId = HttpContext.Session.GetInt32("user_id");
if (userId is null)
{
throw new UnauthorizedAccessException(
"ログイン後に登録してください。");
}
return userId.Value.ToString(
System.Globalization.CultureInfo.InvariantCulture);
}
private void AfterRegistrationSuccessIfNeeded(JsonElement result)
{
if (!IsSuccess(result))
{
return;
}
// 必要に応じて、自社DBへ登録完了日時などを保存する。
}
private async Task AfterAuthenticationSuccessIfNeededAsync(
JsonElement result,
CancellationToken cancellationToken)
{
if (!IsSuccess(result))
{
return;
}
string userKey = GetJsonString(result, "user_key");
if (userKey == "")
{
throw new InvalidOperationException(
"Authentication result does not contain user_key.");
}
// 本番ではuser_keyで自社ユーザーを検索し、
// 無効状態・権限などを確認する。
// var user = await userRepository.FindByExternalKeyAsync(
// userKey, cancellationToken);
HttpContext.Session.Clear();
// 接続試験では、認証済みuser_keyをセッションへ保存する。
// 本番では既存サイトのログイン確立処理へ置き換える。
HttpContext.Session.SetString(
"authenticated_user_key", userKey);
}
private void VerifySameOrigin()
{
string origin = Request.Headers.Origin.ToString();
if (origin == "")
{
return;
}
string expected =
$"{Request.Scheme}://{Request.Host}";
if (!string.Equals(
origin, expected, StringComparison.OrdinalIgnoreCase))
{
throw new UnauthorizedAccessException(
"Origin is not allowed.");
}
}
private static bool IsSuccess(JsonElement result) =>
GetJsonString(result, "status") == "success";
private static string GetString(
Dictionary<string, JsonElement> input,
string name) =>
input.TryGetValue(name, out JsonElement value)
? value.ValueKind == JsonValueKind.String
? value.GetString()?.Trim() ?? ""
: value.ToString().Trim()
: "";
private static string GetJsonString(
JsonElement json, string name) =>
json.TryGetProperty(name, out JsonElement value)
? value.ValueKind == JsonValueKind.String
? value.GetString()?.Trim() ?? ""
: value.ToString().Trim()
: "";
private ObjectResult JsonError(
int status, string errorCode, string message) =>
StatusCode(status, new
{
ok = false,
error_code = errorCode,
message
});
}user_keyはPush! Passkeyとの連携用識別子です。自社DBのIDが整数型でも、APIへ送信するときと認証結果を扱うときは必ず文字列として処理してください。
@{
ViewData["Title"] = "Push! Passkey 接続テスト";
}
<h1>Push! Passkey 接続テスト</h1>
<p id="pushpasskey_result" aria-live="polite"></p>
<button
type="button"
data-pushpasskey="regist"
data-endpoint="/pushpasskey/endpoint"
data-success-url="/Home/PushPasskeyTest">
パスキー登録
</button>
<button
type="button"
data-pushpasskey="auth"
data-endpoint="/pushpasskey/endpoint"
data-success-url="/">
パスキー認証
</button>
<script src="https://auth.jintec.com/js/passkey.js"></script>既存のHomeControllerへ表示用actionを追加します。
public IActionResult PushPasskeyTest()
{
return View();
}
認証成功後のdata-success-urlは、ログイン後に表示するページを指定します。成功後の移動先で追加の結果照会を行わない場合、そのページにpasskey.jsを読み込む必要はありません。
dotnet runでアプリを起動するauthenticated_user_keyが保存されたことを確認するこの段階では固定ユーザーを使用しています。登録と認証の両方が成功するまでは、自社DBや既存ログイン処理へ接続しないでください。
GetRegistrationUserKey()を、自社サイトのログインセッションとDB構成に合わせて変更します。返り値は必ずstringとします。
// DB上のIDがintの場合
int userId = 123;
string userKey = userId.ToString(
System.Globalization.CultureInfo.InvariantCulture);
// Guidの場合
string userKey = user.ExternalId.ToString("D");
user_keyは数字だけに見える場合でも数値ではありません。JSONへ数値として送信すると、先頭ゼロの消失や型不一致の原因になります。
AfterRegistrationSuccessIfNeeded()へ、登録済みフラグや登録日時の更新、監査ログ保存などを追加します。Push! Passkeyの成功応答を受け取っただけで、自社ユーザーの状態や権限を変更しないでください。
認証開始時にはuser_keyを送信しません。認証に成功したCredential IDからPush! Passkey側がユーザーを特定し、結果照会のuser_keyとして返します。
AfterAuthenticationSuccessIfNeededAsync()で、そのuser_keyを使って自社DBのユーザーを検索し、存在、利用状態、権限を確認してから既存サイトのログインセッションを発行します。
string userKey = GetJsonString(result, "user_key");
var user = await userRepository.FindByExternalKeyAsync(
userKey, cancellationToken);
if (user is null || !user.IsActive)
{
throw new UnauthorizedAccessException(
"認証ユーザーを利用できません。");
}
HttpContext.Session.Clear();
HttpContext.Session.SetInt32("user_id", user.Id);
HttpContext.Session.SetString("login_state", "authenticated");
ASP.NET CoreのCookie認証を使用している場合は、セッションへの代入ではなくHttpContext.SignInAsync()で認証Cookieを発行します。既存サイトのログイン方式へ統一してください。
パスキー認証が成功し、自社サイトのログインセッションを発行した後も、必要に応じて登録済みパスキーの件数、端末名、ブラウザ、OS、登録日時、最終利用日時を表示できます。この機能を使用しない場合、認証完了後にPush! Passkey APIを呼び出す必要はありません。
登録情報を表示したいログイン後のページへ、次のタグとpasskey.jsを設置します。
<div class="my-passkey-list"
data-pushpasskey-credentials
data-endpoint="/pushpasskey/endpoint">
</div>
<script src="https://auth.jintec.com/js/passkey.js"></script>
表示タグを検出すると、passkey.jsはエンドポイントへcredential_statusアクションを送信します。ブラウザからuser_keyやAPIキーを送信する必要はありません。
エンドポイントは、ブラウザからuser_keyを受け取らず、既存のログインセッションから対象ユーザーを特定します。そのuser_keyとサーバー側のAPIキーをPush! Passkeyの/credential_statusへ送信します。
// AllowedActionsへ追加します。
"credential_status"
// EndpointAsync()で、request_idを要求する分岐より前に追加します。
if (action == "credential_status")
{
string userKey = GetRegistrationUserKey();
apiPath = "/credential_status";
apiBody["user_key"] = userKey;
}
else if (action == "regist_start")
{
// 既存の登録開始処理
}
上記は既存サンプルへ追加する部分を示しています。導入先のセッション構成に合わせてGetRegistrationUserKey()の取得処理を変更してください。user_keyはstring型の一意な値として扱います。
登録済みのパスキーがある場合、表示タグの内側には概ね次のHTMLが生成されます。表示内容や件数はAPIの応答によって変わります。
<p class="pushpasskey-credentials-summary">登録済みパスキー:1件</p>
<ul class="pushpasskey-credentials-list">
<li class="pushpasskey-credential-item">
<strong class="pushpasskey-credential-device">端末名</strong>
<span class="pushpasskey-credential-environment">Chrome / Windows</span>
<span class="pushpasskey-credential-created">登録:2026/08/20 10:00</span>
<span class="pushpasskey-credential-last-used">最終利用:2026/08/20 11:00</span>
</li>
</ul>
passkey.jsが生成するclassへCSSを指定できます。導入先の他画面へ影響させないため、表示タグへ独自classを追加し、その内側だけへスタイルを適用する方法を推奨します。
.my-passkey-list .pushpasskey-credentials-summary {
font-weight: 700;
margin-bottom: 12px;
}
.my-passkey-list .pushpasskey-credentials-list {
list-style: none;
padding: 0;
}
.my-passkey-list .pushpasskey-credential-item {
margin-bottom: 12px;
}
.my-passkey-list .pushpasskey-credential-item span {
display: block;
}
user_keyを信用しないpasskey.jsは、エンドポイントから返された応答をJSONとして解析します。JSONとして解析できない場合は、単に「JSONではありません」と表示するのではなく、HTTPステータス、Content-Type、原因の判定候補、サーバーから実際に返された内容を画面へ表示します。
この表示により、C# / ASP.NET Core側の例外や警告、Webサーバーのエラーページ、ログイン画面へのリダイレクト、デバッグ出力などを確認しやすくなります。
エンドポイントの応答をJSONとして解析できませんでした。 【HTTPステータス】 500 Internal Server Error 【Content-Type】 text/html; charset=UTF-8 【判定】 エンドポイントまたはWebサーバー側でエラーが発生した可能性があります。 【サーバーから返された内容】 実際にブラウザへ返されたエラー内容
| 表示内容 | 主な原因 | 確認する場所 |
|---|---|---|
| HTTP 404 | エンドポイントURLや公開パスが正しくありません。 | data-endpoint、ルーティング、プロキシ設定 |
| HTTP 403 | Origin、CSRF、認可、WAFなどで拒否されています。 | セキュリティ設定、リクエストヘッダー |
| HTTP 405 | POSTルートへ到達していません。 | ルーティング、Webサーバー・プロキシ設定 |
| HTTP 500以上 | アプリケーションまたはWebサーバー側で例外が発生した可能性があります。 | 画面へ表示された応答内容、ASP.NET Coreまたはリバースプロキシのログ |
| ログイン画面やHTML | 共通のログイン処理や例外ハンドラーがHTMLを返しています。 | 認証ミドルウェア、例外処理、リダイレクト設定 |
| デバッグ文字列とJSONが一緒に表示される | デバッグ用出力がJSONの前後へ混入しています。 | エンドポイントと共通処理の出力 |
| 応答本文が空 | アプリ停止、強制終了、タイムアウト、エラー非表示などが考えられます。 | ASP.NET Coreまたはリバースプロキシのログ |
passkey.jsが表示できるのは、エンドポイントからブラウザへ実際に返された内容です。アプリケーションやWebサーバーが詳細エラーをレスポンスへ出力する設定であれば、その内容も表示されます。詳細エラーをレスポンスへ出力しない設定では、HTTP 500や空の応答だけが表示される場合があります。
エラー表示・ログ記録の設定は、導入先の開発・本番環境の運用方針に合わせて設定してください。
/pushpasskey/endpointを選択する問い合わせ時は、発生時刻、登録・認証・登録情報表示のどこで発生したか、HTTPステータス、画面へ表示された応答内容、必要に応じてDevToolsのResponseとログの該当部分をお知らせください。APIキー、Cookie、セッションID、個人情報は削除してから共有してください。
user_keyをログインセッションから取得しているuser_keyを常に文字列として処理しているuser_keyを送信していないuser_keyで自社ユーザーを検索しているここまで確認できれば、接続試験用コードから本番環境への移行は完了です。詳細なAPI項目と応答仕様は「APIリファレンス」、結果状態の扱いは「結果処理」を参照してください。