このページでは、Ruby on Railsの新規または既存プロジェクトへサンプルコードを配置し、最初に固定のテストユーザーでパスキー登録・認証を確認します。自社サイトのユーザーモデルやログイン処理へ接続するのは、その後です。
Push! Passkey APIとのJSON通信、HTTPステータス確認、結果照会、処理中のrequest_id管理、エラー処理は、Controllerのサンプルコードに含まれています。
このページのサンプルは、次の構成を前提としています。
既存プロジェクトでは、プロジェクトのディレクトリで次を実行してバージョンを確認できます。
ruby -v bin/rails -v
新規に動作確認用プロジェクトを作る場合は、Railsをインストールしたうえで次を実行します。データベースの種類は、自社環境に合わせて変更できます。
gem install rails rails new pushpasskey_sample cd pushpasskey_sample bin/rails db:prepare
| ファイル | 役割 |
|---|---|
.env | APIキーとテスト設定を保存します。Gitへ登録しません。 |
app/controllers/push_passkey_controller.rb | 登録・認証の開始、結果確認、成功後の処理を行います。 |
app/views/push_passkey/test.html.erb | 登録・認証の接続試験にだけ使用するViewです。 |
config/routes.rb | 動作確認画面とAPIエンドポイントのURLを定義します。 |
pushpasskey_sample/
├── .env
├── app/
│ ├── controllers/
│ │ └── push_passkey_controller.rb
│ └── views/
│ └── push_passkey/
│ └── test.html.erb
└── config/
└── routes.rb
認証専用の完了Controllerは必要ありません。認証結果の確認とログインセッションの発行はPushPasskeyController内で行い、成功後はボタンのdata-success-urlで指定したページへ移動します。
最初の動作確認では、環境変数を簡単に設定できるdotenv-railsを使用します。Gemfileのdevelopmentとtestグループへ追加してください。
group :development, :test do gem "dotenv-rails" end
bundle install
PUSHPASSKEY_API_KEY=ここにAPIキーを記載してください PUSHPASSKEY_TEST_MODE=true PUSHPASSKEY_TEST_USER_KEY=pushpasskey_test_user PUSHPASSKEY_CONNECT_TIMEOUT=5 PUSHPASSKEY_REQUEST_TIMEOUT=15
.envはGitへ登録しないでください。次の記載が.gitignoreにあることを確認します。
/.env /.env.*
本番環境では.envを公開サーバーへ配置せず、利用中のホスティング、コンテナ、systemdなどの機能で環境変数を設定してください。
次のコードをapp/controllers/push_passkey_controller.rbとして保存してください。最初の接続試験では、コード内を書き換える必要はありません。
require "net/http"
require "json"
require "uri"
class PushPasskeyController < ApplicationController
PUSHPASSKEY_API_BASE = "https://auth.jintec.com"
COMPLETED_STATUSES = %w[success failed expired cancelled].freeze
# passkey.jsはJSONを送信するため、Rails標準のフォーム用CSRFトークンを
# 送信しません。このエンドポイントだけ標準検証を外し、代わりに
# verify_same_origin!でOriginを検証します。
skip_forgery_protection only: :endpoint
before_action :verify_same_origin!, only: :endpoint
def test
@status = params[:pp_status].to_s.strip
@show_buttons = @status.empty?
@authenticated_user_key =
session[:pushpasskey_authenticated_user_key].to_s
end
def endpoint
case body_params["action"].to_s.strip
when "regist_start" then regist_start
when "auth_start" then auth_start
when "regist_result" then regist_result
when "auth_result" then auth_result
else
render_json({ ok: false, message: "処理の指定が正しくありません。" }, :bad_request)
end
rescue StandardError => e
Rails.logger.error("[Push! Passkey] #{e.class}: #{e.message}")
render_json(
{ ok: false, message: "Push! Passkeyの処理を完了できませんでした。" },
:internal_server_error
) unless performed?
end
private
# ==========================================================
# 本番接続時に変更する箇所 1
# ログイン中のユーザーを一意に識別する文字列を返します。
# ==========================================================
def logged_in_user_key
# 例:current_userを利用している場合
# return current_user.id.to_s if current_user
if test_mode?
return ENV.fetch("PUSHPASSKEY_TEST_USER_KEY", "").to_s.strip
end
""
end
# ==========================================================
# 本番接続時に変更する箇所 2
# パスキー登録成功後の処理です。
# ==========================================================
def after_regist_success(_result)
session[:passkey_message] = "パスキーの登録が完了しました。"
# 必要に応じて登録済み状態や登録日時をDBへ保存します。
end
# ==========================================================
# 本番接続時に変更する箇所 3
# パスキー認証成功後のログイン処理です。
# ==========================================================
def after_auth_success(result)
user_key = result["user_key"].to_s.strip
raise "認証ユーザーを確認できません。" if user_key.empty?
# 実運用ではuser_keyに一致する有効なユーザーをDBから取得し、
# reset_session後に自社サイトのログインセッションを発行します。
reset_session
session[:pushpasskey_authenticated_user_key] = user_key
session[:pushpasskey_authenticated_at] = Time.current.to_i
end
def regist_start
user_key = logged_in_user_key
return render_json({ ok: false, message: "ログイン状態を確認できません。" }, :unauthorized) if user_key.empty?
result = call_pushpasskey("/regist_url", {
"api_key" => api_key,
"user_key" => user_key,
"return_page_url" => return_page_url
})
if result["ok"] && result["request_id"].present?
save_pending_request(result["request_id"].to_s, "regist", user_key)
end
render_json(result)
end
def auth_start
result = call_pushpasskey("/auth_url", {
"api_key" => api_key,
"return_page_url" => return_page_url
})
if result["ok"] && result["request_id"].present?
save_pending_request(result["request_id"].to_s, "auth")
end
render_json(result)
end
def regist_result
request_id = required_request_id
return if performed?
pending = pending_request(request_id, "regist")
return if performed?
result = call_pushpasskey("/regist_result", {
"api_key" => api_key, "request_id" => request_id
})
if result["is_success"]
raise "登録ユーザーが一致しません。" unless result["user_key"].to_s == pending["user_key"].to_s
after_regist_success(result)
end
clear_pending_request(request_id) if result["is_completed"]
render_json(result)
end
def auth_result
request_id = required_request_id
return if performed?
pending_request(request_id, "auth")
return if performed?
result = call_pushpasskey("/auth_result", {
"api_key" => api_key, "request_id" => request_id
})
status = result["status"].to_s.strip.downcase
success = result["is_success"] || status == "success"
if success
raise "認証は成功しましたが、user_keyが返されていません。" if result["user_key"].to_s.strip.empty?
after_auth_success(result)
end
completed = result["is_completed"] || COMPLETED_STATUSES.include?(status)
clear_pending_request(request_id) if completed
render_json(result)
end
def body_params
@body_params ||= request.request_parameters.to_h.deep_stringify_keys
end
def required_request_id
request_id = body_params["request_id"].to_s.strip
if request_id.empty?
render_json({ ok: false, message: "処理情報を確認できません。" }, :bad_request)
end
request_id
end
def call_pushpasskey(path, payload)
key = api_key
raise "Push! Passkeyの設定を確認してください。" if key.empty?
uri = URI.join(PUSHPASSKEY_API_BASE, path)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.open_timeout = ENV.fetch("PUSHPASSKEY_CONNECT_TIMEOUT", "5").to_i
http.read_timeout = ENV.fetch("PUSHPASSKEY_REQUEST_TIMEOUT", "15").to_i
request_data = Net::HTTP::Post.new(uri.request_uri)
request_data["Content-Type"] = "application/json"
request_data["Accept"] = "application/json"
request_data.body = JSON.generate(payload)
response = http.request(request_data)
result = JSON.parse(response.body)
result["ok"] = false unless response.is_a?(Net::HTTPSuccess)
result
rescue JSON::ParserError
raise "Push! Passkeyから正しい応答を取得できません。"
rescue Timeout::Error, SocketError, SystemCallError, OpenSSL::SSL::SSLError => e
raise "Push! Passkeyへ接続できません。", cause: e
end
def save_pending_request(request_id, kind, user_key = "")
requests = session[:pushpasskey_requests] || {}
requests[request_id] = {
"kind" => kind, "user_key" => user_key, "created_at" => Time.current.to_i
}
session[:pushpasskey_requests] = requests
end
def pending_request(request_id, kind)
data = (session[:pushpasskey_requests] || {})[request_id]
unless data.is_a?(Hash) && data["kind"] == kind
render_json(
{ ok: false, message: "処理情報を確認できません。最初からやり直してください。" },
:bad_request
)
return nil
end
data
end
def clear_pending_request(request_id)
requests = session[:pushpasskey_requests] || {}
requests.delete(request_id)
session[:pushpasskey_requests] = requests
end
def return_page_url
value = body_params["return_page_url"].to_s.strip
uri = URI.parse(value)
return "" unless uri.is_a?(URI::HTTP) && uri.host == request.host
value
rescue URI::InvalidURIError
""
end
def verify_same_origin!
origin = request.headers["Origin"].to_s
return if origin.empty?
origin_uri = URI.parse(origin)
return if origin_uri.host == request.host
render_json({ ok: false, message: "このページからは処理を開始できません。" }, :forbidden)
rescue URI::InvalidURIError
render_json({ ok: false, message: "このページからは処理を開始できません。" }, :forbidden)
end
def api_key
ENV.fetch("PUSHPASSKEY_API_KEY", "").to_s.strip
end
def test_mode?
ActiveModel::Type::Boolean.new.cast(
ENV.fetch("PUSHPASSKEY_TEST_MODE", "false")
)
end
def render_json(data, status = :ok)
render json: data, status: status
end
endRailsのCookie Storeには約4KBの上限があります。上記サンプルは未完了のrequest_idだけを保存し、完了後に削除します。本番で多数の同時処理やサーバー間共有が必要な場合は、Redisなどのサーバー側セッションストアを使用してください。
routes.rbのRails.application.routes.draw do内へ、次の2行を追加します。
get "/pushpasskey/test", to: "push_passkey#test" post "/pushpasskey/endpoint", to: "push_passkey#endpoint"
追加後、次のコマンドでルーティングを確認できます。
bin/rails routes | grep pushpasskey
次のコードをapp/views/push_passkey/test.html.erbとして保存します。
<% content_for :head do %>
<script src="https://auth.jintec.com/js/passkey.js" defer></script>
<% end %>
<h1>Push! Passkey動作確認</h1>
<div <%= "hidden" unless @show_buttons %>>
<p>最初にパスキー登録を行い、続いて認証を確認してください。</p>
<button type="button"
data-pushpasskey="regist"
data-endpoint="/pushpasskey/endpoint"
data-success-url="/pushpasskey/test">
パスキーを登録する
</button>
<button type="button"
data-pushpasskey="auth"
data-endpoint="/pushpasskey/endpoint"
data-success-url="/pushpasskey/test">
パスキーで認証する
</button>
</div>
<div id="resultMessage" role="alert" aria-live="polite"></div>
<% if @authenticated_user_key.present? %>
<p>認証したuser_key:<%= @authenticated_user_key %></p>
<% else %>
<p>認証したuser_keyはまだありません。</p>
<% end %>
<% unless @show_buttons %>
<p>認証結果を確認しています。</p>
<% end %>結果照会中もdata-endpointとdata-success-urlを持つボタンをDOMから削除しないでください。このサンプルはhidden属性で画面上だけ非表示にします。
レイアウトでcontent_for :headを出力していない場合は、app/views/layouts/application.html.erbの<head>内へ<%= yield :head %>を追加するか、scriptタグをViewへ直接記載してください。
.envへ管理サイトで発行したAPIキーを記載し、Railsを再起動します。PUSHPASSKEY_TEST_MODEはtrueのままにします。
bin/rails server
https://自社ドメイン/pushpasskey/testを開くpushpasskey_test_userが表示されることを確認する認証開始時にはユーザー情報を送信しません。Push! Passkeyが認証に成功したCredential IDから登録時のuser_keyを特定し、認証結果として返します。
PUSHPASSKEY_TEST_MODE=false
環境変数を変更した後は、Railsプロセスを再起動してください。
パスキー登録は、すでにログインしているユーザーに対して行います。Deviseなどでcurrent_userを使用している場合は、次のように変更します。
def logged_in_user_key current_user&.id.to_s.strip end
戻り値は必ず文字列にしてください。DBのIDが整数でもto_sで文字列へ変換します。nil.to_sは空文字になるため、未ログイン時には登録開始が拒否されます。
user_keyには、変更されない内部ID、会員番号、UUIDなど、自社サイトでユーザーを一意に特定できる値を使用します。メールアドレスなど変更される可能性のある値は避けてください。
def after_regist_success(_result)
session[:passkey_message] = "パスキーの登録が完了しました。"
current_user.update!(
passkey_registered_at: Time.current
)
end
DBへ登録状態を持たない場合は、メッセージ保存だけでも問題ありません。顧客DBの登録済みフラグは、Push! Passkey管理サイトでCredentialを削除した場合に実態とずれる可能性があるため、現在も利用可能かどうかの唯一の判定材料にはしないでください。
認証結果のuser_keyでユーザーを検索し、退会・停止状態などを確認してからログインセッションを発行します。
def after_auth_success(result)
user_key = result["user_key"].to_s.strip
raise "認証ユーザーを確認できません。" if user_key.empty?
user = User.find_by(id: user_key)
unless user&.active?
raise "ログイン可能なユーザーではありません。"
end
reset_session
# Deviseを使用する例
sign_in(user)
session[:login_user_id] = user.id
end
active?は説明用です。自社サイトの有効・停止・退会状態の判定へ置き換えてください。Deviseを使用しない場合は、既存のログイン処理と同じセッション値を設定します。
reset_sessionはセッション固定攻撃を防ぐために必要です。ただし、呼び出すと現在のセッション内容が消えるため、必要な値はその後に保存してください。
passkey.jsは応答全体をJSONとして読み取ります。Controllerや共通例外処理からHTML、デバッグ文字列、Railsのエラーページが返ると、「エンドポイントの応答がJSONではありません。」と表示されます。
putsやppの内容はサーバー標準出力へ出ますが、調査にはRails.loggerを使用するrender html:やログイン画面へのリダイレクトを行わないRails.logger.error(
"[Push! Passkey] #{e.class}: #{e.message}"
)
render json: {
ok: false,
message: "ユーザー情報を取得できません。"
}, status: :internal_server_error
登録ボタンのdata-success-urlには、既存の会員メニューや登録完了ページを指定できます。認証ボタンには、ログイン後のトップページを指定します。
<button type="button" data-pushpasskey="regist" data-endpoint="/pushpasskey/endpoint" data-success-url="/mypage/passkey"> パスキーを登録する </button> <button type="button" data-pushpasskey="auth" data-endpoint="/pushpasskey/endpoint" data-success-url="/mypage"> パスキーでログイン </button>
認証成功後の移動先では、Controller内ですでにログインセッションが発行されています。移動先ページにpasskey.jsを読み込む必要はありません。
パスキー認証が成功し、自社サイトのログインセッションを発行した後も、必要に応じて登録済みパスキーの件数、端末名、ブラウザ、OS、登録日時、最終利用日時を表示できます。この機能を使用しない場合、認証完了後にPush! Passkey APIを呼び出す必要はありません。
登録情報を表示したいログイン後のページへ、次のタグとpasskey.jsを設置します。
<div class="my-passkey-list"
data-pushpasskey-credentials
data-endpoint="/pushpasskey/endpoint">
</div>
<script src="https://auth.jintec.com/js/passkey.js"></script>
表示タグを検出すると、passkey.jsはエンドポイントへcredential_statusアクションを送信します。ブラウザからuser_keyやAPIキーを送信する必要はありません。
エンドポイントは、ブラウザからuser_keyを受け取らず、既存のログインセッションから対象ユーザーを特定します。そのuser_keyとサーバー側のAPIキーをPush! Passkeyの/credential_statusへ送信します。
# endpoint のcaseへ追加します。
when "credential_status" then credential_status
def credential_status
user_key = logged_in_user_key.to_s.strip
if user_key.empty?
return render_json(
{ ok: false, message: "ログイン状態を確認できません。" },
:unauthorized
)
end
result = call_pushpasskey("/credential_status", {
"api_key" => api_key,
"user_key" => user_key
})
render_json(result)
end
上記は既存サンプルへ追加する部分を示しています。導入先のセッション構成に合わせてlogged_in_user_keyの取得処理を変更してください。user_keyはString型の一意な値として扱います。
登録済みのパスキーがある場合、表示タグの内側には概ね次のHTMLが生成されます。表示内容や件数はAPIの応答によって変わります。
<p class="pushpasskey-credentials-summary">登録済みパスキー:1件</p>
<ul class="pushpasskey-credentials-list">
<li class="pushpasskey-credential-item">
<strong class="pushpasskey-credential-device">端末名</strong>
<span class="pushpasskey-credential-environment">Chrome / Windows</span>
<span class="pushpasskey-credential-created">登録:2026/08/20 10:00</span>
<span class="pushpasskey-credential-last-used">最終利用:2026/08/20 11:00</span>
</li>
</ul>
passkey.jsが生成するclassへCSSを指定できます。導入先の他画面へ影響させないため、表示タグへ独自classを追加し、その内側だけへスタイルを適用する方法を推奨します。
.my-passkey-list .pushpasskey-credentials-summary {
font-weight: 700;
margin-bottom: 12px;
}
.my-passkey-list .pushpasskey-credentials-list {
list-style: none;
padding: 0;
}
.my-passkey-list .pushpasskey-credential-item {
margin-bottom: 12px;
}
.my-passkey-list .pushpasskey-credential-item span {
display: block;
}
user_keyを信用しないpasskey.jsは、エンドポイントから返された応答をJSONとして解析します。JSONとして解析できない場合は、単に「JSONではありません」と表示するのではなく、HTTPステータス、Content-Type、原因の判定候補、サーバーから実際に返された内容を画面へ表示します。
この表示により、Ruby on Rails側の例外や警告、Webサーバーのエラーページ、ログイン画面へのリダイレクト、デバッグ出力などを確認しやすくなります。
エンドポイントの応答をJSONとして解析できませんでした。 【HTTPステータス】 500 Internal Server Error 【Content-Type】 text/html; charset=UTF-8 【判定】 エンドポイントまたはWebサーバー側でエラーが発生した可能性があります。 【サーバーから返された内容】 実際にブラウザへ返されたエラー内容
| 表示内容 | 主な原因 | 確認する場所 |
|---|---|---|
| HTTP 404 | エンドポイントURLや公開パスが正しくありません。 | data-endpoint、ルーティング、プロキシ設定 |
| HTTP 403 | Origin、CSRF、認可、WAFなどで拒否されています。 | セキュリティ設定、リクエストヘッダー |
| HTTP 405 | POSTルートへ到達していません。 | ルーティング、Webサーバー・プロキシ設定 |
| HTTP 500以上 | アプリケーションまたはWebサーバー側で例外が発生した可能性があります。 | 画面へ表示された応答内容、Railsまたはリバースプロキシのログ |
| ログイン画面やHTML | 共通のログイン処理や例外ハンドラーがHTMLを返しています。 | 認証ミドルウェア、例外処理、リダイレクト設定 |
| デバッグ文字列とJSONが一緒に表示される | デバッグ用出力がJSONの前後へ混入しています。 | エンドポイントと共通処理の出力 |
| 応答本文が空 | アプリ停止、強制終了、タイムアウト、エラー非表示などが考えられます。 | Railsまたはリバースプロキシのログ |
passkey.jsが表示できるのは、エンドポイントからブラウザへ実際に返された内容です。アプリケーションやWebサーバーが詳細エラーをレスポンスへ出力する設定であれば、その内容も表示されます。詳細エラーをレスポンスへ出力しない設定では、HTTP 500や空の応答だけが表示される場合があります。
エラー表示・ログ記録の設定は、導入先の開発・本番環境の運用方針に合わせて設定してください。
/pushpasskey/endpointを選択する問い合わせ時は、発生時刻、登録・認証・登録情報表示のどこで発生したか、HTTPステータス、画面へ表示された応答内容、必要に応じてDevToolsのResponseとログの該当部分をお知らせください。APIキー、Cookie、セッションID、個人情報は削除してから共有してください。
user_keyで有効なユーザーが取得されるPUSHPASSKEY_TEST_MODE=falseへ変更したlogged_in_user_keyが一意な文字列を返すreset_sessionを実行しているSecure、HttpOnly、適切なSameSiteを設定しているAPIの送受信項目を詳しく確認したい場合だけ、APIリファレンスを参照してください。