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

PHPでの実装

最初にPush! Passkeyだけを動作確認します

最初から自社サイトのDBやログイン処理へ接続する必要はありません。まず、設定ファイルとエンドポイントファイルを配置し、動作確認用ページでパスキー登録・認証・登録情報表示が成功することを確認します。

  1. pushpasskey.phpへAPIキーを記載する
  2. endpoint.php先頭へ、設定ファイルのパスを記載する
  3. 動作確認用のtest.phpを配置する
  4. 登録・認証・登録情報表示を確認した後に、自社サイトの処理へ接続する

Push! Passkey APIとの通信、JSONの生成、HTTPステータスの確認、結果照会、エラー処理は、サンプルコードに含まれています。これらの処理を個別に作成する必要はありません。

1. PHP実行環境

このページのサンプルコードはPHP 5.6以降の構文で記載しています。Push! Passkey APIとの連携自体はPHP 7以降を必須としません。

ただし、PHP 5系およびPHP 7系はPHP公式のサポートが終了しています。新規導入ではPHP 8.2以上を使用してください。PHP 5.6対応は、既存環境からの接続試験を目的とした互換性対応です。

  • 推奨・動作確認環境:PHP 8.2以上
  • サンプルコードの構文互換性:PHP 5.6以上
  • PHP cURL拡張
  • PHP JSON拡張
  • PHPセッション
  • TLS 1.2以上で外部HTTPS通信が可能なcURL・OpenSSL環境
  • HTTPSで公開されているWebサイト

2. 基本ファイル構成

用意するファイル

ファイル 必須 役割
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サーバー設定としたうえで、アクセス制限された場所へ配置してください。

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

pushpasskey.php

次のコードをコピーし、ドキュメントルート外へ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ファイルは必要ありません。

4. endpoint.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へ記載しないでください。

5. 動作確認用ページを作成する

test.php

次のコードをコピーし、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.phpendpoint.phpを同じディレクトリへ配置すれば、上記の相対URLをそのまま使用できます。たとえば両ファイルを/common/へ配置した場合、./endpoint.php./test.phpは、それぞれ/common/endpoint.php/common/test.phpとして解決されます。

結果照会中も、data-endpointdata-success-urlを持つボタンをDOMから削除しないでください。上記サンプルはhidden属性で画面上だけ非表示にしています。

6. 登録・認証・登録情報表示を動作確認する

この段階で変更するのは2か所だけです

  1. pushpasskey.phpへ、管理サイトで発行したAPIキーを記載する
  2. endpoint.php先頭のPUSHPASSKEY_CONFIG_FILEを、設定ファイルの絶対パスへ書き換える

test_modetrueのまま使用します。ログインセッション、ユーザーDB、登録後処理、ログインセッション発行は、まだ実装する必要がありません。

動作確認では、test_user_keyを登録開始時だけ使用します。認証開始時にはユーザー情報を送信せず、認証結果として登録時のuser_keyが返ることを確認します。

パスキー登録を確認する

  1. ブラウザでtest.phpを開く
  2. 「パスキーを登録する」を押す
  3. 画面の案内に従ってパスキーを登録する
  4. 「パスキー登録が完了しました」と表示されることを確認する

パスキー認証を確認する

  1. 「動作確認ページへ戻る」を押す
  2. 「パスキーで認証する」を押す
  3. 先ほど登録したパスキーで認証する
  4. 「パスキー認証が完了しました」と表示されることを確認する
  5. 「認証セッション確認」のuser_keyに、登録時のtest_user_keyが表示されることを確認する

たとえばtest_user_keypushpasskey_test_userの場合、認証後のセッション表示にも同じ値が表示されます。この値は認証開始時に顧客サイトから送信したものではなく、認証に成功したCredential IDを基にPush! Passkeyが特定し、認証結果として返した値です。

登録・認証・登録情報表示が完了し、認証後のuser_keyも一致すれば、APIキー、Push! Passkey APIとの通信、パスキー登録、Credential IDによるユーザー特定、結果照会、顧客サイト側セッションへの保存まで正常に動作しています。

エラーが表示された場合は、エラーコードを確認してください。

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

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

登録と認証の動作確認が完了したら、pushpasskey.phptest_modefalseへ変更します。

'test_mode' => false,

これ以降は、動作確認用の固定ユーザーではなく、自社サイトのログインセッションとユーザーDBを使用します。

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

パスキー登録は、すでにログインしているユーザーに対して行います。自社サイトでログイン中のユーザーを識別しているセッション値へ変更してください。

この関数の戻り値は、必ず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'])
		: '';
}

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

登録成功後は、すでにログインしているユーザーの会員ページへ戻るのが一般的です。必要に応じて、登録済み状態や登録日時をDBへ保存します。

function after_regist_success($result)
{
	$_SESSION['passkey_message'] =
		'パスキーの登録が完了しました。';

	// 必要に応じて、登録済み状態や登録日時をDBへ保存します。
}

登録開始時には、サンプルコードがログインセッションを確認します。また、別サイトから登録処理を開始されないように同一オリジンを確認します。利用中のフレームワークにCSRFトークン機能がある場合は、自社の通常の重要操作と同じCSRF対策を追加してください。

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

パスキー認証では、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を返せることまで確認してください。

変更時の重要事項:endpoint.phpはJSONだけを返します

passkey.jsは、endpoint.phpから返された応答全体をJSONとして読み取ります。JSONの前後に警告文、HTML、デバッグ文字列、空白以外の文字が混ざると、正しい結果を読み取れません。

endpoint.phpと、そこから読み込むPHPファイルでは、次の出力を行わないでください。

  • echoprintvar_dump()print_r()によるデバッグ出力
  • PHP開始タグより前、またはPHP終了タグより後の文字やHTML
  • Warning、Notice、Deprecated、Fatal errorなどのPHPエラー画面への表示
  • ログインチェック処理などによるHTMLページへのリダイレクト

エラーを利用者へ直接出力するのではなく、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応答へ混入することを防ぎながら、サーバー側のログで原因を確認できます。

8. 登録完了後の画面

フロントエンドの登録ボタンに指定した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の読み込みを省略できます。また、登録完了画面ではなく、認証完了後のマイページ、アカウント設定画面、セキュリティ設定画面など、ログインセッションを参照できる別のページに設置しても構いません。

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

パスキー認証が成功し、自社サイトのログインセッションを発行した後も、必要に応じて登録済みパスキーの件数、端末名、ブラウザ、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.jsendpoint.phpcredential_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名は任意の名前へ変更できます。

passkey.jsが生成するHTML

登録済みのパスキーがある場合、表示タグの内側には概ね次の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で使用できるclassと状態属性

対象 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"]

基本CSSサンプル

次の例では、登録済みパスキーをカード形式で表示します。色、余白、横幅などは導入先サイトのデザインに合わせて変更してください。

.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;
	}
}

ブラウザで生成後のHTMLを確認する

Ctrl+Uで表示されるページソースには、JavaScript実行後に追加されたHTMLは表示されません。生成後のHTMLは、Chrome DevToolsの「Elements」で確認します。

  1. 登録情報を表示するページを開き、一覧が表示されるまで待つ
  2. 表示された端末名などの上で右クリックし、「検証」を選択する
  3. DevToolsの「Elements」で選択された要素を確認する
  4. 必要に応じてCtrl+Fdata-pushpasskey-credentialsを検索する

「Console」では、次のコードから生成後のHTML全体を確認できます。

document.querySelector(
	'[data-pushpasskey-credentials]'
).outerHTML

DevToolsの「Elements」で要素を選択すると、右側の「Styles」からCSSを一時的に追加して表示を試せます。ここで追加したCSSはページを再読み込みすると消えるため、調整後は導入先サイトのCSSファイルへ記載してください。

APIキーとuser_keyを送信するルール

  1. ブラウザは、同一サイトのendpoint.phpへ登録情報の取得を要求します。
  2. endpoint.phpはログインセッションを確認し、get_logged_in_user_key()から対象ユーザーのuser_keyを取得します。
  3. endpoint.phpが、サーバー側に保存したAPIキーとuser_keyauth.jintec.comへ送信します。
  4. Push! PasskeyはAPIキーから契約先と対象ドメインを特定し、その範囲内でuser_keyに一致する有効な登録情報だけを返します。
  5. endpoint.phpは表示用の応答をブラウザへ返し、passkey.jsが一覧を描画します。

ブラウザで登録情報APIの応答を確認する

登録情報が表示されない場合や内容を確認したい場合は、Chrome DevToolsの「Network」を使用します。

  1. DevToolsの「Network」を開く
  2. 登録情報を表示するページを再読み込みする
  3. endpoint.phpへのリクエストを選択する
  4. 「Payload」で送信内容を確認する
  5. 「Response」または「Preview」で応答JSONを確認する

ブラウザから送信されるのは、基本的に次のアクションだけです。

{
	"action": "credential_status"
}

user_keyとAPIキーはブラウザから送信されません。endpoint.phpがログインセッションからuser_keyを取得し、サーバー側に保存されたAPIキーと合わせてPush! Passkeyへ送信します。

パスキー認証後にログインセッションを発行し、endpoint.phpがセッションからuser_keyを復元して、APIキーとともにauth.jintec.comへ送信し、登録件数と端末情報を取得する流れ
認証成功後のセッション発行と、登録情報表示APIの処理フロー

独自のログインセッションを使用する場合

クライアントサイトは、pushpasskey_authenticated_user_keyというセッション名を使用する必要はありません。ただし、after_auth_success()で発行した独自セッションから、後続のget_logged_in_user_key()が同じユーザーのuser_keyを取得できる必要があります。

たとえば、ログインセッションへ自社の会員IDだけを保存する場合は、その会員IDで自社DBを検索し、保存済みのPush! Passkey user_keyを取得します。セッションの存在だけを確認して固定値や別ユーザーの値を返してはいけません。

セキュリティ上の注意

  • APIキーをHTMLやJavaScriptへ記載しない
  • ブラウザのパラメータやフォームからuser_keyを受け取らない
  • 未ログイン、利用停止、退会済みのユーザーには登録情報を返さない
  • ログアウト時にログインセッションと関連する認証状態を破棄する
  • 公開鍵本体、Credential ID、credential_source_jsonをブラウザへ返さない

10. endpoint.phpからJSON以外の応答が返された場合

passkey.jsは、endpoint.phpから返された応答をJSONとして解析します。JSONとして解析できない場合は、単に「JSONではありません」と表示するのではなく、HTTPステータス、Content-Type、原因の判定候補、サーバーから実際に返された内容を画面へ表示します。

この表示により、endpoint.phpのPHPエラー、未定義変数、構文エラー、Webサーバーのエラーページ、ログイン画面へのリダイレクト、echovar_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を修正してください。

主な表示内容と確認箇所

表示内容 主な原因 確認する場所
WarningNoticeTypeErrorFatal errorParse 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が一緒に表示される echoprintvar_dump()などがJSONの前後へ出力されています。 endpoint.phpと読み込み先PHPの出力処理
応答本文が空 PHP処理の強制終了、PHP側でエラー内容を画面出力しない設定、Webサーバー障害などが考えられます。 PHP・Webサーバーのエラーログ

PHPのエラー表示設定について

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型を返しているか
  • 関数外で作成したDB接続変数を、関数内から正しく参照できているか
  • 変数名の取り違えや未定義変数がないか
  • after_regist_success()after_auth_success()内で文字列を直接出力していないか
  • endpoint.phpがログイン画面や共通エラー画面へ転送されていないか
  • PHPのWarningやNoticeなどがJSON応答へ混入していないか

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

画面へ表示された内容だけでは原因を特定できない場合は、Chrome DevToolsの「Network」でendpoint.phpへのリクエストを選択し、「Response」とHTTPステータスを確認してください。

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

11. 自社サイトへの接続を確認する

自社サイトの処理へ接続した後は、次の順番で最終確認します。

  1. 自社サイトへログインし、対象ユーザーへパスキーを登録する
  2. 登録成功後に指定した会員ページへ戻る
  3. 自社サイトからログアウトし、パスキー認証を行う
  4. 認証したユーザーのDB情報が取得される
  5. 自社サイトのログインセッションが発行され、会員ページが表示される

認証ボタンのdata-success-urlは、after_auth_success()でログインセッションを発行した後の移動先です。通常、認証結果を確定するためだけに移動先ページでpasskey.jsを読み込む必要はありません。

ただし、移動先ページでパスキー登録情報を表示する場合は、passkey.jsを読み込み、data-pushpasskey-credentialsを指定した表示タグを設置してください。

本番公開前のチェックリスト

  • test_modefalseへ変更した
  • get_logged_in_user_key()がstring型の一意な値を返す
  • 登録対象がログイン中の有効なユーザーであることを確認している
  • 認証結果のuser_keyから、有効なユーザーだけをログインさせている
  • 認証成功後にセッションIDを再生成している
  • endpoint.phpが常にJSONだけを返す
  • PHPのエラー表示・ログ記録設定を、自社の開発・本番環境の運用方針に合わせて設定している
  • 動作確認用のtest.phpを公開サーバーから削除した

本番運用を開始する前に、test_modefalseになっていることと、動作確認用のtest.phpを公開サーバーから削除したことを確認してください。

APIの送受信項目を詳しく確認したい場合だけ、APIリファレンスを参照してください。