幼女の技術ブログ

こちらでは技術ネタをつらつらと

はげったーを支える技術(ユーザー認証とセキュリティ)

Mastodon(主にhandon.club)向けのTogetterライクなサービスとして、hagetter(はげったー)*1というサービスを開発・運用しています。個人開発ということもあり色々と新しい技術要素を取り入れながら試行錯誤しながら開発しているので、その中身について少しずつ紹介していこうと思います。

今回はユーザー認証やセキュリティについて紹介します。

f:id:osanine:20200524135638p:plain:w400
はげったートップ画面

hagetterのユーザー認証について

hagetterではユーザー認証はMastodonインスタンスOAuth認証に依存しており、hagetterとユーザー間のセッション管理にはJSON Web Token(JWT)を利用しています。また、hagetterでは個人開発ということもあり、アプリケーションがセキュリティ品質を担保出来ていない可能性があるという前提の元、アクセストークンなどの機密性の高い情報は可能な限りサーバー内部に保持しないという設計を行っています。(サーバーがハックされて利用者全員分のアクセストークンが漏れたら大問題なので)。

対応するMastodonインスタンスは以前はhandon.clubのみでしたが、最近頑張って複数インスタンスに対応し、現在はmstdn.jpでも利用可能です。(めっちゃ頑張った!)

一意なユーザー名

ユーザー認証を複数の認証プロバイダに依存しているので、複数インスタンスで一意にユーザーを特定出来る必要があります。例えば「osa9」というアカウント名だけですと、handon.clubとmstdn.jpのいずれのユーザーであるかを一意に特定することが出来ません。悪意のあるユーザーが別のインスタンスでosa9というアカウントを取ったとしてもなりすまし出来ないようにする必要があります。
また、仮にosa9のアカウントが消えた後に誰かが同名のアカウントを取り直したとしても以前のユーザーの情報を盗み見ることが出来ないような仕組みが必要になります。

つまり「一意なユーザー名」は「空間的な一意性」と「時間的な一意性」を有している必要があります。

空間的な一意性

Mastodon APIでアカウント情報を取得するとユーザー識別情報として「ID」「username」「acct」の3つを含んでいます。

Mastodonのドキュメントによるとそれぞれ下記のような定義になっています。

  • id: (Integer) The account id header.
  • username: (String) The username of the account, not including domain.
  • acct: (String) The Webfinger account URI. Equal to username for local users, or username@domain for remote users.

idはそのインスタンス内で外部のインスタンスを含む全てのユーザーに一意に割り振られたIDです。あるインスタンスの中では一意ですが、複数のインスタンスでは一意ではないです。基本的にはMastodon内部のDBの外部キーとして用いられるものでしょう。

usernameは「osa9」とかそういうやつです。osa9といっても例えばhandon.clubのosa9やmstdn.jpのosa9が居るように、インスタンス内部の扱いにおいても一意ではないです。

acctは、ローカルユーザーはusername、他インスタンスのユーザーはusername@domainという形を取ります。つまりタイムラインに表示されるユーザー名です。あるインスタンス内では一意ですが、複数のインスタンスの場合は一意にはなりません。handon.clubにとってのosa9はosa9@handon.clubであり、mstdn.jpにとってのosa9はosa9@mstdn.jpです。

つまりMastodonAPIで取得可能な情報の中には残念ながら複数インスタンスで一意なユーザー名は含まれていない、ということになります。

既に答えは出ていますが、「username@domain」とすれば複数インスタンスでも一意となります。 (http://handon.clubhttps://handon.clubhttps://handon.club:8080とかはどうなるか知らん)

hagetterの内部ではMastodon APIでユーザー情報やステータスを取得する際に、acctを上記のusername@domainに書き換える処理を行っています。(最初そのままacctを使っちゃってマルチインスタンス化の時に苦労した)

ちなみに「id@domain」はどうなのかというと、重複はないですが同一ユーザーのidがインスタンスによって異なるため不便です。(osa9@handon.clubは、handon.clubにおいてはid=55、mstdn.jpにおいてはid=512494)

時間的な一意性

こちらはIDを変更可能だったり削除されたIDを再取得可能なTwitterでよく問題になるやつですね。仮にosa9が気分でosa34にIDを変えたとしても、後からosa9を取得した人に自分のデータを盗み見られないようにしなければなりません。なので例えばTwitterサードパーティサービスはユーザー名ではなくユーザーIDでユーザーを識別する必要があります。

Mastodon上では基本的にIDは変更/削除不可能らしいのでこの辺の問題は何もせずにクリアです。やったね。

ユーザー認証とセッション管理

ユーザーがOAuth認証でhagetterに対して認可を行うと、hagetterはそのユーザー情報やタイムラインを取得するためのアクセストークンをMastodonインスタンスから取得します(OAuthの細かい話は割愛)。hagetter上でのログインセッション管理についてはJSON Web Token(JWT)を用いています。

あえて図に起こす必要もないですが、以下のような認証フローになります。

f:id:osanine:20200524160930p:plain
ログインフロー

JSON Web Token(JWT)とは

RFC7519で定められているトークンの仕様です。特徴としては認証クレーム(ざっくり言えばユーザーIDや付随情報)に暗号化(JWE)やデジタル署名(JWS)を施すことでクレームの改竄や捏造を防止していることです。(この記事内ではJWT=JWSとして扱います)

デジタル署名によって、サーバーはユーザー識別に必要な情報をJWTのセッショントークンとしてユーザーに渡すことで、DB等にセッション情報を保持しなくてもユーザーから渡されたトークンから安全に認証情報を取得出来ます。ちなみにJWTの仕組みではPayload部分は暗号化されていないので、ユーザー側からどのような情報が格納されているかを見ることが出来ます。

例えばhagetterではセッショントークンは以下のような形式になっています。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoib3NhOUBoYW5kb24uY2x1YiIsInRva2VuIjoiYWJjZGVkZjEyMzQ1Njc4OTAiLCJpYXQiOjE1OTAzMDEyNjIsImV4cCI6MTU5MDM4NzY2Mn0.nkxmAKVmRQ1OWrRCzbPH3ViIsjxFqilVHWYQqqehYVE

header(JSON)とpayload(JSON)とheader+payloadのデジタル署名の3つをそれぞれBase64urlエンコードし.(ドット)で繋げた形をしています。JWT.ioというサイトにトークンを入れてみると下記のようにデコードされることが分かります。

f:id:osanine:20200524170653p:plain
jwt.ioでhagetterのセッショントークンをデコードしてみる

ユーザー側はサーバーから渡されたトークンのpayload部分をデコードする事で、自身のユーザー名やトークンの有効期限(exp)を知ることが出来ます。サーバーは内部にトークンを保持していなくてもユーザーから渡されたトークンの署名を検証する事で確かに自身が発行したトークンであることを確認出来ます。便利。

ただし、署名しているとはいえユーザーの入力を完全に信用することになるので、いくつか注意点があります。例えばサーバー側でセッション情報を保有していないので、万が一トークンが漏れた場合に失効処理が出来ません(失効情報をDB内に保管すれば出来るけど本末転倒感)。また、JWTには署名を付与しないnoneというタイプがあるのですが、これを無条件に受け入れてしまうと改竄されたトークンで認証機構を通過してしまう可能性があります(JWTはアルゴリズム種別すらユーザーに持たせている!)。

この辺は認証サービスであるAuth0が頑張ってライブラリを作っているので、そのライブラリを使えばほとんどの危険性は回避可能です(感謝)。

注意点については詳しくは以下のブログが参考になるかと思います。

auth0.com

hagetterでのJWTの活用

hagetterではユーザーの認証情報やアクセストークンを可能な限りサーバー内に保持しない運用を行っています。そのためWebサイトへの攻撃者だけでなく管理者もアクセストークンを用いてユーザーのタイムラインを不正に覗き見出来ない安全設計になっています。

ではどこにMastodonから取得したアクセストークンを保持しているのかというと、ここでJWTの仕組みを利用しています。ユーザー認証の際にアクセストークンを暗号化しJWTクレームの中に含め、ユーザーから認証が必要なURLへのアクセスがあった時のみJWTトークンの中からアクセストークンを復号してMastodonに投げています。大まかな流れは以下の通りです。

f:id:osanine:20200524163534p:plain
JWTのフロー

まあJWTトークンを保有しているのはユーザー自身なので、アクセストークンを生で持っても良いといえば良いのですが、アクセストークンは有効期限が長いので万が一アクセストークンが漏れた際にも悪用が出来ないよう考慮して暗号化しています。hagetterのセッション情報は現在は24時間で有効期限が切れるようになっています。なんとセッションの有効期限もJWT内部に含まれています(Payloadのexp)。

データセキュリティ

ちなみに、まとめ作成の部分でも暗号技術が活用されています。

まとめ作成の際にユーザーリクエストに応じてhagetterがMastodon APIからそのユーザーのタイムラインを取得してユーザーに返答します(ここでJWTとアクセストークンの暗号化が利用されています)。ちなみにサーバーは取得したタイムラインを保持していません。これは保持する処理を書くのがめんd…DBにタイムラインを保存するとまとめに使われなかったタイムライン情報を後から消す必要がありますし、悪意を持ったユーザーに何らかの形で盗み見るされる可能性もあるのでセキュリティリスクが高まるという配慮です。(togetterとかはどうやってるんだろう?)

f:id:osanine:20200524185355p:plain
まとめ作成画面

ユーザーは取得したタイムラインをこねくり回してまとめを投稿するわけですが、タイムライン情報はユーザーに渡してサーバー内部で保持していないため、悪意のあるユーザーにタイムラインを勝手に書き換えられて投稿される可能性があります。hagetterではタイムラインの捏造防止のために、ユーザーにタイラインを渡す際に暗号化したToot(Status)を付加し、投稿時にはこの情報を用いて元のTootを復元しています。

f:id:osanine:20200524200517p:plain
フロー

これによってセキュリティと安全性と利便性を両立しています。

セキュリティ設計全般に言えることですが、ユーザーはあらゆる不正を働くものとしてデータ設計を行う必要があります。

まとめ

hagetterのセキュリティは暗号技術の進歩に支えられています。

(もしhagetterにセキュリティホールを見付けたらこっそり教えてください)

🌼🌼🌼 おまけ 🌼🌼🌼

f:id:osanine:20200524190208p:plainf:id:osanine:20200524182953p:plain
実は同じなんです、知っていました?

*1:handon.club版togetterなのでhagetter