WebAuthnによる認証機能を作りながら理解を深める

何をやったか

最近の仕事柄興味があったのと、WEB+DB PRESS Vol.114の特集2を読んだこともあって、理解を深めるためにWebAuthnでの公開鍵登録(今回はサインアップを兼ねる)、認証だけできる簡単なWebアプリを作りました。リポジトリのREADMEに様子のGIFアニメを貼っています*1。今回はChrome 80とTouch IDで試しています:

GitHub - kymmt90/webauthn_app

このボタンをクリックするとHerokuにデプロイできます。リポジトリのREADMEにも同じボタンを置いています:

Deploy

デプロイ後に環境変数 WEBAUTHN_ORIGINhttps://<Herokuアプリ名>.herokuapp.com を追加してください。

理解できた点

WebAuthn自体の詳しい説明は、Web上のリソースを参照したほうがよいので、今回はとくに書いていません。

登場する各ロールの役割

WebAuthの認証フローを見ると用語が多いですが、概ね次のように理解しました。

  • Authenticator
    • ユーザーエージェントからアクセスされる認証器
    • ハードウェアトークン(Yubikey、Titanなど)やデバイス埋め込みの認証機構(Touch IDなど)
  • Relying Party (RP)
    • Authenticatorからもらった情報をもとに認証するサーバ
    • ブラウザから公開鍵とattestation(認証器自体の正当性証明)を受け取り、公開鍵をアカウントと紐付けて保存する
    • ブラウザからassertion(認証時の署名など)を受け取り、認証する

実装の感触

個人的にRailsだとさっと実装できるので、今回はRailsでやりました。webauthn-rubywebauthn-jsonの組み合わせで問題なく実装できます*2。実装項目は次のような感じです。

サーバ

  • クライアントがWebAuthn APIへ渡すためのオプションやチャレンジ値を返すAPIを追加する
    • 公開鍵登録時は GET /webauthn/credential_creation_options
      • WebAuthn::Credential.options_for_create を使う
    • 認証時は GET /webauthn/credential_request_options
      • WebAuthn::Credential.options_for_get を使う
  • 送られてきたアカウント名、公開鍵、attestationから、アカウントを作成して公開鍵をそのアカウントに紐付けて保存するAPIを追加する
    • POST /users
      • WebAuthn::Credential.from_create / WebAuthn::PublicKeyCredentialWithAttestation#verify を使う
  • 送られてきたアカウント名とassertionから、パスワードレス認証するAPIを追加する
    • POST /session
      • WebAuthn::Credential.from_get / WebAuthn::PublicKeyCredentialWithAssertion#verify を使う

クライアント

  • サインアップ用フォーム
    • サーバからもらったオプションを navigator.credentials.create() に渡して認証器からattestationをもらいサーバへ送る
  • ログイン用フォーム
    • サーバからもらったオプションを navigator.credentials.get() に渡して認証器からassertionをもらいサーバへ送る

RPサーバは署名、オリジン、チャレンジ値、認証回数の検証などプロトコルに規定されている検証を実行する必要がありますが、ライブラリである程度カバーできると思います。

認証体験

ふだんパスワードマネージャを使ってはいますが、やはりパスワードレスだと認証の体験がかなり簡単に感じました。これで多要素も満たせる(所持、生体)のも便利ですね。

*1:なぜかはてなブログにGIFを貼ろうとするとエラーになった

*2:webauthn-rubyはデモアプリも公開していて、今回はそれを模倣している

外部サービスのリソースをJSONシリアライザブルなActive Modelとして表現する

RailsでWeb APIを作っていて、外部のサービスからリソースを取得し、DBには保存しないもののレスポンスに含めてクライアントに返したい、ということがありました。このとき次のモジュールが役立ったので紹介します。

モジュールの説明

ActiveModel::Model はActive Recordとも互換性のあるインタフェースでRails上のモデルとして扱える機能がいろいろ入るモジュールです。ActiveModel::AttributesAttributes APIを提供するモジュールです。最後の ActiveModel::Serializers::JSON は有名なActiveModelSerializers gemと名前は似ているのですが別物の、rails/railsactivemodel gem内に含まれるモジュールです。

実現方法

POROに ActiveModel::Serializers::JSON をincludeして render :json に渡すと、Attributes APIの attribute で定義した属性からなるJSONをシリアライズできるようになります。これを利用して、次のようなクラスを作りました。

まず、外部サービスから得られるリソースを表すクラスを作り、上述したモジュールをincludeします。

class ExternalServiceResource
  include ActiveModel::Model
  include ActiveModel::Attributes
  include ActiveModel::Serializers::JSON
end

次に、ExternalServiceResource をもとに具体的なリソースを表すクラスを作ります。ここでは外部ブログサービスから記事リソースを取得しているとします。

class Article < ExternalServiceResource
  attribute :title, :string
  attribute :content, :string
  attribute :status, :integer
  attribute :created_at, :datetime

  DRAFT = 0
  PUBLIC = 1
  DELETED = 2

  def unavailable?
    status.in?([DRAFT, DELETED])
  end
end

class Blog
  # ...

  def articles
    raw_response = client.articles
    raw_articles = JSON.parse(raw_response)['articles']

    raw_articles
      .map { |raw_article| Article.new(raw_article) } # 生JSONをモデルオブジェクトへ変換
      .reject(&:unavailable?)
  end

  private

  def client
    # 外部サービスのWeb APIを叩いてJSONを返すクライアント
  end
end

これらを使うと、外部サービスのリソースも含んだJSONを返すエンドポイント自体は、シンプルに実装できます。

class ArticlesController < ActionController::API
  # articles#index で外部ブログ記事のうちpublicなものを取得できる
  def index
    render json: { articles: blog.articles }
  end

  private

  def blog
    # Blogのインスタンスを返す
  end
end

pros/cons

この方法の利点は、サードパーティgemのActiveModelSerializersを使うのに比べると、Railsのモジュールだけで実現できてシンプルというところや、モデルとして外部リソースを表現できるので、そのリソースに関するロジックをモデルに置くことができるという点が挙げられます。

逆に、欠点として、自前実装になるのでうまくやらないと負債になりそうな点や、リソースの構造がネストするときに、Attributes APIでは ActiveModel::Type.register を使って自前の型を定義していくと実装がやや面倒という点です。ただし、ネストしているリソースについて型を定義せず、次のように型を指定せずに属性を定義することもできるにはできます。

# Article#author に入れるためのモデル
class Author < ExternalServiceResource
  attribute :name
  # ...
end

class Article < ExternalServiceResource
  attribute :title, :string
  attribute :content, :string
  attribute :status, :integer
  attribute :created_at, :datetime

  # Authorのオブジェクトを入れるつもりだが、型指定はとくになし
  attribute :author

  # ...
end

Active Recordでstring型属性を暗号化するためのRailsプラグインを作った

複数プロジェクトで、Active Recordのstring型を拡張して透過的に文字列を暗号化/復号できる型をattributes API(ActiveRecord::Attributes) を使って書く場面を目撃したり、自分でも書く機会があったので、Railsプラグインに切り出してみました。

github.com

使い方は次のとおりです。

# users.tokenはstring型
class User < ActiveRecord::Base
  attribute :token, :encrypted_string
end

# 環境変数かconfigで設定する
ENV['ENCRYPTED_STRING_PASSWORD'] = 'password'
ENV['ENCRYPTED_STRING_SALT'] = SecureRandom.random_bytes
# ActiveRecord::Type::EncryptedString.encryption_password = 'password'
# ActiveRecord::Type::EncryptedString.encryption_salt = SecureRandom.random_bytes

# DB内では暗号化されているがオブジェクト経由で取り出すと復号されている
user = User.create(token: 'token_to_encrypt')
ActiveRecord::Base.connection.select_value('SELECT token FROM users') #=> "eVZzbUlXME1xSlZ5ZWZPQnIvY..."
user.token #=> "token_to_encrypt"

内部的には ActiveSupport::MessageEncryptor を使う、よくある実装になっています。gemにすることですぐに使えるようになって便利ですね。

OpenAPI 3ドキュメントも使えるSchemaConformist 0.3.0をリリースした

rubygems.org

これまでのバージョンの差分はOpenAPI 3ドキュメントが使えるようになった点です。OpenAPI 3に対応したCommittee v3の機能を使うことで、integration test/request spec実行中にOpenAPI 3ドキュメント中のスキーマに基づいたJSONレスポンスのバリデーションを自動実行できるようになりました。

OpenAPI 3についてはRubyKaigi 2019での@ota42yさんの発表資料や、WEB+DB PRESS Vol.108の特集1が参考になります。

speakerdeck.com

gihyo.jp

以上の対応に伴って、Committeeの機能を利用して自動でドキュメントフォーマットが判別できるようになったので、オプション driver の指定は不要になりました。また、オプション schema_path については必須とすることにしました。これは、schema_path を指定しなかったときにデフォルトで入るパス(public/swagger.json など)がとくに一般的なものではないことと、driver をオプションとして渡さなくなったことによります。

他には、ignored_api_paths は正規表現だけでなくふつうのStringも渡せるようにしました。渡された文字列と前方一致するパスを検索して、そのパスで表されるエンドポイントについてはテスト実行時にバリデーションをスキップします。

もし仮に使っている人がいれば、なにかおかしいところがあったら教えてください。

『改正割賦販売法でカード決済はこう変わる』を読んだ

2019年の1Qに読んだ本シリーズ:

改正 割賦販売法でカード決済はこう変わる

改正 割賦販売法でカード決済はこう変わる

概要

2018年6月から施行されている「改正割賦販売法」によって、クレジットカード加盟店がクレジットカードを扱う際の対応がどのように変わったのか、という点について解説されています。とくに、クレジットカードセキュリティのグローバルな基準であるPCI DSSと、国内でのクレジットカードセキュリティの実務指針である「実行計画」に対して、どのように対応していけばよいかについて説明されています。

割賦販売法はクレジットカードの根拠法です。なぜ割賦販売法が改正されたかというと、主にはカード情報流出対策として加盟店に対するカード情報保護義務を規制として追加するため、という理由があります(他にもある)。また、実務指針である「実行計画」は、オリンピックが開催される2020年までは毎年更新されるので、訪日外国人のカード決済を保護していくという意味合いも持っているようです。

なぜ読んだのか

EC系Webサービスを開発/運用しているとPCI DSSという言葉をよく聞くのですが、実態がよくわからないので知りたかったというのがあります。非保持化に関しては実装の経験が何度かありましたが、PCI DSSに対応する場合との違いについても知りたいと思っていました。また、そもそも非保持化などの対応が必要になった理由を知っておきたかったというところもあります。

メモ

PCI DSSと「実行計画」

PCI DSSはクレジットカードに関するグローバルな業界セキュリティ基準です。

PCI Security Standards Council公式サイト

一方、日本クレジット協会が発表している「実行計画」は、日本においてクレジットカードのセキュリティ対策をおこなう際の実務的な指針です。2019年版はこちらです。

関連資料 | 安全・安心なクレジットカード取引への取組み | 一般社団法人日本クレジット協会

クレジットカード情報の「非保持化」は実行計画で示されているものであり、日本国内でのみ採用している方式です。実行計画では、非保持化しない場合はPCI DSSへの対応を求めています。

ここで、「非保持化」とは、例えばECサイトをホストしているサーバが生のクレジットカード情報を保存/処理せず、そもそもクレジットカード情報自体がサーバを通過しない、ということを表しています。ただし、クレジットカードの電磁的なデータだけがその対象となり、紙媒体や通話録音データは保持に当たらない、と規定されています(厳重に保管する必要はある)。

また、非保持化という対応が許されるのは一般の加盟店だけであり、イシュア/アクワイアラ/決済代行業者や国外でサービスを提供する加盟店は、つねにPCI DSSに準拠している必要があります。

非保持化の方法

Webアプリケーションの開発者的に興味がありそうなのはここかと思います。EC加盟店で非保持化するためには次のような方式が利用できます。

  • リダイレクト(リンク型)決済
  • JavaScript型(トークン)決済

リダイレクト決済は、決済サービスのプロバイダが提供する決済用ページへ顧客を誘導する方式です。クレジットカードの処理をプロバイダへ完全に委譲できるのでセキュアですが、加盟店のページと別のドメインのページへとユーザを遷移させることになるので、カゴ落ちが起きやすくなるのが欠点です。

JavaScript型は、トークン化などのカード番号無効化処理をブラウザとPSPの間で実行し、そのトークンをEC加盟店のサーバが決済時に使う方式です。トークンはカード番号ではないので、非保持化の要件を満たしています。また、加盟店のページで決済が完結するのでカゴ落ちしづらいのもメリットと言えます。

他の方法として、加盟店のページに決済サービスのプロバイダが提供するモジュールを埋め込むモジュール型があります。これはカード情報を保存はしませんが通過はするので、バックドアが仕掛けられるとアウトという弱点があります。よって、この方式は非保持とは見なされていません。

PCI DSS準拠

非保持化ではなくPCI DSSに準拠したサービスとしていく場合、その要件に準拠していることをオンサイトレビューなどで示さなければなりません。この作業は件数が多く、負担も大きいので、まずは該当加盟店のカード情報取扱の状況に適した自己問診票 (SAQ) を選択することで、準拠するための項目を減らすことができます。例えば、EC加盟店でリダイレクト決済を使っている場合は、カード情報送受信に関する要件などは省略された要件の少ない問診票 (SQA A)が利用できる、などです。

加盟店用のフルセットのSAQであるSAQ-D加盟店だと331項目ありますが、これはけっこう大変そうです…

その他

2019年からは3Dセキュアの2.0が登場し、ワンタイムパスワードや、怪しい取引に限って3Dセキュアのパスワード入力を求めるリスクベース認証が導入されるようです。

まとめ

PCI DSSとは別に、国内では「実行計画」が存在し、それが非保持化もしくはPCI DSSへの準拠を求めていることがわかりました。また、PCI DSSへの対応の際には自己問診票の活用が大事そうということがわかりました。

全体的に説明が詳しく、EC加盟店の他に通販や対面の加盟店での対応方法も書いてあり、最近のクレジットカード事情がわかって便利かと思うので、興味のある人は読んでみてください。