パスキーを知る
パスキーの基礎
パスキーの登録方法
パスキーの削除方法
開発者向け解説
Push! Passkeyを知る
Push! Passkeyとは
システム構成・責任分界
利用を開始する
管理ツールの使い方
実装する
実装ガイド
フロントエンド実装
PHPでの実装
Rubyでの実装
Node.jsでの実装
Pythonでの実装
Javaでの実装
C#での実装
結果を連携する
認証結果の受け取り
調べる
APIリファレンス
エラーコード
対応環境
セキュリティ上の注意
トラブルシューティング
よくある質問

C#での実装

ASP.NET Coreを使用して登録・認証を動作確認する

このページでは、C#とASP.NET Coreを使用してPush! Passkeyを実装します。最初は自社DBや既存のログイン処理へ接続せず、APIキーと固定のテストユーザーだけで登録・認証を確認します。

動作確認後に、ログイン中のユーザー取得、登録完了処理、認証成功後のログイン処理を自社サイトに合わせて変更します。最初から全処理を変更せず、「API接続の確認」と「自社システムへの組み込み」を分けることで、問題の発生箇所を判断しやすくします。

ASP.NET Coreに絞る理由

C#のWeb開発ではASP.NET Coreが標準的な選択肢です。同じページにMVC、Razor Pages、Minimal APIの完成コードを混在させるとコピー対象が分かりにくくなるため、本ページはASP.NET Core MVCのController方式に絞ります。

既存サイトがRazor PagesやMinimal APIで構築されている場合も、Push! Passkeyへ送信する項目と処理順序は同じです。ルーティング、セッション、JSON応答、CSRF対策を利用中の構成へ読み替えてください。

1. 実行環境を確認する

このサンプルは.NET 8以降、ASP.NET Core MVCを想定しています。Push! Passkey APIへの通信には、ASP.NET Core標準のIHttpClientFactorySystem.Text.Jsonを使用します。

dotnet --version
dotnet --info

新しいテスト用プロジェクトを作成する場合は、次のコマンドを実行します。

dotnet new mvc -n PushPasskeySample
cd PushPasskeySample

Push! Passkey管理サイトで、テストに使用するWebサイトのドメインを登録し、APIキーを発行しておいてください。登録ドメインとブラウザで開くドメインは一致させます。

2. 基本ファイル構成

PushPasskeySample/
├── Controllers/
│   └── PushPasskeyController.cs
├── Views/
│   └── Home/
│       └── PushPasskeyTest.cshtml
├── Program.cs
├── appsettings.json
└── PushPasskeySample.csproj

接続試験では処理の流れを追いやすくするため、API通信と4つのactionの処理を1つのControllerへまとめます。本番導入時は、自社プロジェクトの規約に合わせてController、Service、DTO、Optionsへ分割できます。

3. 設定ファイルを作成する

appsettings.json

{
  "PushPasskey": {
    "ApiKey": "",
    "ApiBaseUrl": "https://auth.jintec.com",
    "TestMode": true,
    "TestUserKey": "test-user-001",
    "RequestTimeoutSeconds": 15
  },
  "AllowedHosts": "*"
}

APIキーは環境変数へ設定する

# 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キー"

4. ASP.NET Coreを設定する

Program.cs

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を使用してください。

5. endpointを作成する

Controllers/PushPasskeyController.cs

最初の接続試験では、名前空間以外を変更せずに使用します。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へ送信するときと認証結果を扱うときは必ず文字列として処理してください。

6. 動作確認用画面を作成する

Views/Home/PushPasskeyTest.cshtml

@{
    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を読み込む必要はありません。

7. 登録と認証を動作確認する

  1. APIキーと登録ドメインを確認する
  2. dotnet runでアプリを起動する
  3. HTTPSのテスト画面を開く
  4. 「パスキー登録」を押し、登録完了を確認する
  5. 「パスキー認証」を押し、成功後に指定ページへ移動することを確認する
  6. セッションにauthenticated_user_keyが保存されたことを確認する

この段階では固定ユーザーを使用しています。登録と認証の両方が成功するまでは、自社DBや既存ログイン処理へ接続しないでください。

8. 自社サイトに合わせて変更する

変更箇所1:ログイン中のユーザーを取得する

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へ数値として送信すると、先頭ゼロの消失や型不一致の原因になります。

変更箇所2:登録成功後の処理を追加する

AfterRegistrationSuccessIfNeeded()へ、登録済みフラグや登録日時の更新、監査ログ保存などを追加します。Push! Passkeyの成功応答を受け取っただけで、自社ユーザーの状態や権限を変更しないでください。

変更箇所3:認証成功後に自社ユーザーを特定する

認証開始時には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を発行します。既存サイトのログイン方式へ統一してください。

9. ログイン中ユーザーのパスキー登録情報を表示する

パスキー認証が成功し、自社サイトのログインセッションを発行した後も、必要に応じて登録済みパスキーの件数、端末名、ブラウザ、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キーを送信する必要はありません。

エンドポイントへcredential_status処理を追加する

エンドポイントは、ブラウザから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型の一意な値として扱います。

passkey.jsが生成するHTML

登録済みのパスキーがある場合、表示タグの内側には概ね次の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;
}

セキュリティ上の注意

  • APIキーをHTMLやJavaScriptへ埋め込まない
  • ブラウザから送信されたuser_keyを信用しない
  • ログインセッションから対象ユーザーを特定する
  • 公開鍵本体、Credential ID、内部管理用データをブラウザへ返さない

10. エンドポイントからJSON以外の応答が返された場合

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 403Origin、CSRF、認可、WAFなどで拒否されています。セキュリティ設定、リクエストヘッダー
HTTP 405POSTルートへ到達していません。ルーティング、Webサーバー・プロキシ設定
HTTP 500以上アプリケーションまたはWebサーバー側で例外が発生した可能性があります。画面へ表示された応答内容、ASP.NET Coreまたはリバースプロキシのログ
ログイン画面やHTML共通のログイン処理や例外ハンドラーがHTMLを返しています。認証ミドルウェア、例外処理、リダイレクト設定
デバッグ文字列とJSONが一緒に表示されるデバッグ用出力がJSONの前後へ混入しています。エンドポイントと共通処理の出力
応答本文が空アプリ停止、強制終了、タイムアウト、エラー非表示などが考えられます。ASP.NET Coreまたはリバースプロキシのログ

画面へ表示される内容について

passkey.jsが表示できるのは、エンドポイントからブラウザへ実際に返された内容です。アプリケーションやWebサーバーが詳細エラーをレスポンスへ出力する設定であれば、その内容も表示されます。詳細エラーをレスポンスへ出力しない設定では、HTTP 500や空の応答だけが表示される場合があります。

エラー表示・ログ記録の設定は、導入先の開発・本番環境の運用方針に合わせて設定してください。

さらに詳しく確認する場合

  1. ブラウザのDevToolsで「Network」を開く
  2. /pushpasskey/endpointを選択する
  3. HTTPステータス、Response、Content-Typeを確認する
  4. 同じ時刻のASP.NET Coreまたはリバースプロキシのログを確認する

問い合わせ時は、発生時刻、登録・認証・登録情報表示のどこで発生したか、HTTPステータス、画面へ表示された応答内容、必要に応じてDevToolsのResponseとログの該当部分をお知らせください。APIキー、Cookie、セッションID、個人情報は削除してから共有してください。

11. 本番公開前の確認

  • テストモードと固定ユーザーを無効にした
  • APIキーを環境変数、User Secrets、Key Vaultなどで管理した
  • 登録時のuser_keyをログインセッションから取得している
  • user_keyを常に文字列として処理している
  • 認証開始時にuser_keyを送信していない
  • 認証結果のuser_keyで自社ユーザーを検索している
  • ユーザーの有効状態と権限を確認してからログインを確立している
  • endpointが正常時・異常時ともJSONだけを返す
  • Originまたは自社標準のCSRF対策を有効にした
  • HTTPS、Secure、HttpOnly、SameSiteを確認した
  • 複数サーバー構成では分散セッションを使用している
  • APIキー、セッションID、個人情報をログへ出していない
  • タイムアウト、API障害、DB障害時の動作を確認した

ここまで確認できれば、接続試験用コードから本番環境への移行は完了です。詳細なAPI項目と応答仕様は「APIリファレンス」、結果状態の扱いは「結果処理」を参照してください。