エムオーテックス株式会社が運営するテックブログです。

CognitoのOAuth2.0フロー中にある「ホストされた UI」からパスワード変更をして欲しくないので、OAuth2.0を自作する

こんにちは、外部連携チームの北岸です。

Amazon Cognito を使って、OAuth2.0(RFC 6749)をお試しで作ったのですが、Cognito のデフォルト仕様とマッチしない所がありました。
それらに触れながらどの様に作ったのかご紹介をさせて頂きます。

はじめに

はじめに、この記事を読むに当たって前提となる「Amazon Cognito」と「OAuth2.0」の知識について触れていきます。
※ ご存知の方は読み飛ばしてください。

Amazon Cognito について

正式な仕様は公式ドキュメントご参照いただきたいのですが、
ここでは「ウェブ/モバイルのアプリケーションに認可機能を提供するサービス」の認識を持っていただければと思います。

AWSを使ってウェブアプリやサービスを作成した場合、
その作成したアプリやサービスの認証/認可に Amazon Cognito を使用することができます。
そうすると、認証/認可を1から全部作る必要がなくなります。
※ 認証/認可するのはAWSアカウントではなく、自作サービスのアカウントです。

Amazon Cognito(以下、Cognito)は様々な認可規格に対応しています。
その中に OAuth2.0 に対応した物もあります。
この OAuth2.0 を自作システムへ組み込む際、Cognito が標準で提供する機能とは異なる要件を満たしたいケースもあるのではと思います。
今回は、Cognito 標準の OAuth2.0 をそのまま使用するのではなく、要件に合わせて Cognito を使っていきます。

OAuth2.0 について

こちらも正式な仕様は OAuth2.0 の公式ドキュメントご参照いただきたいのですが、
ここでは「人ではなく、事前に認めたアプリが安全に token を発行するためのフレームワーク(ルールやプロトコル)」と想定いただければと思います。

OAuth2.0 は、 WebAPI の認可で見かけることが多いです。
WebAPI の用途は様々ですが、自動化などは代表例に挙げられますね。
ツイッターの bot による自動ツイートなど、ツイッターの WebAPI を使って実装している人も多いのではないでしょうか。
この場合、 bot や tool が WebAPI を使用しますが、常に人が認証(ログイン)すれば自動とは言えません。
なので、自動化するには人間(ユーザー)ではなく物(bot / tool /アプリ等)が安全に認可されて WebAPI を使用する必要があります。
その安全に認可するための手法の1つが OAuth2.0 となります。

何がミスマッチだったの?

ここからは、Cognito 標準部分では要件にマッチしなかった部分について書いていこうと思うのですが、
タイトルにも既に書いていますので結論から先に書かせていただきます。

「Cognitoのホストされた UI にある認証画面でパスワードの変更をして欲しくない」ことが要件とマッチしなかった部分になります

↓これが当時の「認証画面」です

認証画面

要件の背景とミスマッチの理由について

要件の背景やミスマッチの理由などについて書かせていただきます。

元々 Cognito とは別に、自作した認証システムがありまして、
今回は「基本的な情報は自前認証システムで持ちつつも、一部の認証/認可を Cognito に移せないか?」というのが背景及び要件となります。
「認証基盤のマイグレーション」等をシチュエーションとしてイメージしてもらえると良いかもしれません。
一度には移せないので、新旧のシステムがそれぞれ稼働しているような状態でしょうか。
今回の「一部の認証/認可」は OAuth2.0 が対象となります。

パスワード変更の処理は自前認証システムが担当しているので Cognito からのパスワード変更は出来ない様にする必要がありました。

Cognito の設定である程度の画面カスタマイズは可能なのですが、私が調べた限り要件を満たせそうなカスタマイズはありませんでした。
また、AWS ソリューションアーキテクトの方にも相談したのですが標準機能の範囲内では難しいようです。

Cognito が用意する Oauth2.0 の認可フローは、この認証画面を使用する事が前提となっているようなので、
必然的に Cognito 標準の認可フローも要件から外れてしまいます。

ならば OAuth2.0 の認可フローごと、作ってみましょう!

開発について

と、いうことなので Cognito を使った OAuth2.0 にある、「認可コードフロー」と「リフレッシュトークンフロー」を作りました。

全ての仕様を説明するのは分量的にも難しいので、今回は構成を中心に説明させていただきます。

認可コードフロー

上記画像が作成した認可コードフローの簡単な構成図になります。
リフレッシュトークンフローの構成は、トークンエンドポイントからの構成と同じなので割愛させていただきます。
基本的には WebAPI のシステムなので、APIGateway -> Lambda -> Cognito でシンプルに作成出来たかと思っています。
ここからは機能ごとに章を区切って重要だと思われる部分の説明をさせていただきます。

クライアント管理について

クライアントの管理は、Cognito のアプリクライアントで行っています。

クライアント管理一部抜粋

「設定画面」で事前にクライアントの登録/設定(Cognito のアプリクライアント作成/設定)を行い、
利用するアプリの登録(リダイレクトURLの登録)やスコープの設定(tokenの権限)を行います。

エンドポイントについて

エンドポイント一部抜粋

赤字で書いているエンドポイント、
/oauth/authorize」が認可コードフローを始める為のエンドポイントで、
/oauth/token」はアクセストークンを発行するエンドポイントです。
この2つが認可コードフローで必須となるエンドポイントとなります。

黒字の「/oauth/login」、「/oauth/confirm」は認証画面の操作用エンドポイントになります。
本人確認用のログイン画面と、スコープ確認用のコンセント画面の操作をこのエンドポイントで行っています。
マッチしなかった認証画面の代わりがここに当たります。

認可コードについて

認可コードフローの重要な仕様の1つとして、「認可コード」があります。
これは、「アクセストークンを利用先サービス(アプリ等)で発行するのに必要なトークン(時限付き)」と思って頂いて良いです。

安全にアクセストークンを発行する仕組みの1つです。

Cognito の標準機能を使えば認可コードも付属の物を使用出来ます。
しかし、今回は自作していますので「Cognito の認可コード」はありません。
ですので認可コードも自作しています。

構成図にある、「/oauth/confirm」で認可コードを発行し、コンセント画面から HTTP のリダイレクトで利用先サービスへ送っています。

付録:OAuth2.0の理解に特に困った所

ここでは少し本題とずれますが OAuth2.0 の認可フローを実際に作る際、特に私が勘違いしやすかった所など話をして行きます。

クライアントの理解について

OAuth2.0 を開発する上でいくつか躓きやすいポイントがあります。
個人的にはクライアントに関する理解はその1つだと思います。

OAuth2.0 の中では、クライアントの記述が度々出てきます。
開発する際、私自身ここの理解によくハマりました。

クライアント(顧客)、つまり「サービスを利用するユーザー本人」の事とごっちゃになりやすいんですよね。
RFC 6749 を理解し、開発するにあたって3つのクライアントを分けて考えると理解しやすくなります。

  • クライアント(顧客):サービスを利用するユーザー本人
  • クライアント(アプリ):ユーザーが所持する、OAuth2.0 で token を取得するアプリ
    • ツイッターbot の例だと、bot がこれにあたります
    • RFC 6749 で言及されるクライアントはこれです
  • クライアント(システム):リダイレクト URL を登録し、クライアント識別子の発行やスコープ等を管理する物
    • OAuth2.0 に対応している製品内ではよく OAuth クライアント、アプリクライアント等と書かれることが多いです

ユーザー本人(クライアント(顧客))が、利用するアプリを決め(クライアント(アプリ))、事前に OAuth2.0 の利用登録(クライアント(システム))を行うので、
この3つのクライアントは役割上ほぼ同じになります(「ユーザーの代理人」の様なイメージ)。
「クライアント」と言う言葉が業界問わず便利に使われ過ぎて、巷にある OAuth2.0 のシステムと比較しながら RFC 6749 を読むとかえって混乱してしまいます。

私は最初から開発者視点で参考になるシステムと RFC 6749 を見たので、クライアント(アプリ)とクライアント(システム)を混同しながら RFC 6749 を読んでしまい、なかなか誤解が解けませんでした。

あくまでシステム設計の際ですが、RFC 6749 でクライアント関連のセクションを読む際は「クライアント(システム)」で置き換えながら読んだ方が理解が進む場合もあるかと思います。
例えば RFC 6749 の「Section 2.2 クライアント識別子」、「Section 2.3.1. クライアントパスワード」で触れられる、「client_id」、「client_secret」はクライアント(システム)単位で発行される事が多いです(Cognito のアプリクライアント等)。

もし、これから OAuth2.0 を触れる方がこのブログを読んでいただいているのであれば、
このクライアントの理解をそれぞれ分けながら読んでいくのがおすすめです。

おわりに

もしこれから OAuth2.0 を自分たちの手で作るとなった際、
上記の説明だけでは全ての仕様がクリアになるわけではないですが、
大まかな流れや構成自体は参考にしていただける部分があるのではと思います。

みなさんの一助になれば幸いです。

最後までお読みいただき、ありがとうございました。