なぜ共通化のために継承を使うべきではないかをできるだけ簡単に説明したい4J

多重継承できない言語でなるべく誰でもわかるように共通化のために継承を使うべきではない理由をまとめた。 継承を使うべきでない理由は他にもあるがとりあえず一番簡単に納得できそうな理由について記載した。 多重継承できたとしても、共通化のために継承を使うのは個人的にあまり良くないと思っているが、それについては記載しない。

前提

ある程度の規模感があるJavaのコードは責務ごとにクラスを分割するとコードが読みやすくなる(≒全部mainメソッドに書いたコードは読みづらい)。

そもそもコンストラクタでバリデーションしろよ、とかは無視してもとりあえず言いたいことは伝わる気がするので諦めた。

多重継承できる言語だとこの論理では継承よりもコンポジションを主張できない。 リスコフの置換原則の話とかはしない。

まとめ

Javaで継承による共通化を行うと、多重継承ができないため1つの親クラスに全ての共通処理を入れる必要がある。 つまり処理ごとにクラスを分割することができないし、クラスに適切な名前をつけることもできない(なので多くの場合Baseなんとかクラスになってしまう)。

これを避けるために無理やりクラスを分割すると、全く関連の無いクラス同士を親子関係にする必要が出てくる。 こうなると、あるクラスに手を入れる際に関係ないクラスを破壊していないか意識する必要が出てきて辛い。 関係ないクラス同士に親子関係があると、あとから見たときに混乱するので辛い。

以下のようなコードがあったとする。

public class UserCreator {
    public User createUser(User user) {
        if (user.id != null) {
            throw new IllegalArgumentException("ユーザーIDは必須です。");
        }
        if (user.age < 0) {
            throw new IllegalArgumentException("年齢は0以上である必要があります。");
        }

        // 永続化
        return db.create(user);
    }
}

public class UserUpdater {
    public User updateUser(User user) {
        if (user.id != null) {
            throw new IllegalArgumentException("ユーザーIDは必須です。");
        }
        if (user.age < 0) {
            throw new IllegalArgumentException("年齢は0以上である必要があります。");
        }

        // 永続化
        return db.update(user);
    }    
}

ユーザーを永続化する際にバリデーションを行っているが、このバリデーションはCreaterとUpdaterで重複している。 これを継承で共通化すると以下のようになる。

public abstract class UserPersistentBase {
    public void validateUser(User user) {
        if (user.id != null) {
            throw new IllegalArgumentException("ユーザーIDは必須です。");
        }
        if (user.age < 0) {
            throw new IllegalArgumentException("年齢は0以上である必要があります。");
        }
    }
}

public class UserCreator extends UserPersistentBase {
    public User createUser(User user) {
        validateUser(user);
        return repository.create(user);
    }
}

public class UserUpdater extends UserPersistentBase {
    public User updateUser(User user) {
        validateUser(user);
        return repository.update(user);
    }
}

validateUserがUserPersistentBaseに共通化され、UserCreator、UserUpdaterがすっきりした。 次にユーザーが作成されたり更新した際に、WebhookでHTTPリクエストが飛ぶ、という要件が追加されたとする。

public abstract class UserPersistentBase {
    public void validateUser(User user) {
        if (user.id != null) {
            throw new IllegalArgumentException("ユーザーIDは必須です。");
        }
        if (user.age < 0) {
            throw new IllegalArgumentException("年齢は0以上である必要があります。");
        }
    }
}

public class UserCreator extends UserPersistentBase {
    public User createUser(User user) {
        validateUser(user);
        try {
            HttpClient client = HttpClientFactory.create();
            UserChangeEvent event = new UserChangeEvent(user, WebHookType.CREATE_USER);
            client.post(System.getProperty("WEBHOOK_URL"), event.toString());
        } catch (HttpRequestFailureException e) {
            // webhookのリクエストエラーの場合は通知のみで処理をすすめる
            log.error("failed webhook request", event);
        }
        return repository.create(user);
    }
}

public class UserUpdater extends UserPersistentBase {
    public User updateUser(User user) {
        validateUser(user);
        try {
            HttpClient client = HttpClientFactory.create();
            UserChangeEvent event = new UserChangeEvent(user, WebHookType.UPDATE_USER);
            client.post(System.getProperty("WEBHOOK_URL"), event.toString());
        } catch (HttpRequestFailureException e) {
            // webhookのリクエストエラーの場合は通知のみで処理をすすめる
            log.error("failed webhook request", event);
        }
        return repository.update(user);
    }
}

Webhookのリクエストを行うコードが重複しているので共通化する。

public abstract class UserPersistentBase {
    public void validateUser(User user) {
        if (user.id != null) {
            throw new IllegalArgumentException("ユーザーIDは必須です。");
        }
        if (user.age < 0) {
            throw new IllegalArgumentException("年齢は0以上である必要があります。");
        }
    }

    public void requestWebhook(String message) {
        try {
            HttpClient client = HttpClientFactory.create();
            client.post(System.getProperty("WEBHOOK_URL"), message);
        } catch (HttpRequestFailureException e) {
            // webhookのリクエストエラーの場合は通知のみで処理をすすめる
            log.error("failed webhook request", event);
        }
    }
}

public class UserCreator extends UserPersistentBase {
    public User createUser(User user) {
        validateUser(user);
        requestWebhook(new UserChangeEvent(user, WebHookType.CREATE_USER).toString());
        return repository.create(user);
    }
}

public class UserUpdater extends UserPersistentBase {
    public User updateUser(User user) {
        validateUser(user);
        requestWebhook(new UserChangeEvent(user, WebHookType.UPDATE_USER).toString());
        return repository.update(user);
    }
}

UserPersistentBaseにユーザー永続化時のバリデーションとWebhookの送信処理が追加された。 UserだけではなくItemの作成クラスについても考えてみる。この場合も同様にWebhookの送信が必要である。

public class ItemCreator {
    public Item createItem(Item item) {
        try {
            HttpClient client = HttpClientFactory.create();
            client.post(System.getProperty("WEBHOOK_URL"), new ItemChangeEvent(item, WebHookType.CREATE_ITEM).toString());
        } catch (HttpRequestFailureException e) {
            // webhookのリクエストエラーの場合は通知のみで処理をすすめる
            log.error("failed webhook request", event);
        }        
        return repository.create(item);
    }
}

Webhookの送信処理はユーザーの場合と同様であり、共通化できそうだがそのまま共通化するとこうなる。

public class ItemCreator extends UserPersistentBase {
    public Item createItem(Item item) {
        requestWebhook(new ItemChangeEvent(item, WebHookType.CREATE_ITEM).toString());
        return repository.create(item);
    }
}

UserPersistentBaseを継承したItemCreatorが出来てしまった。 さらにItemについても更新処理とバリデーションを追加する。

public class ItemCreator extends UserPersistentBase {
    public Item createItem(Item item) {
        validateItem(item);
        requestWebhook(new ItemChangeEvent(item, WebHookType.CREATE_ITEM).toString());
        return repository.create(item);
    }
}

public class ItemUpdater extends UserPersistentBase {
    public Item createItem(Item item) {
        validateItem(item);
        requestWebhook(new ItemChangeEvent(item, WebHookType.CREATE_ITEM).toString());
        return repository.create(item);
    }
}

UserPersistentBaseには現在以下の処理がある。

  • ユーザーのバリデーション
  • アイテムのバリデーション
  • Webhookの送信

これはもはやUserPersistentBaseという名前にマッチしない。 PersistentBaseに変更しても良いが今後永続化するリソースが増えたり共通化したい処理が増えるたびにPersistentBaseに処理が追加され続けると、PersistentBaseが肥大化する。 この状況はもはや責務ごとに処理が分割されているとは言いがたい。

責務ごとにクラスを分けつつ継承で共通化した場合、以下のようになる。

public abstract class UserValidator extends WebhookRequester {
    public void validateUser(User user) {
    }
}

public abstract class ItemValidator extends WebhookRequester {
    public void validateUser(User user) {
    }
}

public abstract class WebhookRequester {
    public void requestWebhook(String message) {
    }
}

public class UserCreator extends UserValidator {
    public User createUser(User user) {
        validateUser(user);
        requestWebhook(new UserChangeEvent(user, WebHookType.CREATE_USER).toString());
        return repository.create(user);
    }
}

public class UserUpdater extends UserValidator {
    public User updateUser(User user) {
        validateUser(user);
        requestWebhook(new UserChangeEvent(user, WebHookType.UPDATE_USER).toString());
        return repository.update(user);
    }
}

public class ItemCreator extends ItemValidator {
    public Item createItem(Item item) {
        validateItem(item);
        requestWebhook(new ItemChangeEvent(item, WebHookType.CREATE_ITEM).toString());
        return repository.create(item);
    }
}

public class ItemUpdater extends ItemValidator {
    public Item createItem(Item item) {
        validateItem(item);
        requestWebhook(new ItemChangeEvent(item, WebHookType.CREATE_ITEM).toString());
        return repository.create(item);
    }
}

UserPersistentBaseがUserValidator、ItemValidator、WebhookRequesterに分割された。 コードは確かに分割されたが、UserValidatorにはWebhookRequesterから継承されたrequestWebhookが生えている。

UserValidatorはバリデーションを行うためのクラスだが、バリデーション以外のことをしている。名前と責務が一致していない。

しかも、UserValidatorはWebhookRequesterに依存したり、overrideによってWebhookRequesterの処理に影響を与える事ができる。 例えば、WebhookRequesterの修正によってバリデーションを壊したり、UserValidatorの修正によってWebhookの処理を壊したりする可能性がでてくる。 つまりそれぞれの処理に手を入れる際、親クラスや子クラスを気にしながら手を入れる必要がある。

さらにこの構造では、共通処理が増えるたびに継承の階層が深くなり、何が何を継承しているのかを判断するのが難しくなってくる。 この階層はJavaの制約を満たすためだけに存在する階層で本質的には不要であり、複雑さを増している。

継承をやめてコンポジションで共通化を行うと以下のようになる。

public class UserValidator {
    public void validateUser(User user) {
    }
}

public class ItemValidator {
    public void validateItem(Item item) {
    }
}

public class WebhookRequester {
    public void requestWebhook(String message) {
    }
}

public class UserCreator {
    UserValidator validator = new UserValidator();
    WebhookRequester webhookRequester = new WebhookRequester();
    public User createUser(User user) {
        validator.validateUser(user);
        webhookRequester.requestWebhook(new UserChangeEvent(user, WebHookType.CREATE_USER).toString());
        return repository.create(user);
    }
}

public class UserUpdater {
    UserValidator validator = new UserValidator();
    WebhookRequester webhookRequester = new WebhookRequester();
    public User updateUser(User user) {
        validator.validateUser(user);
        webhookRequester.requestWebhook(new UserChangeEvent(user, WebHookType.UPDATE_USER).toString());
        return repository.update(user);
    }
}

public class ItemCreator {
    ItemValidator validator = new ItemValidator();
    WebhookRequester webhookRequester = new WebhookRequester();
    public Item createItem(Item item) {
        validator.validateItem(item);
        webhookRequester.requestWebhook(new ItemChangeEvent(item, WebHookType.CREATE_ITEM).toString());
        return repository.create(item);
    }
}

public class ItemUpdater {
    ItemValidator validator = new ItemValidator();
    WebhookRequester webhookRequester = new WebhookRequester();
    public Item createItem(Item item) {
        validator.validateItem(item);
        webhookRequester.requestWebhook(new ItemChangeEvent(item, WebHookType.CREATE_ITEM).toString());
        return repository.create(item);
    }
}

UserValidator、ItemValidator、WebhookRequesterは完全に独立していて各クラスの名前と処理の剥離も無い。 完全に独立しているため、WebhookRequesterに手を入れる時はWebhookRequesterのことだけを意識すればよい。

上記のような状態になってしまうことを避けたいという点だけでも継承よりコンポジションを選べる状況なのであれば、コンポジションを選ぶべきだと思う。

継承はいつ使えばいいのか

通化ではなく拡張のために使うものだと思っている。思っています。

まとめ

Javaで継承による共通化を行うと、多重継承ができないため1つの親クラスに全ての共通処理を入れる必要がある。 つまり処理ごとにクラスを分割することができないし、クラスに適切な名前をつけることもできない(なので多くの場合Baseなんとかクラスになってしまう)。

これを避けるために無理やりクラスを分割すると、全く関連の無いクラス同士を親子関係にする必要が出てくる。 こうなると、あるクラスに手を入れる際に関係ないクラスを破壊していないか意識する必要が出てきて辛い。 関係ないクラス同士に親子関係があると、あとから見たときに混乱するので辛い。

Webhookまわりのコード、何?

わかんない

2020年11月にやったこと

このまとめに書いてあるこれからやること以外のことばかりしている

【増補改訂】 財務3表一体理解法

PLとかBSとかなんもわからんという話をしたら会社の人に教えてもらった

これは大変によかった

この辺なんもわからんのをなんとかしようと思って最初簿記の本読んでたんだけど、なんとなく財務諸表の作り方はわかるけどなんでこういう構造になってるのかさっぱりわからん状態になったがそのあたりの疑問がかなり解消された

個人的にこの本は複式簿記の勉強を始めるよりも前に読むとその後の学習がかなり効率よくなる気がする 表をいったりきたりしながら読むのがちょっとつらいので前ページの表と比べなくても差分がわかる構成になっているともっと読みやすい気がした 電書だとそのへんが特に辛いかも でもこれはよかった

Javaパフォーマンス

GCなんもわからんやばいという話をしたら会社の人に教えてもらった

JPAあたりからなんとなく興味がどっかいってしまったがGCの下りはわかりやすくてよかった

わーっと読んだだけで全然触ってないので触る必要がある

その他

クソアプリアドベントカレンダー用にクソアプリを書いたりしていた

これからやること

  • AWS入門する
    • ECS
  • Webエンジニアが知っておきたいインフラの基
  • 入門監視
  • クリーンアーキテクチャ
  • 実践ハイパフォーマンスMySQL 第3版
  • プログラミングコンテスト攻略のためのアルゴリズムとデータ構造
    • 停滞気味
  • Linuxの仕組み
  • Goならわかるシステムプログラミング
  • 英語
  • 中国語
    • Toeic900超えるまで止める予定
  • ここ最近ほとんど業務でプログラミングをしなくなって、調整とかレビューとかばっかしてるがかなり雰囲気でやってるので、マネジメント系の本を読もうとしている
    • なにがいいのかよくわからん
  • Effective Go - The Go Programming Language
  • パタヘネ
  • OAuth2.0のクライアント書く
  • OAuth 2.0/OIDC関連仕様全部読む
  • WebAuthnのドキュメント読む
  • マイクロサービスパターン
  • コンピュータシステムの理論と実装 ―モダンなコンピュータの作り方
  • DBS

2020年9月、10月にやったこと

資格関連ばっかやってた あとは社内で全員 OAuth 2.0 ちゃんと理解しよう勉強会をやってた

ウェブ・セキュリティ基礎試験(徳丸基礎試験)

とった inabajunmr.hatenablog.com

AWS Certified Security Specialty

とった inabajunmr.hatenablog.com 1ヶ月でいけるだろと思って予約したら割とギリギリなかんじでこればっかりになってしまった

Javaパフォーマンス

150ページくらいまでよんだ 読み始めてから資格のほうが以外とやばいことに気づいてそっちにシフトしたので停滞していた GCとかJVMの話面白いのでもうちょっと掘り下げたい気持ちがでてきた とりあえず最後まで読む

英語

9月はシャドーイングしたり単語力の衰えを感じたので

www.amazon.co.jp

をやったりしていた 10月は停滞していた

その他

社内で参加者 OAuth 2.0 全員理解するぞ勉強会をやった

dev.classmethod.jp

参加者に質問してもらったやつでなんでだっけ?ってなったのがあったので整理した

inabajunmr.hatenablog.com

あとはなんかポエムかいてた

inabajunmr.hatenablog.com

inabajunmr.hatenablog.com

これからやること

  • AWS入門する
    • ECS
  • Webエンジニアが知っておきたいインフラの基
  • 入門監視
  • クリーンアーキテクチャ
  • Javaパフォーマンス
  • 実践ハイパフォーマンスMySQL 第3版
  • プログラミングコンテスト攻略のためのアルゴリズムとデータ構造
    • 停滞気味
  • Linuxの仕組み
  • Goならわかるシステムプログラミング
  • 英語
  • 中国語
    • Toeic900超えるまで止める予定
  • ここ最近ほとんど業務でプログラミングをしなくなって、調整とかレビューとかばっかしてるがかなり雰囲気でやってるので、マネジメント系の本を読もうとしている
    • なにがいいのかよくわからん
  • Effective Go - The Go Programming Language
  • パタヘネ
  • OAuth2.0のクライアント書く
  • OAuth 2.0/OIDC関連仕様全部読む
  • WebAuthnのドキュメント読む
  • マイクロサービスパターン
  • 財務3表立体理解法

既にある、という理由で既にあるコードを信じないほうがいい

息の長いプロダクトのコードを見ていると、「なぜか本番にリリースされていない機能」とか「誰もしらない機能」のコードを見つけることがある。 開発途中で機能が不要になったり、一度リリースした機能を潰したり、といった事情は色々あるがとにかくこのようなコードはたまに見つかる。 さらに退職などの理由でこのようなコードについての背景が誰にもわからなくなっていることもある。

そして、その機能がやはり欲しくなったり、コードを部分的に流用したい、といった場合に、このコードをそのまま使いたくなることがある。

この時、このコードを「既にあるから」という理由で信用したくなる時がある。 これはググったら出てきたコードをよくわからないままコピペしたくなる感覚と似ている気がする。 しかし、コピペはNG、という価値観を持っていても既にあるコードはOK、と判断してしまうことがある気がしている。

「既にあるから」という理由でそのコードを信じてはいけない。 コードの背景がわからないということは、そのコードの品質が担保されているのかわからない、ということである。

品質はテストや本番での動作実績に担保されるが、「既にあること」は品質を担保しない。 プロダクトの品質を担保する基準があるのであれば、「既にあるコード」を利用する場合もそのコードが基準を満たしているか確認する必要がある。 満たしていないのであればテストを追加するなどして、基準を満たす必要がある。 ただし、基準を満たしていて工数の削減につながるのであれば、コードの流用はどんどんするべきだと思う。

AWS Certified Security Specialtyをとったので勉強内容をまとめた

f:id:inabajunmr:20201023002915p:plain

ついで f:id:inabajunmr:20201023002919p:plain

前提知識

コードを書く人的なロールでAWSは触っている が、日常的には触るのはSNS、SQS、DynamoDBくらい とはいえメジャーな?サービスはどういうものなのか?くらいは理解している

後はあそびでEC2立てたりECSタスクかいてアプリをデプロイしたりはする

2017年に以下を取得しているが、ろくにAWS触ってない状態で知識だけ詰め込んだので今受けたらアソシエイトも多分おちる

  • AWS Certified DevOps Engineer - Professional
  • AWS Certified Solutions Architect - Professional

セキュリティ系のサービスはIAMはなんとなくわかるけどほかは全然わからん、くらいの感じ

勉強に使った時間

40時間くらい

やったこと

いろんな合格記で紹介されてるやつとだいたい同じような感じ 流れ的には「資格本1冊読む」→「公式の模試」→「公式のトレーニング(Exam Readiness)」→「BlackBelt読みながらちょくちょくガイドみる」→「公式の模試」→「受験」

ホワイトペーパーは一切読んでない 読んでないので良し悪しがわからん

主にBlackBeltとガイド ガイドは網羅的によむというよりもBlackBelt見てて気になったところを見ていった

実際に動かしてどうこうもしてない(すべき)が、Trusted Advisorのチェック項目はマネコンで眺めたりしていた

要点整理から攻略する『AWS認定 セキュリティ-専門知識』

何したらいいのかわからんのでとりあえず買って読んだ セキュリティ系のサービスそもそもなんなのみたいのがわかる これだけで受かるかというと多分受からない

ADの説明がわかりやすいのと回答付き模擬試験がまあまあなボリュームであるのは嬉しい あとは何の勉強したらいいのリストを作るのに使える が、今考えると別に読まなくてもよかった感はある 回答付きの模擬試験を解きたい人は買おう

Exam Readiness: AWS Certified Security - Specialty

なぜ本を読まなくてもいいかというとこのトレーニングでよさそうだったから 何の勉強したらいいのリストを作るのに使える それ以外はまあなんかいいかなという感じのやつ 回答のコツ、みたいのも紹介されてた気がする

BlackBeltとかガイドとか

Exam Readinessで紹介されてたサービスのBlackBeltを片っ端から読むのが勉強のメイン 実際に読んだやつの一覧は以下 なんか漏れてるかもしれんけどだいたいこれで全部だと思う

時間なくてやろうと思ってたけどスルーしたサービスたち

特にCloudWatchとSecretManagerはスルーしないほうがよさそう スルーした

  • CloudWatch
  • DynamoDB
  • Certificate Manager
  • CFn
  • Shield
  • CloudFront
  • RDS
  • CloudHSM
  • SecretManager
  • Route53
  • SSO
  • Cognito
  • Athena

模試

公式の模試を2回やった 初っ端いきなりやって50パーくらいで次に試験前日にやったら75パーだった かなりギリギリ感 本番のスコアは見方がよくわからないので何パーだかわからん

ふりかえり

いきなりBlackBelt、はそこまで間違ってた感じではないけど動画だと時間かかるので、公式のガイド見てピンとこないやつだけBlackBelt、のが効率いい気がした

ガイドのがわかりやすかったり網羅的だったりおもしろかったりするので ガイド読まなすぎてかなりギリギリだった感ある というかそもそも明らかに試験にでそうなサービスを見切れてないのがまずかった

あとおもしろはんぶんでプラクティショナーも同日にうけた こっちは準備してなくても受かったけどぜんぜん知らん範囲の問題が多かった

とってみてどうだった

AWSのセキュリティにくわしくなったかもしれない

どうして認可コードフローのトークンエンドポイントは redirect_uri を要求するのか

OIDC で redirect_uri の登録と完全一致が必須だという前提で物事を考えていたため、どういう攻撃が成立するのかわからず混乱した。 微妙に確信が持てないので間違ってたらおしえてください。

認可コードフローおさらい

  1. 認可リクエストを送る
    • redirect_uri
    • client_id
    • その他
  2. コールバックで認可コードが redirect_uri に送られてくる
  3. コールバックを受け取ったクライアントはトークンエンドポイントに認可コードを送って、アクセストークンを得る
    • クライアントの認証がある
    • このときに認可リクエストで送信した redirect_uri も送る これなんで必要なのかの話
      • 完全一致してる必要がある

RFC での言及

10.6. 認可コードリダイレクトURIの操作より引用。

認可要求に認可コードグラントタイプを用いるとき, クライアントは redirect_uri パラメーターによりリダイレクトURIを指定できる. 攻撃者がリダイレクトURIの値を操作可能であるとき, 認可サーバーによってリソースオーナーのユーザーエージェントを攻撃者の管理下にあるURIに認可コードを含んだ状態でリダイレクトさせることができる.

攻撃者は, 正しいクライアントにおいてアカウントを作成し, 認可フローを開始することができる. 攻撃者のユーザーエージェントがアクセス許可のために認可サーバーに送られたとき, 攻撃者は正当なクライアントにより提供された認可URIを取得し, クライアントのリダイレクトURIを攻撃者の管理下にあるURIに交換する. 攻撃者はその後, 正当なクライアントに向けた操作された認可アクセスリンクをたどるよう被害者を騙す.

認可サーバーにおいて, 被害者は正当で信頼できるクライアントによる正常で有効なリクエストを促され, そのリクエストを認可する. 被害者はその後, 認可コードとともに攻撃者の管理下にあるエンドポイントにリダイレクトされる. 攻撃者は, クライアントにより提供されたオリジナルのリダイレクトURIを用いて, 認可コードを送ることにより認可フローを完了する. クライアントは認可コードとアクセストークンを交換し, それを攻撃者のクライアントのアカウントと紐づけることで, 被害者により (クライアント経由で) 認可された保護されたリソースへのアクセス権を獲得できる.

このような攻撃を防ぐため, 認可サーバーは認可コードの取得に用いたリダイレクトURIと, 認可コードとアクセストークンの交換時に提供されたリダイレクトURIが同一であることを確認しなければならない (MUST). 認可サーバーはパブリッククライアントに対してリダイレクトURIの登録を要求しなければならず (MUST), コンフィデンシャルクライアントに対してもリダイレクトURIの登録を要求すべきである (SHOULD). リダイレクトURIがリクエストにより提供されたとき, 認可サーバーは登録された値を用いてそれを検証しければならない (MUST).

なんとなく理解した内容

認可リクエストの redirect_uri に攻撃者が管理しているサイトの URI を指定されたときに、認可コードが攻撃者のサイトに飛ぶ。 その認可コードを正規のクライアントにコールバックとして送信すると他人のアクセストークンを保持したクライアントを操作できる。

詳細

認可リクエストに以下のようなパラメーターを指定して、被害者に踏ませる。

client_id=攻撃対象のクライアント&redirect_uri=https://攻撃者のドメイン.example.com

認可コードが https://攻撃者のドメイン.example.com に送られるので、攻撃者は被害者が認可を行った認可コードを手に入れる。

攻撃者はこの認可コードをクライアントの本来の redirect_uri に送信する。 このときトークンエンドポイントが redirect_uri の検証を行っていない場合、クライアントが認可コードとトークンの引き換えに成功する。 認可を行ったのは攻撃者ではなく被害者なので、トークンは被害者に紐付いている。

この状態で攻撃者がリソースサーバーに対して自らのプロフィールを取得する API を利用するような操作を行うと、プロフィールは被害者のものとなり、攻撃者は被害者の個人情報を取得することが出来る。

redirect_uri の検証を行った場合、認可レスポンスを受け取った攻撃対象のクライアントは、トークンエンドポイントに https://攻撃者のドメイン.example.com ではなく https://攻撃対象のドメイン.example.com を送信する。(そもそも攻撃対象のクライアントは攻撃者のredirect_uri を知らない)

こうなると認可リクエストで送信した redirect_uriトークンエンドポイントに送った redirect_uri が別の値になるので、攻撃が失敗する。 ようするに認可リクエストを行ったときに指定した redirect_uri にちゃんとコールバックで認可コードが渡って、その redirect_uri のクライアント自身がトークンリクエストをしているよね?というところが担保できる。

シーケンス

攻撃者が被害者の認可コードを取得

f:id:inabajunmr:20201009233637p:plain

認可リクエストの client_id は正規のクライアントとなる。

攻撃者が被害者に紐付いたアクセストークンを利用

f:id:inabajunmr:20201009233646p:plain

認可コードをアクセストークンに引き換えには

  • クライアント認証
  • 認可コードを発行したクライアントからのトークンリクエス

が必要だが、認可リクエストの client_id が正規のクライアントのものなのでどちらの条件も満たしている。

redirect_uri の検証を行う場合

f:id:inabajunmr:20201009234043p:plain

トークンエンドポイントで redirect_uri の検証をしている場合、認可コードを送信した redirect_uriトークンリクエストを送ってきたクライアントが想定している redirect_uri が異なるので、攻撃を防ぐことが出来る。

RFC6749 ではコンフィデンシャルクライアントの redirect_uri の事前登録を MUSTとしていない

3.1.2.2. 登録要件より引用。

認可サーバーは, 以下のようなクライアントに対してリダイレクトエンドポイントの事前登録を要求すること (MUST): パブリッククライアント. インプリシットグラントタイプを利用するコンフィデンシャルクライアント. 認可サーバーは, すべてのクライアントに対して, 認可エンドポイントアクセス前にリダイレクトURIの事前登録を要求すべきである (SHOULD).

この攻撃は認可コードグラントのみを使うコンフィデンシャルクライアントに対しても有効だが、その場合の redirect_uri の事前登録は MUST ではない。 また、マッチングの条件については以下の記述がある。

認可サーバーは, クライアントに (一部分ではなく) 完全なリダイレクトURIの登録を要求するべきである (SHOULD). (クライアントはリクエスト毎のカスタマイズするために state リクエストパラメーターを使用できる (MAY)) 完全なリダイレクトURIの登録を要求することができない場合, 認可サーバーはURIスキーム, オーソリティー, パス (認可要求の時, リダイレクトURIのクエリーコンポーネントのみ動的に変更を許可する) の登録を要求するべきである (SHOULD).

この SHOULD を守っていない場合、パスに指定した値によってオープンリダイレクタが可能になっていると、redirect_uri を事前登録していてもこの攻撃は成立する気がする。

参考

シーケンス書いてから見直したらまるっきり同じ話だった。

OAuth 2.0 の code は漏れても大丈夫ってホント!? - OAuth.jp