ずいぶん前に、なんちゃってOpenID Connectサーバを自作しました。
なんちゃってOAuth2/OpenID Connectサーバを自作する
今回は、AWS Lambdaを使って実際に立ち上げてみたのち、CognitoのIDフェデレーションを使って、Cognitoから自作のOpenID ConnectにつないでID連携してみます。
ソース一式は以下に置きました。
https://github.com/poruruba/openid_server.git
AWS LambdaでOpenID Connectを立ち上げる
なんちゃってOpenID Connectの説明は、当時の投稿を見ていただくとして、手っ取り早く、Gitからソース一式をダウンロードしてください。
> git clone https://github.com/poruruba/openid_server.git
> cd openid_server
オレオレ証明書を使うので、先に、以下を実行して公開鍵ペアを作成します。
> mkdir api\controllers\oauth2\jwks
> openssl genrsa -out api\controllers\oauth2\jwks\privkey.pem 2048
注意事項があります。
生成されたprivkey.pemの改行コードはLFである必要があります。(そうしないとnpmモジュールのrsa-pem-to-jwkが失敗しました。私はこれではまりました)
とりあえず以下で、なんちゃってOpenID Connectサーバが立ち上がるかと思います。
> npm install
> node app.js
.envに何も指定していなければ、ポート10080で立ち上がっているはずです。
試してみる
ブラウザから、以下を開いてみましょう。
さっそく、認証してみましょう。
client_id、scope、stateに適当な値を入れて、「サインイン開始」ボタンを押下しましょう。(なんちゃってサーバなので、値は何も見ていないのです。。)
ログインページが表示されます。
ここでも、ユーザID・パスワードには適当に値をいれて、「サインイン」ボタンを押下します。
ログインが成功し、なんちゃってIDトークンとアクセストークンが生成されました!
フローを図で示しておきます。
一番最初のページで、response_typeをtokenにした時とcodeにした時でちょっとフローが異なります。
(response_type=codeにおいて、本来ならセキュリティ上アプリクライアントシークレットやトークンはブラウザに渡ることがないように実装しますが、今回はわかりやすさを優先していますので、ご注意ください)
できたトークンを以下のサイトで内容を確認できます。
AWS API Gateway+Lambdaで立ち上げる
LambdaアップロードのためのZIPファイルを作成する
上記のようなローカルホストに立ち上げてもよいのですが、HTTPSで接続したいので、API Gateway+Lambdaで立ち上げてみます。
(独自にHTTPSで立ち上げられる場合はこの作業は不要です。)
LambdaにZIPで固めてアップロードします。
> cd api\controllers\oauth2
> npm init -y
> npm install --save rsa-pem-to-jwk
> npm install --save jsonwebtoken
> mkdir helpers
> cp ..\..\helpers\response.js helpers\response.js
> cp ..\..\helpers\redirect.js helpers\redirect.js
最後に、oauth2フォルダにあるファイルとフォルダ一式をZIPファイルに固めます。例えばindex.zipとします。
LambdaにZIPファイルをアップロードする
それでは、Lambda関数を作成しZIPファイルをアップロードします。
適当に、test-oauth2 という名前の関数にしました。
アップロードしてみましょう。
こんな感じでアップロードできました。
node_modulesやjwks、helpersも一緒にアップロードされました。
最後に、環境変数で、HELPER_BASEとして「./helpers/」と設定します。
また、オレオレ証明書のファイルの場所がLambdaからは違って見えるので以下のようにソースコードを修正します。
var priv_pem = fs.readFileSync('./api/controllers/oauth2/jwks/privkey.pem');
→
var priv_pem = fs.readFileSync('./jwks/privkey.pem');
※swagger_nodeとlambdaで多少動きが違うところがあり、トークンエンドポイントにおけるBody部の解釈が異なるようです。
・・・
if( event.path == '/oauth2/token'){
// Lambda+API Gatewayの場合はこちら
var params = new URLSearchParams(event.body);
// swagger_nodeの場合はこちら
// var params = Object.entries(JSON.parse(event.body)).reduce((l,[k,v])=>l.set(k,v), new Map());
・・・
いくつか環境に合わせて修正が必要なのですが、とりあえず、API Gatewayの作成に移ります。
API GatewayでAPIを作成する
API Gatewayに新しいAPIを作成します。
新しいAPIの作成のチェックボックスは、「Swaggerからインポート」を選択します。
そして、エディットボックスに、api\swagger\swagger.yamlのファイルの内容をコピペします。
そして、「インポート」ボタンを押下します。
途中警告が出ますが、「インポートして警告を無視する」ボタンを押下して、インポートを継続します。一部API Gatewayではサポートしていないものがあるためです。
まず、/swaggerは使わないので削除します。
次に、各GETやPOSTに、先ほど作ったLambda関数「test-oauth2」を割り当てます。
このとき、Lambdaプロキシ統合の使用のチェックボックスをOnにしてください。
次に、各GETやPOSTに対して、CORSを有効化します。
最後に、デプロイします。
ステージ名はv1としてみました。
これで、デプロイされ、URLが割り当たりました。以下の感じになっていると思います。このURLを覚えておきます。
https://*****.execute-api.ap-northeast-1.amazonaws.com/v1
次に、test-oauth2のLambda関数に戻ります。
環境変数で、BASE_URLとして先ほどのAPI Gatewayで割り当たったURLを設定します。
試してみる
さっそく、Lambdaに上げたなんちゃってOpenID Connectサーバにアクセスしてみます。
アクセスには、先ほど立ち上げたローカルのサーバを使います。
start.jsとstart_login.jsとstart_redirect.jsのbase_urlをAPI GatewayのURLに変更します。
そうすることで、ローカルサーバではなく、API Gatewayに立ち上げた認証エンドポイント・トークン取得エンドポイントを呼び出すようになります。
ローカルで試した時と全く同じような動作となったかと思います。
フローにしてみると、こんな感じです。
CognitoのIDフェデレーション連携してみる
Cognitoには、それ自身でユーザアカウントのサインアップ・サインインのための機能やOpenID ConnectのIDトークン/アクセストークンを生成・検証する機能提供していますが、そのアカウントとして、GoogleやLINE、YahooIDなどのソーシャルアカウントを取り込むことができます。
参考
AWS CognitoにGoogleとYahooとLINEアカウントを連携させる
今回は、それと同様に、なんちゃてで立ち上げたOpenID ConnectサーバをCognitoに組み込みたいと思います。
まずは、Cognitoでユーザプールを作成します。
適当に「DummyUserPool」という名前にしました。よく使うである属性emailは標準属性として選択しておきました。
あとは適当に。
とりあえず、アプリクライアントは後で作成するので、次のステップに進めます。
ユーザプールが作成されました。
ついでに、ドメイン名も設定しておきます。
それではさっそく、IDフェデレーションの設定をしていきます。
左のナビゲーションから、フェデレーションのIDプロバイダを選択します。
そして、当然、OpenID Connectを選択します。
設定していきますが、なんちゃってサーバは、値を見ないので、適当な値で構いません。
ただし、発行者のところは、
api\controllers\oauth2\index.jsの以下のところに指定した値にします。
const issuer = process.env.ISSUER || 'https://localhost';
何も変えていなければ、「https://localhost」 です。(このURLに接続するわけではないのでなんでもよいですが、HTTPSである必要があります。)
以下は例です。
プロバイダ名:test-oauth2
クライアントID:test-client(というより、何もチェックしていないので何でもよいです)
クライアントシークレット:test-secret(というより、何もチェックしていないので何でもよいです)
属性のリクエストメソッド:GET
承認スコープ:openid profile email
発行者:https://localhost
「検出の実行」を押下します。が、反応しないので、認証エンドポイント等々の入力テキストボックスが表示されます。
以下を設定します。
認証エンドポイント:【API Gatewayで割り当てられたURL】/oauth2/authorize
トークンエンドポイント:【API Gatewayで割り当てられたURL】/oauth2/token
ユーザ情報エンドポイント:【API Gatewayで割り当てられたURL】/oauth2/userInfo
Jwks uri:【API Gatewayで割り当てられたURL】/.well-known/jwks.json
最後に、「プロバイダの作成」ボタンを押下します。
属性マッピングの設定もします。
OIDC属性として「email」、ユーザープール属性として「Email」を選択し、「変更の保存」を押下します。
アプリクライアントを作成する
なんちゃってOpenID Connectサーバの準備ができましたので、認証するための準備を進めます。
まずは、いつものようにアプリクライアントを作成します。「クライアントシークレットを生成」はOnにします。
以下の感じで作成されました。
アプリクライアントIDとアプリクライアントのシークレットが割り当たりました。
次に、アプリクライアントの設定に移ります。
有効なIDプロバイダとして、先ほど設定したtest-oauth2を選択しておきます。ついでに、Cognito User PoolもOnにしておきましょう。後で、その意味がわかります。
コールバックURLとログアウトURLには、ローカルに立ち上げたサーバのURLを入力します。例えば、「http://localhost:10080/login/redirect.html」ってな感じです。
許可されているOAuthフローには、Authorization code grantとImplicit grantを選択しておきます。
許可されているOAuthスコープには、email、openid、profileを選択しておきます。
試してみる
それでは、さっそくCognito経由でなんちゃってOpenID Connectサーバで認証してみます。
ローカルに立ち上げたWebサーバの一部を書き換えます。
stat.jsとstart_redirect.jsのbase_urlをCognitoのドメイン名に変更します。ドメイン名は、Cognitoのアプリの統合で設定したドメインプレフィックスを含んだもので、以下の通りです。
https://【ドメインのプレフィックス】.auth.ap-northeast-1.amazoncognito.com
start_login.jsは、変更せず、API GatewayのURLのままにします。
それでは、ブラウザから、ローカルに立ち上げたWebサーバにアクセスしてみましょう
まずは、response_type=codeで試してみましょう。
client_idには、Cognitoで払い出したアプリクライアントIDを指定します。
scopeには、「openid profile email」を指定します。
stateには適当でよいです。たとえば、「abcd」とでもしておきます。
ログイン方法として、test-oauth2とCognito User Poolの2つを選択しました。ですので、表示されたページにはどちらでログインするか選択するようになっています。もちろん、test-oauth2を選択します。
そうすると、いつものログイン画面が現れます。ユーザIDとパスワードは適当に入力します。
リダイレクトされてきました。ブラウザのURLに、認可コードが返ってきているのがわかります。
ここで、Cognitoで払い出されたアプリクライアントIDとアプリクライアントのシークレットを入力し、「トークン生成」ボタンを押下します。
無事に、Cognitoからトークンが払い出されました!
「userInfo」も呼び出せるようです。
フローにしてみると、こんな感じです。
(ややこしーーー)
以上