最初から自社サイトのDBやログイン処理へ接続する必要はありません。まず、設定ファイルとエンドポイントファイルを配置し、動作確認用ページでパスキー登録・認証・登録情報表示が成功することを確認します。
pushpasskey.phpへAPIキーを記載するendpoint.php先頭へ、設定ファイルのパスを記載するtest.phpを配置するPush! Passkey APIとの通信、JSONの生成、HTTPステータスの確認、結果照会、エラー処理は、サンプルコードに含まれています。これらの処理を個別に作成する必要はありません。
このページのサンプルコードはPHP 5.6以降の構文で記載しています。Push! Passkey APIとの連携自体はPHP 7以降を必須としません。
ただし、PHP 5系およびPHP 7系はPHP公式のサポートが終了しています。新規導入ではPHP 8.2以上を使用してください。PHP 5.6対応は、既存環境からの接続試験を目的とした互換性対応です。
| ファイル | 必須 | 役割 |
|---|---|---|
pushpasskey.php |
必須 | APIキーと通信時間を設定するファイルです。新しく作成します。 |
endpoint.php |
必須 | 登録・認証の開始、結果確認、成功後の処理を行います。新しく作成します。 |
test.php |
テスト用 | 登録・認証の接続試験にだけ使用するページです。 |
regist_ok.php |
テスト用 | 登録完了後に表示するページです。本番では既存の会員ページを指定します。 |
認証専用のauth_ok.phpは必要ありません。認証結果の確認とログインセッションの発行は、endpoint.php内で行います。認証成功後は、フロントエンドのdata-success-urlで指定した会員メニューなどへ移動します。
/var/www/
├── config/
│ └── pushpasskey.php
└── public/
├── pushpasskey/
│ ├── endpoint.php
│ └── test.php
└── mypage/
└── regist_ok.php
publicがWebサイトのドキュメントルートです。APIキーを記載するpushpasskey.phpは、ブラウザから直接開けないドキュメントルートの外へ配置します。
レンタルサーバーなど、ドキュメントルート外へ配置できない場合は、PHPファイルを直接表示しないWebサーバー設定としたうえで、アクセス制限された場所へ配置してください。
次のコードをコピーし、ドキュメントルート外へpushpasskey.phpとして保存してください。
<?php return array( // 管理サイトで発行したAPIキーへ書き換えてください。 'api_key' => 'ここにAPIキーを記載してください', // 最初の接続試験ではtrueのまま使用します。 // 自社サイトへ接続するときにfalseへ変更します。 'test_mode' => true, // 動作確認用のユーザー識別子です。 // 接続試験中は変更する必要はありません。 'test_user_key' => 'pushpasskey_test_user', // 通常は変更する必要はありません。 'connect_timeout' => 5, 'request_timeout' => 15 );
次の1か所だけ、管理サイトで発行したAPIキーへ書き換えます。
'api_key' => 'ここにAPIキーを記載してください',
pushpasskey.phpをドキュメントルート外へ配置するため、基本サンプルではAPIキーをこのファイルへ直接記載します。サーバーで環境変数を管理している場合は、次の書き方へ変更することもできます。
'api_key' => getenv('PUSHPASSKEY_API_KEY') ?: '',
環境変数を使用する場合も、追加のPHPファイルは必要ありません。
次のコードをendpoint.phpとして保存し、フロントエンドのdata-endpointで指定した場所へ配置してください。掲載コードは動作確認済みのサンプルと同一です。先頭のPUSHPASSKEY_CONFIG_FILEだけは、導入先でpushpasskey.phpを配置した絶対パスへ変更してください。
<?php
session_start();
/*
* ==========================================================
* 最初の動作確認で変更する箇所
* pushpasskey.phpを配置した絶対パスを指定してください。
* ==========================================================
*/
const PUSHPASSKEY_CONFIG_FILE =
'/var/www/dev.jintec.com/pushpasskey/pushpasskey.php';
// Push! Passkey APIの接続先です。この値は変更しません。
const PUSHPASSKEY_API_BASE = 'https://auth.jintec.com';
/*
* ==========================================================
* 本番接続時に変更する箇所 1
* ログイン中のユーザーを一意に識別する値を返してください。
* ==========================================================
*/
function get_logged_in_user_key($config)
{
if(!empty($_SESSION['pushpasskey_authenticated_user_key'])){
return trim(
(string)$_SESSION['pushpasskey_authenticated_user_key']
);
}
if(!empty($_SESSION['user_key'])){
return trim((string)$_SESSION['user_key']);
}
if(!empty($config['test_mode'])){
return trim((string)(isset($config['test_user_key']) ? $config['test_user_key'] : ''));
}
return '';
}
/*
* ==========================================================
* 本番接続時に変更する箇所 2
* パスキー登録が成功した後に行う処理を記載してください。
* ==========================================================
*/
function after_regist_success($result)
{
// 例:登録完了メッセージを会員画面へ表示する。
$_SESSION['passkey_message'] = 'パスキーの登録が完了しました。';
// 必要に応じて、登録済み状態や登録日時をDBへ保存してください。
}
/*
* ==========================================================
* 本番接続時に変更する箇所 3
* パスキー認証が成功した後のログイン処理を記載してください。
* ==========================================================
*/
function after_auth_success($result)
{
$user_key = isset($result['user_key'])
? trim((string)$result['user_key'])
: '';
if($user_key === ''){
throw new RuntimeException('認証ユーザーを確認できません。');
}
/*
* 実運用では、次の処理を自社サイトに合わせて実装します。
*
* 1. $user_keyに一致するユーザーをDBから取得する
* 2. ユーザーが有効であり、ログイン可能であることを確認する
* 3. 必要に応じて、安全な乱数から新しい接続キーを生成する
* 4. 接続キーや最終ログイン日時をDBへ保存する
* 5. session_regenerate_id(true)を実行する
* 6. ユーザーIDや接続キーを$_SESSIONへ保存する
*/
// 動作確認用です。これだけでは自社サイトへのログインは成立しません。
$_SESSION['pushpasskey_authenticated_user_key'] = $user_key;
$_SESSION['pushpasskey_authenticated_at'] = time();
}
function json_response($data, $status_code = 200)
{
http_response_code($status_code);
header('Content-Type: application/json; charset=UTF-8');
header('Cache-Control: no-store');
header('X-Content-Type-Options: nosniff');
echo json_encode(
$data,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
exit;
}
function request_body()
{
if(!empty($_POST)){
return $_POST;
}
$raw = file_get_contents('php://input');
if($raw === false || trim($raw) === ''){
return [];
}
$data = json_decode($raw, true);
return is_array($data) ? $data : [];
}
function assert_same_origin()
{
$origin = isset($_SERVER['HTTP_ORIGIN'])
? trim((string)$_SERVER['HTTP_ORIGIN'])
: '';
if($origin === ''){
return;
}
$origin_host = parse_url($origin, PHP_URL_HOST);
$current_host = isset($_SERVER['HTTP_HOST'])
? preg_replace('/:\\d+$/', '', (string)$_SERVER['HTTP_HOST'])
: '';
if(!is_string($origin_host) ||
$current_host === '' ||
strcasecmp($origin_host, $current_host) !== 0){
json_response([
'ok' => false,
'message' => 'このページからは処理を開始できません。',
], 403);
}
}
function return_page_url($body)
{
$url = isset($body['return_page_url'])
? trim((string)$body['return_page_url'])
: '';
if($url === '' || filter_var($url, FILTER_VALIDATE_URL) === false){
return '';
}
$url_host = parse_url($url, PHP_URL_HOST);
$current_host = isset($_SERVER['HTTP_HOST'])
? preg_replace('/:\\d+$/', '', (string)$_SERVER['HTTP_HOST'])
: '';
if(!is_string($url_host) ||
$current_host === '' ||
strcasecmp($url_host, $current_host) !== 0){
return '';
}
return $url;
}
function call_pushpasskey(
$path,
$payload,
$config
)
{
$url = PUSHPASSKEY_API_BASE.$path;
$json = json_encode(
$payload,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
if($json === false){
throw new RuntimeException('送信データを作成できません。');
}
$ch = curl_init($url);
if($ch === false){
throw new RuntimeException('通信を開始できません。');
}
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Accept: application/json',
],
CURLOPT_POSTFIELDS => $json,
CURLOPT_CONNECTTIMEOUT =>
(int)$config['connect_timeout'],
CURLOPT_TIMEOUT =>
(int)$config['request_timeout'],
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
]);
$response_body = curl_exec($ch);
$http_status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
if($response_body === false || $response_body === ''){
throw new RuntimeException('Push! Passkeyへ接続できません。');
}
$result = json_decode($response_body, true);
if(!is_array($result)){
throw new RuntimeException('Push! Passkeyから正しい応答を取得できません。');
}
if($http_status < 200 || $http_status >= 300){
$result['ok'] = false;
}
return $result;
}
function save_pending_request(
$request_id,
$kind,
$user_key = ''
)
{
$_SESSION['pushpasskey_requests'][$request_id] = [
'kind' => $kind,
'user_key' => $user_key,
'created_at' => time(),
];
}
function pending_request($request_id, $kind)
{
$data = isset($_SESSION['pushpasskey_requests'][$request_id])
? $_SESSION['pushpasskey_requests'][$request_id]
: null;
if(!is_array($data) || (isset($data['kind']) ? $data['kind'] : '') !== $kind){
json_response([
'ok' => false,
'message' => '処理情報を確認できません。最初からやり直してください。',
], 400);
}
return $data;
}
function clear_pending_request($request_id)
{
unset($_SESSION['pushpasskey_requests'][$request_id]);
}
try{
assert_same_origin();
$config = require PUSHPASSKEY_CONFIG_FILE;
if(!is_array($config) || empty($config['api_key'])){
throw new RuntimeException('Push! Passkeyの設定を確認してください。');
}
$body = request_body();
$action = isset($body['action'])
? trim((string)$body['action'])
: '';
if($action === 'regist_start'){
$user_key = get_logged_in_user_key($config);
if($user_key === ''){
json_response([
'ok' => false,
'message' => 'ログイン状態を確認できません。',
], 401);
}
$result = call_pushpasskey('/regist_url', [
'api_key' => $config['api_key'],
'user_key' => $user_key,
'return_page_url' => return_page_url($body),
], $config);
if(!empty($result['ok']) && !empty($result['request_id'])){
save_pending_request(
(string)$result['request_id'],
'regist',
$user_key
);
}
json_response($result);
}
if($action === 'auth_start'){
$result = call_pushpasskey('/auth_url', [
'api_key' => $config['api_key'],
'return_page_url' => return_page_url($body),
], $config);
if(!empty($result['ok']) && !empty($result['request_id'])){
save_pending_request(
(string)$result['request_id'],
'auth'
);
}
json_response($result);
}
if($action === 'credential_status'){
$user_key = get_logged_in_user_key($config);
if($user_key === ''){
json_response([
'ok' => false,
'message' => 'ログイン状態を確認できません。',
], 401);
}
$result = call_pushpasskey('/credential_status', [
'api_key' => $config['api_key'],
'user_key' => $user_key,
], $config);
json_response($result);
}
$request_id = isset($body['request_id'])
? trim((string)$body['request_id'])
: '';
if($request_id === ''){
json_response([
'ok' => false,
'message' => '処理情報を確認できません。',
], 400);
}
if($action === 'regist_result'){
$pending = pending_request($request_id, 'regist');
$result = call_pushpasskey('/regist_result', [
'api_key' => $config['api_key'],
'request_id' => $request_id,
], $config);
if(!empty($result['is_success'])){
$result_user_key = isset($result['user_key'])
? (string)$result['user_key']
: '';
if($result_user_key !== (string)$pending['user_key']){
throw new RuntimeException('登録ユーザーが一致しません。');
}
after_regist_success($result);
}
if(!empty($result['is_completed'])){
clear_pending_request($request_id);
}
json_response($result);
}
if($action === 'auth_result'){
pending_request($request_id, 'auth');
$result = call_pushpasskey('/auth_result', [
'api_key' => $config['api_key'],
'request_id' => $request_id,
], $config);
$status = isset($result['status'])
? strtolower(trim((string)$result['status']))
: '';
$is_success =
(!empty($result['is_success']))
|| ($status === 'success');
if($is_success){
$result_user_key = isset($result['user_key'])
? trim((string)$result['user_key'])
: '';
if($result_user_key === ''){
throw new RuntimeException(
'認証は成功しましたが、user_keyが返されていません。'
);
}
after_auth_success($result);
}
$is_completed =
(!empty($result['is_completed']))
|| in_array(
$status,
['success', 'failed', 'expired', 'cancelled'],
true
);
if($is_completed){
clear_pending_request($request_id);
}
/*
* リダイレクト後のtest.phpが確実に更新後のセッションを
* 読み込めるよう、この時点でセッションを書き込みます。
*/
if(session_status() === PHP_SESSION_ACTIVE){
session_write_close();
}
json_response($result);
}
json_response([
'ok' => false,
'message' => '処理の指定が正しくありません。',
], 400);
}catch(Exception $e){
error_log('[Push! Passkey] '.$e->getMessage());
json_response([
'ok' => false,
'message' => 'Push! Passkeyの処理を完了できませんでした。',
], 500);
}
Push! Passkey APIの接続先、JSON通信、HTTPステータス確認、処理中のrequest_id管理、登録情報取得、エラー処理は、上記コードに組み込み済みです。通常、これらの部分を書き換える必要はありません。
登録情報を表示すると、ブラウザからcredential_statusアクションが送信されます。endpoint.phpはブラウザからuser_keyを受け取らず、ログインセッションからユーザーを特定してPush! Passkeyの/credential_statusへ問い合わせます。
APIキー、user_key、公開鍵情報をフロントエンドのHTMLへ記載しないでください。
次のコードをコピーし、endpoint.phpと同じディレクトリへtest.phpとして保存してください。このページでは、自社サイトのログイン処理を実装する前に、パスキー登録・認証・登録情報表示を確認できます。
<?php session_start(); $status = isset($_GET['pp_status']) ? trim((string)$_GET['pp_status']) : ''; /* * 戻り先URLにpp_statusが付いている間も、passkey.jsが * data-endpoint等を読み取れるよう、ボタンはDOMから削除しません。 * 結果照会中は画面上だけ非表示にします。 */ $show_buttons = ($status === ''); $authenticated_user_key = isset($_SESSION['pushpasskey_authenticated_user_key']) ? $_SESSION['pushpasskey_authenticated_user_key'] : ''; ?> <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Push! Passkey動作確認</title> <script src="https://auth.jintec.com/js/passkey.js"></script> </head> <body> <h1>Push! Passkey動作確認</h1> <div<?= $show_buttons ? '' : ' hidden' ?>> <p>最初にパスキー登録を行い、続いて認証を確認してください。</p> <button type="button" data-pushpasskey="regist" data-endpoint="./endpoint.php" data-success-url="./test.php"> パスキーを登録する </button> <button type="button" data-pushpasskey="auth" data-endpoint="./endpoint.php" data-success-url="./test.php"> パスキーで認証する </button> </div> <div id="resultMessage" role="alert" aria-live="polite"></div> <h2>パスキー登録状況</h2> <div data-pushpasskey-credentials data-endpoint="./endpoint.php"> </div> <?php if($authenticated_user_key !== ''): ?> <p>認証したuser_key: <?= htmlspecialchars( $authenticated_user_key, ENT_QUOTES, 'UTF-8' ) ?> </p> <?php else: ?> <p>認証したuser_keyはまだありません。</p> <?php endif; ?> <?php if(!$show_buttons): ?> <p>認証結果を確認しています。</p> <?php endif; ?> <h2>認証セッション確認</h2> <pre><?php echo htmlspecialchars( print_r([ 'session_id' => session_id(), 'user_key' => isset($_SESSION['pushpasskey_authenticated_user_key']) ? $_SESSION['pushpasskey_authenticated_user_key'] : null, 'authenticated_at' => isset($_SESSION['pushpasskey_authenticated_at']) ? $_SESSION['pushpasskey_authenticated_at'] : null, ], true), ENT_QUOTES, 'UTF-8' ); ?></pre> </body> </html>
test.phpとendpoint.phpを同じディレクトリへ配置すれば、上記の相対URLをそのまま使用できます。たとえば両ファイルを/common/へ配置した場合、./endpoint.phpと./test.phpは、それぞれ/common/endpoint.phpと/common/test.phpとして解決されます。
結果照会中も、data-endpointとdata-success-urlを持つボタンをDOMから削除しないでください。上記サンプルはhidden属性で画面上だけ非表示にしています。
pushpasskey.phpへ、管理サイトで発行したAPIキーを記載するendpoint.php先頭のPUSHPASSKEY_CONFIG_FILEを、設定ファイルの絶対パスへ書き換えるtest_modeはtrueのまま使用します。ログインセッション、ユーザーDB、登録後処理、ログインセッション発行は、まだ実装する必要がありません。
動作確認では、test_user_keyを登録開始時だけ使用します。認証開始時にはユーザー情報を送信せず、認証結果として登録時のuser_keyが返ることを確認します。
test.phpを開くuser_keyに、登録時のtest_user_keyが表示されることを確認するたとえばtest_user_keyがpushpasskey_test_userの場合、認証後のセッション表示にも同じ値が表示されます。この値は認証開始時に顧客サイトから送信したものではなく、認証に成功したCredential IDを基にPush! Passkeyが特定し、認証結果として返した値です。
登録・認証・登録情報表示が完了し、認証後のuser_keyも一致すれば、APIキー、Push! Passkey APIとの通信、パスキー登録、Credential IDによるユーザー特定、結果照会、顧客サイト側セッションへの保存まで正常に動作しています。
エラーが表示された場合は、エラーコードを確認してください。
登録と認証の動作確認が完了したら、pushpasskey.phpのtest_modeをfalseへ変更します。
'test_mode' => false,
これ以降は、動作確認用の固定ユーザーではなく、自社サイトのログインセッションとユーザーDBを使用します。
パスキー登録は、すでにログインしているユーザーに対して行います。自社サイトでログイン中のユーザーを識別しているセッション値へ変更してください。
この関数の戻り値は、必ずstring型にしてください。自社DBのユーザーIDが整数型であっても、Push! Passkeyへ渡すuser_keyは文字列へ変換して返します。
function get_logged_in_user_key($config)
{
return isset($_SESSION['user_key'])
? trim((string)$_SESSION['user_key'])
: '';
}
このサンプルでは、(string)で明示的に文字列へ変換しています。たとえばセッションに整数の123が保存されている場合も、Push! Passkeyへは文字列の"123"を渡します。
// 正しい例:整数のIDもstring型へ変換して返す return trim((string)$_SESSION['user_id']); // 誤った例:int型のまま返している return $_SESSION['user_id'];
掲載している互換サンプルではPHP 5.6でも解析できるよう、strict_types、引数・戻り値の型宣言、Throwable、null合体演算子を使用していません。自社で型宣言を追加する場合は、セッションやDBから取得したuser_keyを必ず文字列へ変換してください。
user_keyには、会員番号、変更されない内部ID、UUIDなど、自社サイトでユーザーを一意に特定できる値を使用します。
user_keyを使用するのは登録開始時です。認証開始時には送信せず、Push! Passkeyが認証成功したCredential IDから対応するuser_keyを特定して結果として返します。
get_logged_in_user_key()は、登録開始時だけでなく、ログイン中ユーザーのパスキー登録情報を取得するときにも使用します。そのため、認証成功後に独自のログインセッションを発行する場合は、そのセッションから同じユーザーのuser_keyを復元できるように実装してください。
$_SESSION['pushpasskey_authenticated_user_key']という名前は、動作確認用サンプルのセッション名であり、利用が必須ではありません。自社サイトがlogin_user_idなどの独自セッションを使用する場合は、セッションの会員IDから自社DBを検索し、対応するPush! Passkeyのuser_keyを返します。
function get_logged_in_user_key($config)
{
if(empty($_SESSION['login_user_id'])){
return '';
}
// 説明用の関数名です。自社DBの処理へ置き換えてください。
$user = find_user_by_id((int)$_SESSION['login_user_id']);
if(!$user || (int)$user['valid'] !== 1){
return '';
}
return isset($user['pushpasskey_user_key'])
? trim((string)$user['pushpasskey_user_key'])
: '';
}
登録成功後は、すでにログインしているユーザーの会員ページへ戻るのが一般的です。必要に応じて、登録済み状態や登録日時をDBへ保存します。
function after_regist_success($result)
{
$_SESSION['passkey_message'] =
'パスキーの登録が完了しました。';
// 必要に応じて、登録済み状態や登録日時をDBへ保存します。
}
登録開始時には、サンプルコードがログインセッションを確認します。また、別サイトから登録処理を開始されないように同一オリジンを確認します。利用中のフレームワークにCSRFトークン機能がある場合は、自社の通常の重要操作と同じCSRF対策を追加してください。
パスキー認証では、Push! Passkeyから返されたuser_keyを使って自社サイトのユーザーを特定します。その後、ユーザーが有効であることを確認し、ログインセッションを発行します。
function after_auth_success($result)
{
$user_key = isset($result['user_key'])
? trim((string)$result['user_key'])
: '';
if($user_key === ''){
throw new RuntimeException(
'認証ユーザーを確認できません。'
);
}
// 1. $user_keyに一致するユーザーをDBから取得します。
$user = find_user_by_user_key($user_key);
// 2. ユーザーが存在し、ログイン可能か確認します。
if(!$user || (int)$user['valid'] !== 1){
throw new RuntimeException(
'ログイン可能なユーザーではありません。'
);
}
// 3. 必要に応じて新しい接続キーを生成します。
// openssl_random_pseudo_bytes()はPHP 5.6でも使用できます。
$connection_key = bin2hex(openssl_random_pseudo_bytes(32));
// 4. 接続キーと最終ログイン日時をDBへ保存します。
update_login_state(
(int)$user['id'],
$connection_key
);
// 5. セッション固定攻撃を防ぐためIDを再生成します。
session_regenerate_id(true);
// 6. ログインに必要な値をセッションへ保存します。
$_SESSION['login_user_id'] = (int)$user['id'];
$_SESSION['connection_key'] = $connection_key;
}
find_user_by_user_key()とupdate_login_state()は説明用の関数名です。自社サイトのDB構成と既存のログイン処理に合わせて置き換えてください。
DB更新とセッション発行は、自社サイトで現在使用しているログイン処理へ接続してください。パスキー認証が成功したという理由だけで、利用停止中や退会済みのユーザーをログインさせないようにします。
after_auth_success()が実行されただけでは、後続リクエストのローカル変数$user_keyは保持されません。登録情報を後から表示する場合は、この関数で発行したログインセッションを基に、前述のget_logged_in_user_key()がuser_keyを返せることまで確認してください。
passkey.jsは、endpoint.phpから返された応答全体をJSONとして読み取ります。JSONの前後に警告文、HTML、デバッグ文字列、空白以外の文字が混ざると、正しい結果を読み取れません。
endpoint.phpと、そこから読み込むPHPファイルでは、次の出力を行わないでください。
echo、print、var_dump()、print_r()によるデバッグ出力エラーを利用者へ直接出力するのではなく、RuntimeExceptionなどの例外を投げ、サンプルコードに含まれる共通エラー処理からJSONを返してください。調査用の情報はerror_log()でPHPエラーログへ記録します。
// 誤った例:JSON以外の文字列を直接出力する
print 'DB接続エラー:'.$e->getMessage();
exit;
// 推奨例:画面へ出力せず、例外として共通処理へ渡す
error_log('[Push! Passkey] '.$e->getMessage());
throw new RuntimeException(
'ユーザー情報を取得できません。',
0,
$e
);
本番環境ではdisplay_errorsを無効にし、log_errorsを有効にしてください。これにより、PHPの警告や内部情報がJSON応答へ混入することを防ぎながら、サーバー側のログで原因を確認できます。
フロントエンドの登録ボタンに指定したdata-success-urlへ、登録成功後に移動します。既存の会員メニューへ戻す場合は、専用ページを作成する必要はありません。
専用の登録完了画面を用意する場合は、次のような簡単なページを使用できます。
<?php
session_start();
if(empty($_SESSION['user_key'])){
header('Location: /login.php');
exit;
}
$message = isset($_SESSION['passkey_message'])
? $_SESSION['passkey_message']
: 'パスキーの登録が完了しました。';
unset($_SESSION['passkey_message']);
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>パスキー登録完了</title>
</head>
<body>
<h1><?= htmlspecialchars(
$message,
ENT_QUOTES,
'UTF-8'
) ?></h1>
<h2>登録済みのパスキー</h2>
<div
data-pushpasskey-credentials
data-endpoint="/pushpasskey/endpoint.php">
</div>
<p><a href="/mypage/">会員メニューへ戻る</a></p>
<script src="https://auth.jintec.com/js/passkey.js"></script>
</body>
</html>
登録情報をこの画面へ表示しない場合は、表示タグとpasskey.jsの読み込みを省略できます。また、登録完了画面ではなく、認証完了後のマイページ、アカウント設定画面、セキュリティ設定画面など、ログインセッションを参照できる別のページに設置しても構いません。
パスキー認証が成功し、自社サイトのログインセッションを発行した後も、必要に応じて登録済みパスキーの件数、端末名、ブラウザ、OS、登録日時、最終利用日時を表示できます。この機能を使用しない場合、認証完了後にPush! Passkey APIを呼び出す必要はありません。
登録情報を表示したいログイン後のページへ、次のタグとpasskey.jsを設置します。
<div data-pushpasskey-credentials data-endpoint="/pushpasskey/endpoint.php"> </div> <script src="https://auth.jintec.com/js/passkey.js"></script>
表示タグを検出すると、passkey.jsはendpoint.phpへcredential_statusアクションを送信します。ブラウザからuser_keyやAPIキーを送信する必要はありません。
passkey.jsは、登録情報APIの応答を取得した後、表示タグの内側へ登録件数や端末情報のHTMLを動的に生成します。表示タグを設置しただけでも情報は表示されますが、導入先サイトのCSSを使って自由にデザインを調整できます。
導入先ごとのデザインを他の画面へ影響させないため、表示タグへ独自のclassを追加する方法を推奨します。独自classはpasskey.jsによって削除されません。
<div class="my-passkey-list" data-pushpasskey-credentials data-endpoint="/pushpasskey/endpoint.php"> </div>
以降のCSSサンプルでは、導入先が追加したmy-passkey-listの内側だけへスタイルを適用します。このclass名は任意の名前へ変更できます。
登録済みのパスキーがある場合、表示タグの内側には概ね次のHTMLが生成されます。表示内容や件数はAPIの応答によって変わります。
<div class="my-passkey-list" data-pushpasskey-credentials data-endpoint="/pushpasskey/endpoint.php" data-pushpasskey-state="success" aria-live="polite" aria-busy="false"> <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> </div>
登録済みのパスキーがない場合は、表示タグの内側へ次の要素が生成されます。
<p class="pushpasskey-credentials-empty"> 登録されているパスキーはありません。 </p>
| 対象 | CSSセレクタ |
|---|---|
| 表示領域全体 | [data-pushpasskey-credentials] |
| 登録件数 | .pushpasskey-credentials-summary |
| 一覧全体 | .pushpasskey-credentials-list |
| パスキー1件分 | .pushpasskey-credential-item |
| 端末名 | .pushpasskey-credential-device |
| ブラウザ・OS | .pushpasskey-credential-environment |
| 登録日時 | .pushpasskey-credential-created |
| 最終利用日時 | .pushpasskey-credential-last-used |
| 未登録メッセージ | .pushpasskey-credentials-empty |
表示領域には処理状態を示すdata-pushpasskey-state属性も設定されます。読み込み中、表示成功、未登録、エラーの状態ごとにデザインを変更できます。
[data-pushpasskey-state="loading"] [data-pushpasskey-state="success"] [data-pushpasskey-state="empty"] [data-pushpasskey-state="error"]
次の例では、登録済みパスキーをカード形式で表示します。色、余白、横幅などは導入先サイトのデザインに合わせて変更してください。
.my-passkey-list {
max-width: 700px;
margin: 30px 0 0;
color: #333;
}
.my-passkey-list[data-pushpasskey-state="loading"] {
padding: 20px;
text-align: center;
color: #666;
background: #f7f7f7;
border: 1px solid #ddd;
border-radius: 6px;
}
.my-passkey-list .pushpasskey-credentials-summary {
margin: 0 0 12px;
padding: 10px 15px;
font-weight: bold;
color: #17695a;
background: #edf9f6;
border-left: 5px solid #28a98b;
}
.my-passkey-list .pushpasskey-credentials-list {
margin: 0;
padding: 0;
list-style: none;
}
.my-passkey-list .pushpasskey-credential-item {
display: grid;
grid-template-columns:
minmax(140px, 1.5fr) minmax(120px, 1fr);
gap: 8px 20px;
margin: 0 0 12px;
padding: 16px;
background: #fff;
border: 1px solid #d8e5e2;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.06);
}
.my-passkey-list .pushpasskey-credential-device {
display: block;
font-size: 16px;
color: #17695a;
}
.my-passkey-list .pushpasskey-credential-environment {
color: #444;
}
.my-passkey-list .pushpasskey-credential-created,
.my-passkey-list .pushpasskey-credential-last-used {
font-size: 13px;
color: #666;
}
.my-passkey-list .pushpasskey-credentials-empty {
margin: 0;
padding: 16px;
color: #555;
background: #f7f7f7;
border: 1px solid #ddd;
border-radius: 6px;
}
.my-passkey-list[data-pushpasskey-state="error"] {
padding: 16px;
color: #a40000;
background: #fff2f2;
border: 1px solid #e3a5a5;
border-radius: 6px;
}
@media screen and (max-width: 600px) {
.my-passkey-list .pushpasskey-credential-item {
display: block;
}
.my-passkey-list .pushpasskey-credential-device,
.my-passkey-list .pushpasskey-credential-environment,
.my-passkey-list .pushpasskey-credential-created,
.my-passkey-list .pushpasskey-credential-last-used {
display: block;
margin-bottom: 6px;
}
}
Ctrl+Uで表示されるページソースには、JavaScript実行後に追加されたHTMLは表示されません。生成後のHTMLは、Chrome DevToolsの「Elements」で確認します。
Ctrl+Fでdata-pushpasskey-credentialsを検索する「Console」では、次のコードから生成後のHTML全体を確認できます。
document.querySelector( '[data-pushpasskey-credentials]' ).outerHTML
DevToolsの「Elements」で要素を選択すると、右側の「Styles」からCSSを一時的に追加して表示を試せます。ここで追加したCSSはページを再読み込みすると消えるため、調整後は導入先サイトのCSSファイルへ記載してください。
endpoint.phpへ登録情報の取得を要求します。endpoint.phpはログインセッションを確認し、get_logged_in_user_key()から対象ユーザーのuser_keyを取得します。endpoint.phpが、サーバー側に保存したAPIキーとuser_keyをauth.jintec.comへ送信します。user_keyに一致する有効な登録情報だけを返します。endpoint.phpは表示用の応答をブラウザへ返し、passkey.jsが一覧を描画します。登録情報が表示されない場合や内容を確認したい場合は、Chrome DevToolsの「Network」を使用します。
endpoint.phpへのリクエストを選択するブラウザから送信されるのは、基本的に次のアクションだけです。
{
"action": "credential_status"
}
user_keyとAPIキーはブラウザから送信されません。endpoint.phpがログインセッションからuser_keyを取得し、サーバー側に保存されたAPIキーと合わせてPush! Passkeyへ送信します。
クライアントサイトは、pushpasskey_authenticated_user_keyというセッション名を使用する必要はありません。ただし、after_auth_success()で発行した独自セッションから、後続のget_logged_in_user_key()が同じユーザーのuser_keyを取得できる必要があります。
たとえば、ログインセッションへ自社の会員IDだけを保存する場合は、その会員IDで自社DBを検索し、保存済みのPush! Passkey user_keyを取得します。セッションの存在だけを確認して固定値や別ユーザーの値を返してはいけません。
user_keyを受け取らないcredential_source_jsonをブラウザへ返さないpasskey.jsは、endpoint.phpから返された応答をJSONとして解析します。JSONとして解析できない場合は、単に「JSONではありません」と表示するのではなく、HTTPステータス、Content-Type、原因の判定候補、サーバーから実際に返された内容を画面へ表示します。
この表示により、endpoint.phpのPHPエラー、未定義変数、構文エラー、Webサーバーのエラーページ、ログイン画面へのリダイレクト、echoやvar_dump()などの不要な出力を確認しやすくなります。
endpoint.php の応答をJSONとして解析できませんでした。 HTTPステータス: 200 OK Content-Type: text/html; charset=UTF-8 判定: endpoint.phpのPHPエラーまたは警告がJSON応答へ混入している可能性があります。 サーバーから返された内容: Warning: Undefined variable $user_key in /var/www/example/endpoint.php on line 58
上記のようにPHPのメッセージが表示された場合は、記載されているファイル名、行番号、変数名などを手掛かりにendpoint.phpまたは読み込み先のPHPを修正してください。
| 表示内容 | 主な原因 | 確認する場所 |
|---|---|---|
Warning、Notice、TypeError、Fatal error、Parse error |
PHPの変数、型、関数呼び出し、構文などに誤りがあります。 | 表示されたファイルと行番号、PHPエラーログ |
| HTTP 404 | endpoint.phpが見つからない、またはdata-endpointのURLが正しくありません。 |
data-endpoint、ファイル配置、URL |
| HTTP 403 | Webサーバー側でアクセスが拒否されています。 | アクセス制限、Webサーバー設定 |
| HTTP 405 | POSTリクエストが許可されていない、またはルーティングが正しくありません。 | Webサーバー設定、PHP実行設定、ルーティング |
| HTTP 500以上 | endpoint.phpまたはWebサーバー側でエラーが発生した可能性があります。 |
画面へ表示された応答内容、PHP・Webサーバーのエラーログ |
| ログイン画面やエラー画面のHTML | 共通のログインチェックやエラーハンドラーがHTMLを返しています。 | endpoint.phpから読み込む共通処理、リダイレクト処理 |
| デバッグ文字列とJSONが一緒に表示される | echo、print、var_dump()などがJSONの前後へ出力されています。 |
endpoint.phpと読み込み先PHPの出力処理 |
| 応答本文が空 | PHP処理の強制終了、PHP側でエラー内容を画面出力しない設定、Webサーバー障害などが考えられます。 | PHP・Webサーバーのエラーログ |
passkey.jsが表示できるのは、endpoint.phpからブラウザへ実際に返された内容です。PHPのdisplay_errorsが有効で、PHPがWarningやFatal errorなどをレスポンスへ出力している場合は、その内容も表示されます。
一方、PHP側でエラーを画面へ出力しない設定の場合は、HTTP 500や空の応答だけが返ることがあります。その場合は、表示されたHTTPステータスを確認したうえで、導入先サーバーのPHPまたはWebサーバーのエラーログを確認してください。PHPのエラー表示設定は、導入先の開発・本番環境の運用方針に合わせて設定してください。
get_logged_in_user_key()がint型ではなくstring型を返しているかafter_regist_success()とafter_auth_success()内で文字列を直接出力していないかendpoint.phpがログイン画面や共通エラー画面へ転送されていないか画面へ表示された内容だけでは原因を特定できない場合は、Chrome DevToolsの「Network」でendpoint.phpへのリクエストを選択し、「Response」とHTTPステータスを確認してください。
問い合わせ時は、発生時刻、登録・認証のどちらで発生したか、HTTPステータス、画面へ表示された応答内容、必要に応じてDevToolsのResponse、PHPまたはWebサーバーのエラーログをお知らせください。APIキー、Cookie、セッションID、個人情報は削除してから共有してください。
自社サイトの処理へ接続した後は、次の順番で最終確認します。
認証ボタンのdata-success-urlは、after_auth_success()でログインセッションを発行した後の移動先です。通常、認証結果を確定するためだけに移動先ページでpasskey.jsを読み込む必要はありません。
ただし、移動先ページでパスキー登録情報を表示する場合は、passkey.jsを読み込み、data-pushpasskey-credentialsを指定した表示タグを設置してください。
test_modeをfalseへ変更したget_logged_in_user_key()がstring型の一意な値を返すuser_keyから、有効なユーザーだけをログインさせているendpoint.phpが常にJSONだけを返すtest.phpを公開サーバーから削除した本番運用を開始する前に、test_modeがfalseになっていることと、動作確認用のtest.phpを公開サーバーから削除したことを確認してください。
APIの送受信項目を詳しく確認したい場合だけ、APIリファレンスを参照してください。