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

Ruby on Railsでの実装

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

このページでは、Ruby on Railsの新規または既存プロジェクトへサンプルコードを配置し、最初に固定のテストユーザーでパスキー登録・認証を確認します。自社サイトのユーザーモデルやログイン処理へ接続するのは、その後です。

  1. 環境変数へAPIキーを設定する
  2. Controllerとルーティングをコピーする
  3. 動作確認用Viewをコピーする
  4. 登録と認証が成功した後に、自社サイトの処理へ接続する

Push! Passkey APIとのJSON通信、HTTPステータス確認、結果照会、処理中のrequest_id管理、エラー処理は、Controllerのサンプルコードに含まれています。

1. 実行環境を確認する

このページのサンプルは、次の構成を前提としています。

  • Ruby 3.2以降
  • Ruby on Rails 7.1以降
  • HTTPSで公開されているWebサイト
  • Cookieを利用するRailsセッション

既存プロジェクトでは、プロジェクトのディレクトリで次を実行してバージョンを確認できます。

ruby -v
bin/rails -v

新規に動作確認用プロジェクトを作る場合は、Railsをインストールしたうえで次を実行します。データベースの種類は、自社環境に合わせて変更できます。

gem install rails
rails new pushpasskey_sample
cd pushpasskey_sample
bin/rails db:prepare

2. 基本ファイル構成

ファイル役割
.envAPIキーとテスト設定を保存します。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で指定したページへ移動します。

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

dotenv-railsを追加する

最初の動作確認では、環境変数を簡単に設定できるdotenv-railsを使用します。Gemfiledevelopmenttestグループへ追加してください。

group :development, :test do
  gem "dotenv-rails"
end
bundle install

.envを作成する

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などの機能で環境変数を設定してください。

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

push_passkey_controller.rb

次のコードを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
end

RailsのCookie Storeには約4KBの上限があります。上記サンプルは未完了のrequest_idだけを保存し、完了後に削除します。本番で多数の同時処理やサーバー間共有が必要な場合は、Redisなどのサーバー側セッションストアを使用してください。

config/routes.rb

routes.rbRails.application.routes.draw do内へ、次の2行を追加します。

get  "/pushpasskey/test",     to: "push_passkey#test"
post "/pushpasskey/endpoint", to: "push_passkey#endpoint"

追加後、次のコマンドでルーティングを確認できます。

bin/rails routes | grep pushpasskey

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

test.html.erb

次のコードを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-endpointdata-success-urlを持つボタンをDOMから削除しないでください。このサンプルはhidden属性で画面上だけ非表示にします。

レイアウトでcontent_for :headを出力していない場合は、app/views/layouts/application.html.erb<head>内へ<%= yield :head %>を追加するか、scriptタグをViewへ直接記載してください。

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

この段階で変更するのはAPIキーだけです

.envへ管理サイトで発行したAPIキーを記載し、Railsを再起動します。PUSHPASSKEY_TEST_MODEtrueのままにします。

bin/rails server
  1. https://自社ドメイン/pushpasskey/testを開く
  2. 「パスキーを登録する」を押し、登録完了を確認する
  3. 動作確認画面へ戻り、「パスキーで認証する」を押す
  4. 認証後にpushpasskey_test_userが表示されることを確認する

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

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

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

PUSHPASSKEY_TEST_MODE=false

環境変数を変更した後は、Railsプロセスを再起動してください。

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

パスキー登録は、すでにログインしているユーザーに対して行います。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など、自社サイトでユーザーを一意に特定できる値を使用します。メールアドレスなど変更される可能性のある値は避けてください。

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

def after_regist_success(_result)
  session[:passkey_message] = "パスキーの登録が完了しました。"
  current_user.update!(
    passkey_registered_at: Time.current
  )
end

DBへ登録状態を持たない場合は、メッセージ保存だけでも問題ありません。顧客DBの登録済みフラグは、Push! Passkey管理サイトでCredentialを削除した場合に実態とずれる可能性があるため、現在も利用可能かどうかの唯一の判定材料にはしないでください。

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

認証結果の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はセッション固定攻撃を防ぐために必要です。ただし、呼び出すと現在のセッション内容が消えるため、必要な値はその後に保存してください。

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

passkey.jsは応答全体をJSONとして読み取ります。Controllerや共通例外処理からHTML、デバッグ文字列、Railsのエラーページが返ると、「エンドポイントの応答がJSONではありません。」と表示されます。

  • putsppの内容はサーバー標準出力へ出ますが、調査にはRails.loggerを使用する
  • render html:やログイン画面へのリダイレクトを行わない
  • 例外を握りつぶして空の応答を返さない
  • APIエンドポイントへHTML用の認証フィルターを適用しない
Rails.logger.error(
  "[Push! Passkey] #{e.class}: #{e.message}"
)

render json: {
  ok: false,
  message: "ユーザー情報を取得できません。"
}, status: :internal_server_error

8. 登録・認証成功後の画面

登録ボタンの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を読み込む必要はありません。

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 の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型の一意な値として扱います。

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、原因の判定候補、サーバーから実際に返された内容を画面へ表示します。

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

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

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

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

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

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

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

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

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

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

  • PUSHPASSKEY_TEST_MODE=falseへ変更した
  • APIキーをGitや公開ディレクトリへ保存していない
  • logged_in_user_keyが一意な文字列を返す
  • 認証結果から有効なユーザーだけをログインさせている
  • 認証成功時にreset_sessionを実行している
  • エンドポイントが常にJSONだけを返す
  • 同一オリジン検証を削除していない
  • 本番のCookieにSecureHttpOnly、適切なSameSiteを設定している
  • 動作確認用ルートとViewを公開環境から削除した

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