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

Javaでの実装

Spring Bootを使用して登録・認証を動作確認する

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

動作確認後に、ログイン中のユーザー取得、登録完了処理、認証成功後のログイン処理の3か所を自社サイトに合わせて変更します。

Spring Bootに絞る理由

JavaのWeb開発にはJakarta EE、Quarkus、Micronautなどもありますが、一般的な業務WebアプリではSpring Bootが広く利用されています。同じページに複数フレームワークの完成コードを載せるとコピー対象が分かりにくくなるため、本ページはSpring MVC方式のSpring Bootに絞ります。

既存サイトが別のJavaフレームワークで構築されている場合、変更する必要はありません。APIへの送信項目と処理順序は共通です。ルーティング、セッション、JSON応答、CSRF対策を利用中のフレームワークへ置き換えてください。

1. 実行環境を確認する

このサンプルはJava 21、Spring Boot 3系、Mavenを想定しています。Spring MVCを使用し、Push! Passkey APIへの同期通信にはSpring FrameworkのRestClientを使用します。

java -version
mvn -version

Spring Initializrで次の依存関係を選択するか、既存プロジェクトのpom.xmlへ追加してください。

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

Push! Passkey管理サイトで、テストに使用するWebサイトのドメインを登録し、APIキーを発行しておいてください。

2. 基本ファイル構成

src/main/
├── java/com/example/pushpasskey/
│   ├── PushPasskeyApplication.java
│   ├── PushPasskeyController.java
│   └── SecurityConfig.java
└── resources/
    ├── static/
    │   └── pushpasskey-test.html
    └── application.yml

最初の接続試験では、処理の流れを追いやすくするため、Push! Passkeyとの通信処理を1つのControllerへまとめます。本番ではプロジェクトの規約に合わせてController、Service、DTO、設定クラスへ分割できます。

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

src/main/resources/application.yml

pushpasskey:
  api-key: ${PUSHPASSKEY_API_KEY}
  test-mode: ${PUSHPASSKEY_TEST_MODE:true}
  test-user-key: ${PUSHPASSKEY_TEST_USER_KEY:test-user-001}
  request-timeout-seconds: ${PUSHPASSKEY_REQUEST_TIMEOUT_SECONDS:15}

server:
  servlet:
    session:
      timeout: 30m
      cookie:
        http-only: true
        secure: true
        same-site: lax

環境変数

PUSHPASSKEY_API_KEY=発行されたAPIキー
PUSHPASSKEY_TEST_MODE=true
PUSHPASSKEY_TEST_USER_KEY=test-user-001

APIキーをapplication.ymlやJavaソースへ直接記載しないでください。IDEの実行設定、OSの環境変数、コンテナのSecretなどから渡します。

ローカルHTTP環境だけで試験する場合はCookieのsecureを一時的にfalseへ変更できます。本番は必ずHTTPSとtrueを使用してください。

4. エンドポイントを作成する

PushPasskeyController.java

パッケージ名は自社プロジェクトに合わせて変更してください。最初の接続試験では、それ以外の処理を書き換える必要はありません。

package com.example.pushpasskey;

import java.net.URI;
import java.time.Duration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClient;

@RestController
@RequestMapping("/pushpasskey")
public class PushPasskeyController {
    private static final Logger log = LoggerFactory.getLogger(
        PushPasskeyController.class
    );
    private static final String API_BASE = "https://auth.jintec.com";

    private final String apiKey;
    private final boolean testMode;
    private final String testUserKey;
    private final RestClient restClient;
    private final ObjectMapper objectMapper;

    public PushPasskeyController(
        @Value("${pushpasskey.api-key}") String apiKey,
        @Value("${pushpasskey.test-mode:true}") boolean testMode,
        @Value("${pushpasskey.test-user-key:test-user-001}") String testUserKey,
        @Value("${pushpasskey.request-timeout-seconds:15}") long timeoutSeconds,
        ObjectMapper objectMapper
    ) {
        this.apiKey = apiKey.trim();
        this.testMode = testMode;
        this.testUserKey = testUserKey.trim();
        this.objectMapper = objectMapper;

        JdkClientHttpRequestFactory factory = new JdkClientHttpRequestFactory();
        factory.setReadTimeout(Duration.ofSeconds(timeoutSeconds));
        this.restClient = RestClient.builder()
            .requestFactory(factory)
            .baseUrl(API_BASE)
            .build();
    }

    @GetMapping("/status")
    public Map<String, Object> status(HttpSession session) {
        return Map.of(
            "ok", true,
            "authenticated_user_key",
            stringValue(session.getAttribute("authenticated_user_key"))
        );
    }

    @PostMapping(
        value = "/endpoint",
        consumes = MediaType.APPLICATION_JSON_VALUE,
        produces = MediaType.APPLICATION_JSON_VALUE
    )
    public ResponseEntity<Map<String, Object>> endpoint(
        @RequestBody Map<String, Object> body,
        HttpServletRequest request,
        HttpSession session
    ) {
        try {
            verifySameOrigin(request);
            String action = stringValue(body.get("action"));

            return switch (action) {
                case "regist_start" -> registStart(body, request, session);
                case "auth_start" -> authStart(body, request, session);
                case "regist_result" -> registResult(body, session);
                case "auth_result" -> authResult(body, session);
                default -> jsonError(400, "処理の指定が正しくありません。");
            };
        } catch (IllegalArgumentException e) {
            return jsonError(400, e.getMessage());
        } catch (SecurityException e) {
            return jsonError(403, e.getMessage());
        } catch (Exception e) {
            log.error("Push! Passkey endpoint error", e);
            return jsonError(500, "Push! Passkeyの処理を完了できませんでした。");
        }
    }

    // 本番接続時に変更する箇所1
    private String getLoggedInUserKey(HttpSession session) {
        if (testMode) {
            return testUserKey;
        }
        Object userId = session.getAttribute("user_id");
        return userId == null ? "" : String.valueOf(userId).trim();
    }

    // 本番接続時に変更する箇所2
    private void afterRegistSuccess(
        Map<String, Object> result,
        HttpSession session
    ) {
        session.setAttribute("passkey_message", "パスキーの登録が完了しました。");
    }

    // 本番接続時に変更する箇所3
    private void afterAuthSuccess(
        Map<String, Object> result,
        HttpServletRequest request
    ) {
        String userKey = stringValue(result.get("user_key"));
        if (userKey.isBlank()) {
            throw new IllegalStateException("認証ユーザーを確認できません。");
        }

        HttpSession oldSession = request.getSession(false);
        if (oldSession != null) {
            oldSession.invalidate();
        }
        HttpSession newSession = request.getSession(true);
        newSession.setAttribute("authenticated_user_key", userKey);
    }

    private ResponseEntity<Map<String, Object>> registStart(
        Map<String, Object> body,
        HttpServletRequest request,
        HttpSession session
    ) {
        String userKey = getLoggedInUserKey(session);
        if (userKey.isBlank()) {
            return jsonError(401, "ログイン状態を確認できません。");
        }

        Map<String, Object> result = callApi("/regist_url", Map.of(
            "api_key", apiKey,
            "user_key", userKey,
            "return_page_url", returnPageUrl(body, request)
        ));
        savePending(result, "regist", userKey, session);
        return ResponseEntity.ok(result);
    }

    private ResponseEntity<Map<String, Object>> authStart(
        Map<String, Object> body,
        HttpServletRequest request,
        HttpSession session
    ) {
        Map<String, Object> result = callApi("/auth_url", Map.of(
            "api_key", apiKey,
            "return_page_url", returnPageUrl(body, request)
        ));
        savePending(result, "auth", "", session);
        return ResponseEntity.ok(result);
    }

    private ResponseEntity<Map<String, Object>> registResult(
        Map<String, Object> body,
        HttpSession session
    ) {
        String requestId = requiredRequestId(body);
        Map<String, String> pending = pending(requestId, "regist", session);
        Map<String, Object> result = callApi("/regist_result", Map.of(
            "api_key", apiKey,
            "request_id", requestId
        ));

        if (booleanValue(result.get("is_success"))) {
            if (!stringValue(result.get("user_key")).equals(pending.get("user_key"))) {
                throw new IllegalStateException("登録ユーザーが一致しません。");
            }
            afterRegistSuccess(result, session);
        }
        if (booleanValue(result.get("is_completed"))) {
            clearPending(requestId, session);
        }
        return ResponseEntity.ok(result);
    }

    private ResponseEntity<Map<String, Object>> authResult(
        Map<String, Object> body,
        HttpServletRequest request,
        HttpSession session
    ) {
        String requestId = requiredRequestId(body);
        pending(requestId, "auth", session);
        Map<String, Object> result = callApi("/auth_result", Map.of(
            "api_key", apiKey,
            "request_id", requestId
        ));

        String status = stringValue(result.get("status")).toLowerCase();
        boolean success = booleanValue(result.get("is_success"))
            || "success".equals(status);
        boolean completed = booleanValue(result.get("is_completed"))
            || java.util.Set.of("success", "failed", "expired", "cancelled")
                .contains(status);

        if (success) {
            clearPending(requestId, session);
            afterAuthSuccess(result, request);
        } else if (completed) {
            clearPending(requestId, session);
        }
        return ResponseEntity.ok(result);
    }

    private Map<String, Object> callApi(String path, Map<String, Object> payload) {
        if (apiKey.isBlank()) {
            throw new IllegalStateException("Push! Passkeyの設定を確認してください。");
        }

        ResponseEntity<String> response = restClient.post()
            .uri(path)
            .contentType(MediaType.APPLICATION_JSON)
            .accept(MediaType.APPLICATION_JSON)
            .body(payload)
            .retrieve()
            .onStatus(HttpStatusCode::isError, (req, res) -> { })
            .toEntity(String.class);

        try {
            Map<String, Object> result = objectMapper.readValue(
                response.getBody(), new TypeReference<>() { }
            );
            if (!response.getStatusCode().is2xxSuccessful()) {
                result.put("ok", false);
            }
            return result;
        } catch (Exception e) {
            throw new IllegalStateException(
                "Push! PasskeyからJSON以外の応答が返されました。", e
            );
        }
    }

    @SuppressWarnings("unchecked")
    private void savePending(
        Map<String, Object> result,
        String kind,
        String userKey,
        HttpSession session
    ) {
        String requestId = stringValue(result.get("request_id"));
        if (!booleanValue(result.get("ok")) || requestId.isBlank()) {
            return;
        }
        Map<String, Map<String, String>> requests = (Map<String, Map<String, String>>)
            session.getAttribute("pushpasskey_requests");
        requests = requests == null ? new HashMap<>() : new HashMap<>(requests);
        requests.put(requestId, Map.of("kind", kind, "user_key", userKey));
        session.setAttribute("pushpasskey_requests", requests);
    }

    @SuppressWarnings("unchecked")
    private Map<String, String> pending(
        String requestId,
        String kind,
        HttpSession session
    ) {
        Map<String, Map<String, String>> requests = (Map<String, Map<String, String>>)
            session.getAttribute("pushpasskey_requests");
        Map<String, String> value = requests == null ? null : requests.get(requestId);
        if (value == null || !kind.equals(value.get("kind"))) {
            throw new IllegalArgumentException(
                "処理情報を確認できません。最初からやり直してください。"
            );
        }
        return value;
    }

    @SuppressWarnings("unchecked")
    private void clearPending(String requestId, HttpSession session) {
        Map<String, Map<String, String>> requests = (Map<String, Map<String, String>>)
            session.getAttribute("pushpasskey_requests");
        if (requests != null) {
            Map<String, Map<String, String>> copy = new HashMap<>(requests);
            copy.remove(requestId);
            session.setAttribute("pushpasskey_requests", copy);
        }
    }

    private String requiredRequestId(Map<String, Object> body) {
        String value = stringValue(body.get("request_id"));
        if (value.isBlank()) {
            throw new IllegalArgumentException("処理情報を確認できません。");
        }
        return value;
    }

    private String returnPageUrl(
        Map<String, Object> body,
        HttpServletRequest request
    ) {
        String value = stringValue(body.get("return_page_url"));
        try {
            URI uri = URI.create(value);
            return request.getServerName().equalsIgnoreCase(uri.getHost())
                ? value : "";
        } catch (Exception e) {
            return "";
        }
    }

    private void verifySameOrigin(HttpServletRequest request) {
        String origin = request.getHeader("Origin");
        if (origin == null || origin.isBlank()) {
            return;
        }
        URI uri = URI.create(origin);
        if (!request.getServerName().equalsIgnoreCase(uri.getHost())) {
            throw new SecurityException("このページからは処理を開始できません。");
        }
    }

    private static String stringValue(Object value) {
        return value == null ? "" : String.valueOf(value).trim();
    }

    private static boolean booleanValue(Object value) {
        return Boolean.TRUE.equals(value) || "true".equalsIgnoreCase(stringValue(value));
    }

    private ResponseEntity<Map<String, Object>> jsonError(
        int status,
        String message
    ) {
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("ok", false);
        body.put("message", message);
        return ResponseEntity.status(status).body(body);
    }
}

認証開始時にはuser_keyを送信しません。Push! Passkeyは認証に成功したCredential IDから登録済みのuser_keyを特定し、認証結果として返します。

5. Spring Securityを設定する

SecurityConfig.java

package com.example.pushpasskey;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(
                    "/pushpasskey/endpoint",
                    "/pushpasskey/status",
                    "/pushpasskey-test.html"
                ).permitAll()
                .anyRequest().permitAll()
            )
            .csrf(csrf -> csrf
                .ignoringRequestMatchers("/pushpasskey/endpoint")
            );
        return http.build();
    }
}

Spring SecurityはPOSTに対するCSRF保護を標準で有効にします。サンプルでは外部配信のpasskey.jsからJSONを送るため、このエンドポイントだけ標準CSRF検証の対象外にし、ControllerでOriginを照合します。

既存サイトでCSRFトークンをpasskey.jsの送信へ組み込める場合は、標準CSRF保護を維持する方式が優先です。サイト全体のCSRF保護は無効にしないでください。

6. 動作確認用ファイルを作成する

src/main/resources/static/pushpasskey-test.html

<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>Push! Passkey Javaテスト</title>
</head>
<body>
  <h1>Push! Passkey Javaテスト</h1>
  <p id="pushpasskey-message"></p>

  <button type="button"
    data-pushpasskey="regist"
    data-endpoint="/pushpasskey/endpoint"
    data-success-url="/pushpasskey-test.html">
    パスキー登録
  </button>

  <button type="button"
    data-pushpasskey="auth"
    data-endpoint="/pushpasskey/endpoint"
    data-success-url="/pushpasskey-test.html">
    パスキー認証
  </button>

  <pre id="status"></pre>
  <script src="https://auth.jintec.com/js/passkey.js"></script>
  <script>
    fetch('/pushpasskey/status')
      .then(response => response.json())
      .then(data => {
        document.getElementById('status').textContent =
          data.authenticated_user_key
            ? '認証したuser_key:' + data.authenticated_user_key
            : '認証したuser_keyはまだありません。';
      });
  </script>
</body>
</html>

結果照会中も、data-pushpasskeydata-endpointdata-success-urlを持つボタンをDOMから削除しないでください。非表示にする場合はCSSまたはhidden属性を使用します。

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

mvn spring-boot:run

ブラウザでhttps://登録済みドメイン/pushpasskey-test.htmlを開きます。この段階で変更するのはAPIキーと、必要に応じてテスト用user_keyだけです。

  1. 「パスキー登録」を押し、端末へパスキーを保存する
  2. 登録完了メッセージが表示されることを確認する
  3. 「パスキー認証」を押し、保存したパスキーで認証する
  4. 画面にtest-user-001が表示されることを確認する

認証成功後の移動先には、通常passkey.jsは不要です。結果照会とログインセッションの発行は移動前に完了しています。テストページでは登録と認証を同じ画面で繰り返すため、スクリプトを配置しています。

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

最初にテストモードを終了する

PUSHPASSKEY_TEST_MODE=false

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

private String getLoggedInUserKey(HttpSession session) {
    Object userId = session.getAttribute("user_id");
    return userId == null ? "" : String.valueOf(userId).trim();
}

user_keyは、元のDB列が数値型でもPush! Passkeyとの送受信では必ず文字列として扱います。JavaのLongIntegerをそのままJSONへ渡さず、String.valueOf()で変換してください。DB検索時だけ自社DBの型へ戻します。

変更箇所2:登録成功後の処理

afterRegistSuccess()で登録済みフラグ、登録日時、監査ログなどを保存します。認証の正本となるCredential情報はPush! Passkey側で管理されるため、レスポンス全体を自社DBへ複製する必要はありません。

変更箇所3:認証成功後のログイン処理

private void afterAuthSuccess(
    Map<String, Object> result,
    HttpServletRequest request
) {
    String userKey = stringValue(result.get("user_key"));
    if (userKey.isBlank()) {
        throw new IllegalStateException("認証ユーザーを確認できません。");
    }

    // DB検索時だけ、自社DBの型へ変換します。
    long userId = Long.parseLong(userKey);
    User user = userService.findActiveUser(userId);
    if (user == null) {
        throw new IllegalStateException("有効なユーザーを確認できません。");
    }

    request.changeSessionId();
    request.getSession().setAttribute("user_id", user.getId());
}

認証結果のuser_keyで自社DBの有効なユーザーを取得し、停止状態、退会状態、利用権限も確認してからログインを確立します。Spring Securityを利用している場合は、独自セッション値だけでなくSecurityContextを正しく作成・保存してください。

重要事項:エンドポイントはJSONだけを返す

passkey.jsは応答全体をJSONとして読み取ります。ControllerからHTML、リダイレクト、デバッグ文字列が返ると、「エンドポイントの応答がJSONではありません。」と表示されます。

  • @RestControllerまたは@ResponseBodyを使用する
  • 正常時も異常時もJSONを返す
  • ログイン画面への302リダイレクトを返さない
  • 例外の詳細はSLF4Jへ記録し、レスポンス本文へ出力しない
  • リバースプロキシの404・502・503 HTMLも確認する

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へ送信します。

// endpoint() のswitchへ追加します。
case "credential_status" -> credentialStatus(session);

private ResponseEntity> credentialStatus(HttpSession session) {
    String userKey = loggedInUserKey(session);
    if (userKey.isBlank()) {
        return ResponseEntity.status(401).body(Map.of(
            "ok", false, "message", "ログイン状態を確認できません。"
        ));
    }
    return ResponseEntity.ok(callApi("/credential_status", Map.of(
        "api_key", apiKey,
        "user_key", userKey
    )));
}

上記は既存サンプルへ追加する部分を示しています。導入先のセッション構成に合わせてloggedInUserKey(...)の取得処理を変更してください。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、原因の判定候補、サーバーから実際に返された内容を画面へ表示します。

この表示により、Java / Spring Boot側の例外や警告、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サーバー側で例外が発生した可能性があります。画面へ表示された応答内容、Spring Bootまたはリバースプロキシのログ
ログイン画面やHTML共通のログイン処理や例外ハンドラーがHTMLを返しています。認証ミドルウェア、例外処理、リダイレクト設定
デバッグ文字列とJSONが一緒に表示されるデバッグ用出力がJSONの前後へ混入しています。エンドポイントと共通処理の出力
応答本文が空アプリ停止、強制終了、タイムアウト、エラー非表示などが考えられます。Spring Bootまたはリバースプロキシのログ

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

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

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

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

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

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

11. 本番公開前の確認

  • テストモードが無効になっている
  • APIキーが環境変数またはSecretで管理されている
  • 登録時のuser_keyが一意な文字列である
  • 認証開始時にユーザー情報を要求していない
  • 認証結果のuser_keyから有効なユーザーを取得できる
  • 認証成功後にセッション固定化対策を行っている
  • request_idをセッションまたはDBで照合し、再利用を防いでいる
  • 複数台構成ではSpring SessionとRedisまたはDBを利用している
  • エンドポイントが常にJSONだけを返す
  • HTTPS、Secure、HttpOnly、SameSite Cookieが有効である
  • API通信にタイムアウトを設定している
  • APIキーや認証結果をログへ出力していない