#はじめに
Unityでアプリを作って、ユーザーにログインしてもらいたいこと、ありますよね(たぶん)。
本業のゲーム開発者の方などは、もちろん独自の認証システムやユーザーデータベースを用意されると思います。
でも、同人的に遊んでみたい方などは、そこまでやるのはちょっと大変(たぶん)。
というわけで、今回はそんなニッチな路線を狙って、試してみたことを備忘録もかねてまとめてみます。
#初投稿なので自己紹介!
私は出版社勤務の(普通の)編集者で、週刊誌を作ったり、小説の単行本を担当したりしてきました。
が、最近はエンジニアの方々に集まっていただいてチームを作り、ウェブメディアを中心とするサービス開発を「なるべく内製化しよう」という運動をしています。
紙の本だけに向き合うのでは未来像が描きにくい時代、コンテンツを届ける仕事をするなら、独自開発もできなきゃダメよね!という気持ちです。
年末年始に挑戦してみたことがあり、せっかくなのでできたことをQiitaに投稿してみたいと思います!
※2019/01/05:書き上がったら、知り合いのエンジニアの方々にレビューをお願いしようと思うので、現時点ではまだ暫定版としておきます。
※2019/01/05:素人の書いたものですので、誤解や無意味な部分、セキュリティリスクなどが含まれている可能性もあります。今後もブラッシュアップしていくつもりですが、あくまで「やってみた」記事だとお考えください。
目次 | 内容 |
---|---|
この記事で扱う内容 | 実現すること/具体的に行うこと |
何がしたかったか | ※読み飛ばし可 |
そもそも、「Auth0」とは | 選定の理由 |
Auth0のセッティング | APIとApplicationの作成 |
Auth0のライブラリ「Lock」でログインウィジェットを作ってみる | あっという間にログインウィジェットが完成 |
AWSでaws-serverless-expressを組んでみる | AWS CLIのセットアップ/aws-serverless-expressのセットアップ |
aws-serverless-expressにAuth0のログインウィジェットを組み込む | ログインウィジェットをpug化 |
Callbackページの設定 | Auth0の設定/callbackページもpugで作る |
app.jsを書き直す | npmの追加/access_tokenを検証する/検証が通ったらcallbackページをレンダリング |
動作確認 | ログイン --> ボタンを押してjsがちゃんと動くことの確認 |
独自ドメインの適用 | CloudFrontで独自ドメインを当てる |
SNSなどOpenIDでのログインを追加 | Googleの設定/Facebookの設定 |
Unityからアクセスしてみる | EmbeddedBrowserを入れてURLを指定する/サイズの調整/callbackは動作するか? |
本当はやるべきこと | セキュリティについても考えたかった |
#この記事で扱う内容
##実現すること
- ユーザーに、UnityアプリからID/PASSやSNS(Facebook,Twitter...)でログインをしてもらう
- ログインしたユーザーだけが、次のシーンに進めるようにする
##具体的に行うこと
- 認証サービスAuth0をセッティング
- AWSのaws-serverless-expressでAPI GatewayとLambda一式をサクサク作成
- UnityのEmbedded Browserを利用してログインUIを作成
- ログインした人だけが見られるLambda経由でUnityのシーンを切り替える
#何をしたかったか(※読み飛ばし可)
もともと興味があったのは、Unityを使ってMultiPlayer Gameを作ることでした。
バーチャル空間を自由に作れるようになれば、今後、いろいろなオモシロ会員制ビジネスも作れるよね!というボンヤリした夢があるからです。
そんな中、2018年12月リリースのUnity2018.3では、スクリプティング・ランタイムとしてこれまで標準だった.NET3.xにかえて、.NET4.xも使えるようになりました。
(https://unity3d.com/jp/unity/whats-new/unity-2018.3.0)
たしか2018.1以降くらいであれば、Settingを変えれば4.xも実験的には利用できていたと思うのですが、ついに「実験的」という文言がはずれたのです。
一方で、Auth0+Unityで問題となってきたのが、この.NETのバージョン問題でした(と、勝手に思っている)。
例:「https://community.auth0.com/t/how-to-implement-auth0-in-unity-game-engine/8796」
というわけで、「Auth0の.NET SDKがそのまま使えるんじゃ?」と考えたのが、年末年始にこの課題に手をつけたキッカケでした。
ただ、そっちのほうは初心者としては、まだまとめられるほどの内容がありません。
なので、先に簡単に動かせたほうをまとめてみました。
#そもそも、「Auth0」とは
こちらの記事→「認証プラットフォーム Auth0 とは?」でAuth0日本法人の古田さんがまとめてくださっていますが、「ログインしてもらう」サービスを、さまざまなレベルで、ポチポチ、サクサクで作れるサービスIDaaS(Identity as a Service)です。
古田さんのQiitaには、Auth0のdefaultの選択肢にはないLINEアカウントでのログインを追加する方法(「Auth0でLINEログインを実装してみた (v2.1対応)」)など、おもしろい記事がいろいろあるので、ぜひ参考になさってください!
私自身は、なぜ今回、Auth0を使ってみたかというと、
- さまざまなIdP(Id Provider、GoogleとかFacebookとかTwitterとか)が簡単につなぎこめる
- AWSのリソースとの相性がよい(今回は扱いませんが、Cognitoにもつなぎこめるので、UnityのAsset BundleをS3において、特定のユーザーだけアクセス可能にする、といったこともできるはず)
- いろいろマネージドな形でリソースを提供してくれるので、開発コストが低い(ログインウィジェットもすぐ実装できる)
といった理由があげられますが、実は仕事でご縁があったから自分でも触ってみたかった、というのが一番大きいです(笑)。
#Auth0のセッティング
Auth0は、全般的にドキュメントが大変しっかりしたサービスなので、ググるといろいろなガイダンスが出てきます。
無料でアカウントを作っても、かなり充実した機能が使える印象です。
参考:「クラウド認証サービス Auth0 の無料トライアルを試してみよう」
アカウントを作成 > テナントを作成
までは、上記を参考に進めてください。
この状態だと、defaultのapplicationとAPIしかない状態だと思います。お試しなので、そのまま作業を進めても構わないのですが、念のため、テスト専用のAPIとapplicationを用意します。
先にAPIをポチポチと作りましょう。メニューのAPIsに進みます。
Dashboard > APIs > CREATE API
すると、下のようなモーダルが出てきます。
Name
は自分にわかりやすいものでOK。
Identifier
は注意書きにもあるように「URLの形式をとっていればいいだけ」で、実際に外部からアクセス可能なURLである必要はないので、適当なURLを書いてください。
ただし、あとから変更はできないので、独自ドメインを取っている方などは、できれば自分が管理しているドメインのURLを書けるといいのではないかと思います。
Signing Algorithm
は、OpenIDでソーシャルログインと連携したいので、RS256にしておくほうがよいようです。
(※あとで出てくるaccess_tokenの形式とも関係しています)
これでAPIがひとつできます。
(APIなのにAppっていう名前にしてもうた…汗)
このままだと、このAPIはオールマイティで、どこからでも叩ける上に、いろいろできてしまうので、本来はscopeで必要最小限のroleだけ使うようにセッティングしたり、Rulesでアクセスの制限を行ったりする必要があります。
今回は私が力尽きたので、そこの記事化は取りこぼしという感じです。
次にapplicationを作ります。
Dashboard > Application > CREATE APPLICATION
すると、下のような選択肢が出てきます。
今回はLambda経由であれこれ取り回しを行うので、Machine to Machine App
として作成してみます(他でもいいかもしれない)。
認証に使うAPIを選択する画面になるので、先ほど作ったAPIを選びます(どれを使うかは、あとからSettingで変えられます)。
今回は、FacebookなどSNS系のIdPとの連携などは、全体の仕組みを組み上げてからセッティングします。
#Auth0のライブラリ「Lock」でログインウィジェットを作ってみる
ここまで完全にポチポチで進んできました。
「で、実際このApplicationとどうやってやりとりするの?」
ということなんですが、工数が少なくて済むのは、Auth0が提供しているライブラリ「Lock」(https://auth0.com/docs/libraries/lock/v11)でログイン&サインアップフォームを作ってしまうことではないかと思います。
Lockで作られるウィジェットのデフォルトの状態は上記のページなどで確認していただけますが、そのままだと明らかにAuth0なので、オリジナルサービスとしては少し悲しいです。
ところがLockでは、optionを指定するだけで、ちょっとしたカスタマイズがごくごく簡単に行えます。
(https://auth0.com/docs/libraries/lock/v11/configuration)
しかも今回は、UnityのEmbedded Browserでログインフォームを表示させて使うことを考えていますので、シンプルにログインフォームだけの画面が作れれればOK。
というわけで、試しに、下記のようなログインのページを作成します。
ライブラリのページの最下部に、コードのサンプルがありますが、inline用のコードほぼそのまんまです。
<head>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<div id="root" style="width: 320px; margin: 40px auto; padding: 10px; box-sizing: border-box;">
embedded area
</div>
<script src="https://cdn.auth0.com/js/lock/11.6.1/lock.min.js"></script>
<script>
var lock = new Auth0Lock('YOUR_CLIENT_ID', 'YOUR_AUTH0_DOMAIN', {
container: 'root',
language: 'ja',
languageDictionary: {
title: "LOGIN"
},
theme: {
logo: 'https://YOUR_DOMAIN/logo.png',
primaryColor: '#33cccc'
},
socialButtonStyle: 'small',
auth: {
redirectUrl: 'YOUR_REDIRECT_URL',
responseType: 'token id_token',
params: {
scope: 'openid email'
},
audience: 'YOUR_AUDIENCE',
responseMode: 'form_post',
}
});
lock.show();
</script>
</body>
YOUR_CLIENT_ID
とYOUR_AUTH0_DOMAIN
、audience: 'YOUR_AUDIENE'
については後述します。
YOUR_REDIRECT_URL
は、認証後にcallbackされてくる先のURLですが、この段階ではまだ指定できませんので、ひとまず放置します。
responsMode
は、のちにAWSのaws-serverless-expressでセッティングするときに効いてくるおまじないです。
(なんて思わせぶりをできるような玄人さんの身分ではないので…。これは認証後のredirect時に、tokenをPOSTすることを明示するセッティングです)
そのほかのパーツは、見た目のカスタマイズ用のoptionです。
language: 'ja'
でUIを日本語化できます。余計ごとながら、化けるのでhead > meta
で文字コードはちゃんと指定しましょう。
languageDictionary
はUIの各パーツの文言などをいじる部分(だと思う)ですが、ここではtitle
を変えました。デフォルトだと「Auth0」と書いてあります。
logo
のhttps://YOUR_DOMAIN/logo.png
は、どこかオンラインにホストしたログインフォーム用のロゴ画像です。
高さ58pxが推奨とされています(この辺りも上のoptionの説明の中で詳述されています)。
指定しない場合は、Auth0のロゴマークが出てきます。
primaryColor
はパーツの基調色です。Auth0のデフォルトは朱色。ここはお好みでどーぞ。
めっちゃ便利。
Googleでのログインボタンが出ているのは、Auth0がデフォルトのセッティングで「Googleもつけるとこうなるよ」というサンプルを入れ込んでいるからです。
あとでSNSログインを調整するときに整えていきます。
上記で説明を後回しにしたYOUR_CLIENT_ID
とYOUR_AUTH0_DOMAIN
ですが、これはAuth0のコンソールで確認できます。
Dashboard > Application
で先ほど作ったAppを選ぶと出ています。
実は、ここを開かなくても、ドメインは右上の自分のアカウントのボタンの横に表示されている<テナント名>.auth0.com
です。
また、Client ID
も他の場所にも表示されているんですが、ここを開くとコピーボタンがあるので楽ですよね。
もうひとつ、重要なのがaudience
です。
Auth0のいろいろなチュートリアルをやると、audienceに~~~~/userinfo
といった形のURLを入れてみましょう、といったサンプルがありますが、今回は、それはやめておきましょう。
audienceに上記の形のURLを入れると、返ってくるaccess_tokenがjsonにならないというドツボがあるようです。
あとあと、Lambdaの中でパースするときに不便なので、audienceには次の値を入れましょう。
Dashboard > APIs
で先ほど使うことにしたAPIを選びます。
はい。これはAPIを作るときに書いた、適当なURLです(笑)。
このように、コードの中に出てきてしまうので、あんまりふざけた責任の持てないURLを書いてしまわないようにしたほうがよさそうですよね。
#AWSでaws-serverless-expressを組んでみる
さて、ここまでで作ったようなログインページを、S3でもなんでもいいのでホスティングすれば、ログインのフローを作っていくことはできます。
ただ、Unityアプリに埋め込んだブラウザからしか使わないページをわざわざホスティングしつづけるのも、なんだかつまんないかなあ、と思ったので、サーバレスな構造をいろいろ試してみました。
- API Gatewayにlambdaで書いたcustom authorizerを噛ませて、cognito連携してIAMのロールを振り出す
- API Gatewayのcustom authorizerとして、Auth0をIdPとして連携したcognitoを指定してみる
最初に挑戦したのがこういう系のAWS内の権限を取り回すようなやり方だったんですが、うまくいかなかったんですね。
そもそも、初心者なんで、まだそこまで実力がなかった、というのが一番の理由。その代わりめっちゃ勉強にはなりました。
あと、少しだけ言い訳すると、「簡単にできる」と標榜するには、ちょっと大変かなあと感じてしまいました。
そんな中で、Auth0のドキュメンテーションを見ていて、「あー、なんかこれいけるんじゃない?」と思ったのが、Node.jsのexpressで攻める方法でした。
そもそも、Auth0のOpenID Connect(OIDC)のフローを回すと、access_tokenとid_tokenが取れます。
そのvalidateをするサンプルが、みんなexpressで書いてあったんですよね。
※「OpenIDでtokenが取れるとかどうとかって、なに?」っていう方は、下記のページがホントにわかりやすいです。いろんな記事で引用されています。
「一番分かりやすい OAuth の説明」
「一番分かりやすい OpenID Connect の説明」
※単純に、expressはめちゃくちゃスタンダードなNode.jsのフレームワークだというだけかもしれないけど、そこで暴走してしまうのが素人の強み(笑)。
上記で、サンプルがexpressで書いてある、と言ったのは、たとえば下記のようなページです。
「Server Client + API: Node.js Implementation for the API」
##AWS CLIの準備
※すでにAWS CLIをバリバリご利用の方は、このセクションは飛ばしてください。
この部分は、次の記事を参考にさせていただきました!
「AWS CLIのインストール」
私自身はmacユーザーなので、基本macを前提に書きますが、参考記事はみなさん他のOSについても解説してくださっているので、参考になさってください。
そもそものAWSのアカウント取得などなどは、たくさん記事があるので、探してみてください。
###STEP1: pipのインストール
pipはpythonベースのパッケージ管理ツール、であるそうです。
curl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py"
sudo python get-pip.py
pythonをお使いの方は、すでに入っている可能性もあります。macOSの場合は、デフォルトのpythonだと入っていないようです。
python自体をきっちりインストールしたい方は、こちらの記事をどうぞ。pyenvで入れると、pipがついてくるようです。
- 「Pythonインストール(Mac編)」※この記事は古いということで、新版が出ています。ただ、そちらではpipへの言及はありません。
- 「Python3インストール(Mac編)」
- Windowsでのpipのインストールについて(https://qiita.com/suzuki_y/items/3261ffa9b67410803443)
###STEP2: AWS CLIのインストール
sudo pip install awscli --upgrade --ignore-installed six
###STEP3: AWSの設定確認
configを行うのに必要な情報を取得していきます。ここは公式ドキュメント(https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-chap-configure.html)がわかりやすいです。
AWSコンソール > IAM > ユーザー
と進み、使いたいユーザー名をクリックします。
タブの中から、認証情報
を選ぶと、アクセスキーの生成
があります。
ここから、アクセスキー
とシークレットアクセスキー
のペアが得られます。
###STEP4: configの設定
aws configure
と叩いてやると、下記の設定を順番に聞かれます。
STEP3で生成したアクセスキー
とシークレットアクセスキー
を入れていきます。
region
は、日本のユーザーの方はたいてい「東京(ap-northeast-1)」だと思いますが、違うかたはご自身の環境に合わせてください。
output format
はjson
としておけばよいようです。
AWS Access Key ID [None]: hogehogehogehoge
AWS Secret Access Key [None]: fugafugafugafuga
Default region name [None]: ap-northeast-1
Default output format [None]: json
あと、アップデートなどその他の手順もあるのですが、それは元記事で書いてくださっているのを参考になさってください。
これで一応、AWSリソースをCLIで使えるようになりました!
##aws-serverless-expressを使ってみる
この部分は、次の記事を参考にさせていただきました!
「AWS 謹製 aws-serverless-express を使って APIGateway + Lambda + Node + Express で RESTful サービスの雛形を最速で作る」
参考というか、調査中にこの記事に出会わなかったら、これをやってみませんでした。伏して感謝します!
非常にわかりやすく書いてくださっているので、初心者でもすぐにサーバレスでexpressを使える環境が作れました。
ただ、その後、Lambdaやaws-serverless-expressの仕様が少し変わっている部分もあるようだったので、その辺りを順を追って書いてみます。
私の説明がすっ飛んでいるところもあると思いますので、上記記事はぜひご一読ください!
###STEP1: Node.jsの環境設定
上記の参考記事の時点と、Lambdaが対応しているNode.jsのバージョンが変わっているようだったので、私は一番新しいバージョンで環境を設定してみました。
2019/01/04時点で、Lambdaの対応している最新のNode.jsは、v8.10となっています(Lambdaのコンソールを見ると、そうなっていました)。
nodebrew use v8.10.0
###STEP2: git cloneしてくる
AWSが提供してくれているaws-serverless-express
のリポジトリはこちら。ドキュメンテーションも確認しながら作業するとよいようです。
https://github.com/awslabs/aws-serverless-express
作業するフォルダを仮にtestApp
とします。
git clone https://github.com/awslabs/aws-serverless-express.git
cp -r ./aws-serverless-express/examples/basic-starter ~/testApp
cd ~/testApp
参考記事の時点では、exampleはひとつだったようなのですが、この記事を書いている時点では「examples」になっており、参考記事で紹介されているものは「basic-starter」のほうのようです。
###STEP3: config設定
npm run config -- --account-id="<accountId>" --bucket-name="<bucketName>" --region="ap-northeast-1"
アカウントID
は、AWSコンソール > 右上の自分のアカウント情報(アカウント)
を開くと、最上部に出ている文字列(数列?)です。
region
を最後に指定していますが、これはデフォルトだと米国西部になってしまうようなので、必ず指定しましょう。
バケット名
は、これからアップロードするソースの置き場なのですが、ここで割とハマりました。
バケット名
には、「世界でただひとつの名前」をつける必要があるらしいのです。
最初、私は「serverless-express-test」などと、なんとなくありそうな名前にしていたら、一向にCloudFormationが成功しませんでした。
しかも、このnpm run config
は「一発勝負」で、最初の1回しか効きません。
ここで設定したものをやり直したい場合は、フォルダ内のpackage.json
,simple-proxy-api.yaml
,cloudformation.yaml
を手動で修正しないといけないのです。
(とはいえ、バケット名が出てくるのはpackage.jsonだけだったけど)
npm run config
が一発勝負だよ、といったことは、githubのリポジトリのexample/basic-starter
で説明されています。
https://github.com/awslabs/aws-serverless-express/tree/master/examples/basic-starter
というわけで、バケット名には、およそ世界の誰も書かないような名前をつけてconfigを行ってください(笑)!
(私は結局、自分の名前をローマ字で平打ちして末尾に追加しましたw)
###STEP4: いよいよセットアップ!
ドキドキです!
npm run setup
成功パターンの場合、ちょっと待っていると、何事もなかったようにターミナルが落ち着きます。
そこで、AWSコンソール > CloudFormation
を確認してみましょう(「管理とガバナンス」の中にあります)。
なんかできてる!
うれしい。
このスタックの名前
を変更したい場合は、npm run config
の最後に--function-name="<functionName>"
として指定すれば、好きなものにできます。
スタック名をクリックすると、こんな画面に出ます。
ApiUrl
にアクセスしてみてください。
かわいい。
スクショでは下のほうは切れてしまっていますが、このサンプルページには、簡単な会員情報をGET/POSTするendpointが列挙されています。
###STEP5: 修正反映方法の確認
これで、サーバレスでexpressを使う準備はできました。いやー、すごい。勢いだけで、ここまで来られる時代が来た!
ここから、basic-starterをちょこっといじるだけでログイン画面を作ってしまおうという魂胆なのですが、このApiGateway+Lambdaのシステムを修正した場合に、反映する方法を確認しておきます。
npm run package-deploy
こちらです!
参考記事のときと、更新用のコマンドが変わっているようなので、ご注意ください(githubの説明はこちらになっていました)。
#aws-serverless-expressにAuth0のログインウィジェットを組み込む
ここまで来たら、あと一息です。
aws-serverless-expressのbasic-starterはどういう構造になっているのかみてみると、
basic-starter
|
|--app.js
|--lambda.js
|--package.json
|--その他の設定ファイル
|
|--node_modules
| |--いろいろなnpm
|
|--views
|--index.pug
となっています。appの挙動を変えていくには、通常はapp.js
を修正していきます。
lambda.js
などには手を入れる必要はありません。
ただ、Auth0でのログインをサクサク組み込んでいけると気分的に「アガる」と思うので、先走って、前半で作成したAuth0のログインウィジェットをここに組み込んで表示させてしまいましょう。
素のAPIのエンドポイントを叩いたときの、リスがニコニコしているかわいい画面はどこで作られているかと見てみると、index.pug
です。
「pugってなんじゃい」という方は、こちらの記事などが参考になります。
「Pug(Jade)って何だ?特徴や基本的な使い方の解説」
旧名「Jade」といったそうですが、htmlを効率的にコーディングするためのテンプレートエンジン、ということのようです。
といっても、もともとhtmlだったものをpug化するのは、割と簡単です。
私は、こちらのページなどを参考に手で書き直してみました。
「Pug(Jade)記法でHTMLのテンプレート的なの」
ただ今回は、pugの記法の哲学とか美しさを度外視して、まずは目的に最短で近づこう、ということなので、ちょっとチートして変換ツールを使ってもいいかと思います。
下記のサイトは、Jade時代に作られたということで、必ずしもpugに対応しているわけではない、といった注意書きのある記事も見かけたのですが、今回使う分には困らないようでした。
「https://html2jade.org/」
HTMLを放り込むとJadeにしてくれる、というツールです。
変換ツールを使うか使わないかはお好みですが、前半で作ったログインウィジェットをpug化してみると、こんなふうになります(サンプルは手動で書き直したものです)。
head
meta(name="viewport" content="width=device-width, initial-scale=1")
meta(http-equiv="Content-Type" content="text/html; charset=UTF-8")
style.
div#root {
width: 320px;
margin: 40px auto;
padding: 10px;
box-sizing: border-box;
}
body
div#root
embedded area
script(src="https://cdn.auth0.com/js/lock/11.6.1/lock.min.js")
script.
var lock = new Auth0Lock('YOUR_CLIENT_ID', 'YOUR_AUTH0_DOMAIN', {
container: 'root',
language: 'ja',
languageDictionary: {
title: "LOGIN"
},
theme: {
logo: 'https://YOUR_DOMAIN/logo.png',
primaryColor: '#33cccc'
},
socialButtonStyle: 'small',
auth: {
redirectUrl: 'YOUR_REDIRECT_URL',
responseType: 'token id_token',
params: {
scope: 'openid email'
},
audience: 'YOUR_AUDIENCE',
responseMode: 'form_post',
}
});
lock.show();
注意点としては、pugの記法ではタグの閉じがない代わりに、インデントが割と重要な意味を持つらしい、ということで、htmlでの階層構造に合わせてきれいにインデントしてあげることが必要なようです。
また、上のサンプルには入れていませんが、パラメーターのセクションにredirectUrl: 'YOUR_REDIRECT_URL' //これがコールバック
などといったコメントを入れていると、エラーになるという記事も見かけました(実際、私もコメントが入っているpugではうまく表示ができませんでした)。
では、/testApp/basic-starter/views/index.pug
を、上で作ったlogin.pug
と入れ替え、login.pug --> index.pug
とリネームしてみましょう。
その上で、システムを更新します。
npm run package-deploy
先ほどのAPIのエンドポイントにアクセスすると…
イエイ!
これはアガりますね。
#Callbackページの設定
##Auth0での設定
ここまでずっと放置してきたのが、ログインウィジェットのredirectUrl
です。
そこで、index.pug
(旧login.pug)内のcallbackのエンドポイントをredirectUrl: 'https://<YOUR_API_PREFIX>.execute-api.ap-northeast-1.amazonaws.com/prod/callback'
としてみます。
Auth0では、Dashboard > Application > Settings
の中に、callbackとして許可するURLを記載する必要があります。
コンマ区切りで必要なURLを追加することができるので、開発環境や独自ドメインを当てる前のAPI Gatewayなども入れておくことができます(完成したら不要なものは消したほうがよいでしょう)。
※独自ドメインを設定される方は、index.pug
内に記述するべきredirectUrlも変わってしまうので、最後に全体の整合性が取れるように調整してください。
このタイミングで、Allowed Web Origin
なども記述しておくとよいかもしれません。
##Callbackのページもpugで作っておく
今回は、Unityのsceneを進めるよう、SceneManagerと連携するボタンをつけるのが目標ですが、実体としては単にjavascriptを発動させるボタンができればいいので、サンプルとして、次のようなpugを用意します。
html
head
meta(name='viewport', content='width=device-width, initial-scale=1')
meta(http-equiv='Content-Type', content='text/html; charset=UTF-8')
style.
input {
width: 16rem;
height: 4.5rem;
margin: 0 auto;
background-color: #33cccc;
color: #fff;
font-size: 20px;
}
script(type='text/javascript').
function startGame(){
alert('hello world!');
}
body
#button-area(style='width: 320px; margin: 40px auto; padding: 10px; box-sizing: border-box;')
input(type='button', value='StartGame', onclick='javascript:startGame();')
ボタンを押すと、「hello world!」というやつです。よくあるパターン。
button-area
のstyleがベタ書きなところが、先ほどのindex.pug
(元login.pug)と違いますが、多分callback.pugのほうが「美しくないpug」です。
でも、どちらでもちゃんと動いてくれます。
このcallback.pug
も、index.pug
と同じ/basic-starter/views
フォルダに入れておきましょう。
#app.jsを書き直す
見た目の部分はviews
フォルダに用意できたので、次に機能の部分を調整していきます。
主にいじるのはapp.js
ですが、いくつか考えておくべきことがあります。
##npmの追加
前半戦で、わかりやすいOAuthとOpenIDの解説記事について触れました。
あの記事を読むと一目瞭然なのですが、ログインが済んだら、クライアントが受け取ったaccess_tokenを検証しなければいけません。
Auth0が提供しているこちらのドキュメント(「Server Client + API: Node.js Implementation for the API」)のサンプルには、こんなコードが出ています。
// set dependencies
const express = require('express');
const app = express();
const jwt = require('express-jwt');
const jwksRsa = require('jwks-rsa');
const bodyParser = require('body-parser');
// enable the use of request body parsing middleware
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: true
}));
// create timesheets upload API endpoint
app.post('/timesheets/upload', function(req, res){
res.status(201).send({message: "This is the POST /timesheets/upload endpoint"});
})
// launch the API Server at localhost:8080
app.listen(8080);
// set dependencies - code omitted
// enable the use of request body parsing middleware - code omitted
// Create middleware for checking the JWT
const checkJwt = jwt({
// Dynamically provide a signing key based on the kid in the header and the signing keys provided by the JWKS endpoint.
secret: jwksRsa.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `https://YOUR_AUTH0_DOMAIN/.well-known/jwks.json`
}),
// Validate the audience and the issuer.
audience: process.env.AUTH0_AUDIENCE,
issuer: `https://YOUR_AUTH0_DOMAIN/`,
algorithms: ['RS256']
});
// create timesheets API endpoint
app.post('/timesheets/upload', checkJwt, function(req, res){
var timesheet = req.body;
// Save the timesheet entry to the database...
//send the response
res.status(201).send(timesheet);
})
// launch the API Server at localhost:8080 - code omitted
これを応用するためには、まずexpress-jwt
とjwks-rsa
のnpmを入れておく必要があります。
もともと、aws-serverless-expressには、express
とbody-parser
は入っているんですよね。
なぜそれがすぐわかるかというと、デフォルトのapp.js
がそもそも、こんな作りだからです。
body-parser
の初期化もjsonとurlencodedで別々にやるとか、基本の所作がもう済んでいる。
こういう枠組みの使い回しがきく、というようなところが強みなんだろなー。
'use strict'
const path = require('path')
const express = require('express')
const bodyParser = require('body-parser')
const cors = require('cors')
const compression = require('compression')
const awsServerlessExpressMiddleware = require('aws-serverless-express/middleware')
const app = express()
const router = express.Router()
app.set('view engine', 'pug')
if (process.env.NODE_ENV === 'test') {
// NOTE: aws-serverless-express uses this app for its integration tests
// and only applies compression to the /sam endpoint during testing.
router.use('/sam', compression())
} else {
router.use(compression())
}
router.use(cors())
router.use(bodyParser.json())
router.use(bodyParser.urlencoded({ extended: true }))
router.use(awsServerlessExpressMiddleware.eventContext())
// NOTE: tests can't find the views directory without this
app.set('views', path.join(__dirname, 'views'))
router.get('/', (req, res) => {
res.render('index', {
apiUrl: req.apiGateway ? `https://${req.apiGateway.event.headers.Host}/${req.apiGateway.event.requestContext.stage}` : 'http://localhost:3000'
})
})
router.get('/sam', (req, res) => {
res.sendFile(`${__dirname}/sam-logo.png`)
})
router.get('/users', (req, res) => {
res.json(users)
})
router.get('/users/:userId', (req, res) => {
const user = getUser(req.params.userId)
if (!user) return res.status(404).json({})
return res.json(user)
})
router.post('/users', (req, res) => {
const user = {
id: ++userIdCounter,
name: req.body.name
}
users.push(user)
res.status(201).json(user)
})
router.put('/users/:userId', (req, res) => {
const user = getUser(req.params.userId)
if (!user) return res.status(404).json({})
user.name = req.body.name
res.json(user)
})
router.delete('/users/:userId', (req, res) => {
const userIndex = getUserIndex(req.params.userId)
if (userIndex === -1) return res.status(404).json({})
users.splice(userIndex, 1)
res.json(users)
})
const getUser = (userId) => users.find(u => u.id === parseInt(userId))
const getUserIndex = (userId) => users.findIndex(u => u.id === parseInt(userId))
// Ephemeral in-memory data store
const users = [{
id: 1,
name: 'Joe'
}, {
id: 2,
name: 'Jane'
}]
let userIdCounter = users.length
// The aws-serverless-express library creates a server and listens on a Unix
// Domain Socket for you, so you can remove the usual call to app.listen.
// app.listen(3000)
app.use('/', router)
// Export your express server so you can import it in the lambda function.
module.exports = app
というわけで、npmを追加します。
npm install express-jwt jwks-rsa --save
ちゃんとnode_module
フォルダに追加されます。
##access_tokenの検証プロセスを追加
上で準備したモジュールを読み込んで、tokenの検証プロセスを追加してみます。
issuer
の末尾は/
が入るので注意してください。
'use strict'
const path = require('path')
const express = require('express')
const bodyParser = require('body-parser')
const cors = require('cors')
const compression = require('compression')
const awsServerlessExpressMiddleware = require('aws-serverless-express/middleware')
const app = express()
const router = express.Router()
const jwt = require('express-jwt');
const jwksRsa = require('jwks-rsa');
app.set('view engine', 'pug')
if (process.env.NODE_ENV === 'test') {
// NOTE: aws-serverless-express uses this app for its integration tests
// and only applies compression to the /sam endpoint during testing.
router.use('/sam', compression())
} else {
router.use(compression())
}
router.use(cors())
router.use(bodyParser.json())
router.use(bodyParser.urlencoded({ extended: true }))
router.use(awsServerlessExpressMiddleware.eventContext())
// NOTE: tests can't find the views directory without this
app.set('views', path.join(__dirname, 'views'))
router.get('/', (req, res) => {
res.render('index', {
apiUrl: req.apiGateway ? `https://${req.apiGateway.event.headers.Host}/${req.apiGateway.event.requestContext.stage}` : 'http://localhost:3000'
})
})
// Create middleware for checking the JWT
const checkJwt = jwt({
// Dynamically provide a signing key based on the kid in the header and the signing keys provided by the JWKS endpoint.
secret: jwksRsa.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `https://<YOUR_AUTH0_DOMAIN>/.well-known/jwks.json`
}),
// Validate the audience and the issuer.
audience: 'YOUR_AUDIENCE',
issuer: `https://<YOUR_AUTH0_DOMAIN>/`,
algorithms: ['RS256']
});
router.get('/callback', (req, res) => {
res.send('Please Login')
})
router.post('/callback', (req, res) => {
//router.use(checkJwt)
const all_params = {
access_token: req.body.access_token,
id_token: req.body.id_token,
expires_in: req.body.expires_in,
scope: req.body.scope,
state: req.body.state,
token_type: req.body.token_type
}
res.json(all_params)
})
// The aws-serverless-express library creates a server and listens on a Unix
// Domain Socket for you, so you can remove the usual call to app.listen.
// app.listen(3000)
app.use('/', router)
// Export your express server so you can import it in the lambda function.
module.exports = app
何をしたかというと、
const jwt = require('express-jwt');
const jwksRsa = require('jwks-rsa');
まず、ここで追加したモジュールを読み込みました。
次にサンプルにならって、access_tokenを検証するmiddlewareを構成しました。
// Create middleware for checking the JWT
const checkJwt = jwt({
// Dynamically provide a signing key based on the kid in the header and the signing keys provided by the JWKS endpoint.
secret: jwksRsa.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `https://<YOUR_AUTH0_DOMAIN>/.well-known/jwks.json`
}),
// Validate the audience and the issuer.
audience: 'YOUR_AUDIENCE',
issuer: `https://<YOUR_AUTH0_DOMAIN>/`,
algorithms: ['RS256']
});
そこから下に作られていた、デフォルトのGET/POSTを参考にしながら(と言いつつ消してしまいましたが)、callbackのGET/POSTを書いています。
router.get('/callback', (req, res) => {
res.send('Please Login') //トークンなしでアクセスしてきたら、「Please Login」という文字列を返す。
})
router.post('/callback', (req, res) => {
//router.use(checkJwt) //試しにcheckJwtを使わないで、何がPOSTされているか確認する。
const all_params = {
access_token: req.body.access_token,
id_token: req.body.id_token,
expires_in: req.body.expires_in,
scope: req.body.scope,
state: req.body.state,
token_type: req.body.token_type
}
res.json(all_params)
})
この状態でシステムを更新して(npm run package-deploy
)ログインをしてみていただくとわかりますが、ログインが終わると
-
access_token
(アクセスを許可するトークン) -
id_token
(IdPから提供されたidの情報) -
expires_in
(アクセストークンの有効期限)
といった情報が返ってきています。access_token
,id_token
はJSON Web Token(JWT)になっているので、
https://jwt.io/
こちらのツールを使うと、内容を確認することができます。
##callback.pugを適用する
それでは、最後に先ほど作ったcallback.pug
を適用しましょう。
デフォルトのapp.jsには、こんな記述がありました。
router.get('/', (req, res) => {
res.render('index', {
apiUrl: req.apiGateway ? `https://${req.apiGateway.event.headers.Host}/${req.apiGateway.event.requestContext.stage}` : 'http://localhost:3000'
})
})
これがindex.pug
を読んで、apiUrl
にレンダリングしている部分のようです。
そこで、app.js
のcallback
のPOST
を次のように書き換えてみます。
router.post('/callback', (req, res) => {
router.use(checkJwt)
res.render('callback', {
apiUrl: req.apiGateway ? `https://${req.apiGateway.event.headers.Host}/${req.apiGateway.event.requestContext.stage}/callback` : 'http://localhost:3000/callback'
})
})
checkJwt
が通ったら、callback.pug
を/callback
のURLにレンダリングしなさい、という形になったと思います。
#動作確認
更新 --> 動作確認
してみましょう。
ログインを行うと、こんなページが出てきます。
「StartGame」のボタンを押してみます。
jsが働いて、「hello world!」が表示されました!
デベロッパーツールが入ってしまってスクショが見苦しいのはご容赦ください。
#独自ドメインの適用
API Gatewayとのやりとりは、httpsになりますが、独自ドメインを適用する場合はhttpで書いてきてしまう人もいるかもしれません。
そこで、httpsへのリダイレクトも同時にできてしまうCloudFrontを利用する方法で、独自ドメインを当てました。
流れは、こちらの記事に従います。
「CloudFrontに複数オリジン(API GatewayオリジンとS3オリジン)の設定」
Route53 --> CloudFront --> API Gateway
という流れになりますが、ドメインはUnityの埋め込みブラウザから見るぶんには気にならない部分ですので、今回は割愛します。
実際にやってみて面白かったのは、CloudFrontでrootに/prod
とステージを入れておくと、実際にブラウザでアクセスするURLではステージを書く必要がなくなるんですね。
index.pug
内のcallbackのURLなども書き換える必要がありますので、独自ドメインを当てる方はご注意ください。
また、上記の参考記事で「複数オリジン」の記事を選んでいる理由は、ウィジェットに使っているロゴマークをS3に置いていたからです。
どうせなら、ログイン周りの小物パーツは同じドメイン下に置きたいですよね。
#SNSなどOpenIDでのログインを追加
さて、ここからがAuth0の真骨頂(?)ともいえる、SNSなどIdPとの簡単連携です。
そもそも、Lockでログインウィジェットを作ったとき、コード上ではなんの指定もしていないのに、Googleアカウントでのログインボタンが出ていましたよね。
このウィジェットが素晴らしいのは、Auth0のコンソールでポチポチ設定すれば、IdPが簡単に増やせることです。
実際にやってみましょう。
Auth0でDashboard > Connections > Social
を選択します。
すると、こんなふうに数多くのIdPから、ログインなどで連携したいサービスを選ぶことができます。
この記事はUnityで遊ぼう、といった趣旨なので有名どころのSNSと連携できればよいですが、ECサイトならamazonやpaypal、ビジネス系アプリならLinkedinなどと連携させてもいいでしょう。
ホント便利ですね。
今回はこちらのドキュメントを元に、GoogleとFacebookを追加してみます。
Identity Providers Supported by Auth0
##Google
ドキュメント通りですが、https://console.developers.google.com/projectselector/apis/credentials
にアクセスすると、こんな画面になります。
「作成」で新しい認証情報を作ります。
私の場合は、会社がG Suiteを使っていることもあり、組織が会社に結び付けられました。
すると、ドキュメントとは違って、先に同意画面を作りなさい、と言われます。
そのまま、同意画面を設定
することにします。
ここでscopeを増やしておくと、Googleから提供される個人情報の選択肢の幅が増えます。
会員ビジネスなどでソーシャルログインが重宝されるのは、こうやってマーケティングのヒントになる情報が取れるからですよね。
ただ、今回はお試しなので、そのままにしておきます。
すると、クライアントID
とクライアントシークレット
が生成されました。
これをコピーして、どこかに保存しておきます。
Auth0のコンソールに戻って、ソーシャルログインの一覧からGoogleを選ぶと、次のような画面になります。
このClient ID
とClient Secret
に先ほど取得した値を入れます。
Permissions
は今回の場合、デフォルトのままで問題ありません。上部のタブのApplications
では、このテナント全体でこのIdPの設定を使うか、個別のApplicationで使うように限定するかを選ぶことができます。
個人的には、必要なものだけに制限したほうがよいだろうと思うので、そのようにしておきました。
下部のボタンでSAVE
したのち、TRY
を押してみると、自分のGoogleアカウントでログインするかどうかを確認するモーダルが開きます。
ここでは、いったんこのページは無視して閉じておきましょう。
試しに、この記事で作ったログインページ、
https://<YOUR_API_PREFIX>.execute-api.ap-northeast-1.amazonaws.com/prod
(または独自ドメインの方はそちら)
を開きます。
タブを開きっぱなしで作業していた方はリロードしてください。
「ユーザー登録」のタブで「G」のマークを押すと、先ほどのテストと同じように、ご自分のGoogleアカウントでログインしますか、と問われます。
承認すると、ログインが完了し、callbackに飛ばされます。
注意書きの文言が「auth0.com」と共有します、となってしまう部分をどうやったら解消できるのかは、ちょっと今後の課題です。
事実としてAuth0にも情報が共有されるわけなので、そこのところはしょうがないのか、何か書き方があるのかもしれません。
##Facebook
同様にFacebookやTwitterもドキュメントにそって設定していけば、ウィジェットに勝手にボタンが追加されます。
Facebook側もDeveloperの様子がAuth0のドキュメントの作成時と変わっているところがあるようですが、コンソールのレイアウトが変わったというくらいです。
フローとしてはGoogleと同じで、privacy policyのURLなどを入力 > Client IDとSecretを取得 > Auth0に入力してSAVE > Applicationsで使うAppを選択
となります。
この登録がAuth0のコンソールでうまく行くと、ウィジェットの様子が自動的に変わります(ブラウザをリロードしてください)。
佳き哉。
いったん設定できてしまえば、Auth0のコンソールをポチポチするだけでウィジェットが反応してくれるというのがうれしいですよね!
#Unityからアクセスしてみる
ここまでのログインのフローをwebベースで頑張ってきた理由は、まさに上で見たようなソーシャルでのログインのフローの特徴によるところが大きかったです。
というのも、アカウント連携の同意を取るために、やたらとリダイレクトされるんですね。
Unityのアプリ側でこういうフローを追っていくのは、かなり大変だと思います(実は大変じゃないのかもしれないけど、素人目には大変そう)。
ここからは、いよいよUnityでこのログインページにアクセスしてみます。
まず、index.pug
とcallback.pug
のstyleを調整しておきます。
ここまでは一般的なブラウザで見たときに心地よい感じにしてありましたが、UnityのUI内に埋め込むブラウザは小さいので、上下のmarginを40px取っていたところを0にしておきましょう。
div#root {
margin: auto;
padding: 0px;
box-sizing: border-box;
}
div#button-area {
width: 320px;
margin: auto;
padding: 0px;
box-sizing: border-box;
}
修正するファイルは別々ですが、それぞれ上記のようなstyleにしておきます。
lockのデフォルトのCSSは、横幅481pxをブレークポイントとするレスポンシブなので、埋め込みブラウザの横幅は482pxとしましょう。
##UnityのEmbedded Browser
Unityで使えるブラウザはいくつかあるようですが、個人的にいいと思ったのは、
https://assetstore.unity.com/packages/tools/gui/embedded-browser-55459
こちらです。ChromeのコアであるChromiumベースで作られていて、実際の使用感もいい感じです。
ただ、弱点もあります。以下ではPCゲームっぽい想定で話を進めていますが、embedded browser自体がPC向けで、Android/iOSに対応していません。
(macOS buildに対応しているのは、逆にembedded browserくらいだ、というコメントも見受けられましたが…)
スマホ向けを含めて検討されている方は、他の選択肢も探されてみてください。
「UNITYでのWEBVIEW(埋め込みブラウザ)について」
「【Webview】unity-webview / UniWebView3」
さて、そのembedded browserですが、
Unity > AssetStore > "embedded browser"で検索
という流れで購入、importしてください。
Embedded BrowserにはいくつかのPrefabが含まれていますが、今回使うのはGUI用のPrefabです。
Hierarchy window
で右クリック > UI > Panel
を行い、UI用のパネルを1枚作成します。
すると、同時にCanvasとEventSystemがついてきます(この辺りはお好みで。単にCanvasだけ作っても問題ありません)。
このCanvas > Panel
の下に、GUI用のBrowser Prefabをドラッグ&ドロップしてください。
とりあえず右端のほうにログインフォームを持っていきましょう。hierarchyでBrowserオブジェクトを選択し、inspectorで下のように設定します。
Browser(Script)
のUrl
には、これまでに構築してきたログインウィジェットのURLを入れてください。
ここでゲームを実行してみます。すると…
どーん。
はい、ログインウィジェットが出ました!
試しに、mail/passwordでログインしてみます。その結果がこちら。
うーん、位置がいまいちですが、「StartGame」ボタンが出ました!
押してみましょう。
ちゃんとhello world!が表示されました!
ちなみに、他のIdPを選ぶとこんな風にリダイレクトされます。
これこれ、こういうリダイレクトがブラウザベースなら簡単に取り回せるので、Unityに埋め込んだときにもうれしいですよね。
Panelにゲームのランチャー風の画像を置いてやれば、かなり雰囲気が出てくるのではないでしょうか。
##シーン切り替えに挑戦
シーンの切り替えも、画面やBGMのFadeout/Fadeinなど、本来ならいくつか気をつかいたいポイントはあります。
また、実際にランチャーモードでログイン > ゲーム本編は全画面
といった作りもあり得るので、windowの制御も入れたいかもしれません。
ですが、今回は話をシンプルにして、単純にシーンのスイッチ指示がjavascript経由で出せればOKとしてやってみます。
embedded browserで読み込んだjavascriptとunity本体のC#の連携については、こちらの記事などで解説されているのを見つけました。
「Unity・VR内でブラウザを利用するメモ」
「Embedded Browserの公式ドキュメント」
先に、シーン転換のトリガーとなるjsのほうを書いてしまいます。
ここではonclick
で指定されたイベントを取っているだけなので、実際の中身はconsole.logにしていますが、引数をUnity側に渡すこともできます。
html
head
meta(name='viewport', content='width=device-width, initial-scale=1')
meta(http-equiv='Content-Type', content='text/html; charset=UTF-8')
style.
input {
width: 16rem;
height: 4.5rem;
margin: 0 auto;
background-color: #33cccc;
color: #fff;
font-size: 20px;
}
div#button-area {
width: 320px;
margin: auto;
padding: 0px;
box-sizing: border-box;
}
script(type='text/javascript').
function startGame(){
console.log('clicked!');
}
body
div#button-area
input(type='button', value='StartGame', onclick="startGame()")
listener側は、browserオブジェクトにStartGameListener.cs
をアタッチしておきます。
using UnityEngine;
using UnityEngine.SceneManagement;
using ZenFulcrum.EmbeddedBrowser;
[RequireComponent(typeof(Browser))]
public class StartGameListener : MonoBehaviour
{
private Browser browser;
// Start is called before the first frame update
void Start()
{
browser = GetComponent<Browser>();
}
// Update is called once per frame
void Update()
{
browser = GetComponent<Browser>();
browser.RegisterFunction("startGame", arg => GameStarter());
}
private void GameStarter()
{
browser = GetComponent<Browser>();
browser.gameObject.SetActive(false);
Destroy(browser.gameObject);
SceneManager.LoadScene("Scene2", LoadSceneMode.Single);
}
}
何もこんなにしつこくbrowserオブジェクトを消そうとしなくてもいいと思うのですが、なんとなく放っておくと無駄にメモリーを食うんじゃないか(というイメージ)があったので、消してみました。
実際にシーン移動をしてみるとわかりますが、次のシーンにいっても「DontDestroyOnLoad」というオブジェクトが残ります。
これはeditorのplaymodeなどの終了時に、embedded browserを確実に終了させるためのオマケだそうですので、神経質に消そうとしなくてもよさそうです。
###「Scene2」を作って動作確認
Scene2を作ったら、build settingsでちゃんとbuild対象にしておくようにしましょう。
ここでは、イラスト1枚ですが、こんなシーンにしてみました。
一部にモザイクをかけた上にgif化したら、画質がえらいことになっていますが、ログインして、StartGameを押すと、次のシーンに行けるようになったことが確認できると思います。
やったね!
#本当はやるべきこと
今回の記事で欠けているのは、
- ログアウトする機能
- Auth0のAPIの権限調整(APIのscopeを制限すること/Rulesの設定など)
- access_tokenのexpire_inの適切な設定を考えること(どれくらい生かしておくか?)
- tokenの取り回しが本当にセキュアにできているか(というか余計なところで気づかぬうちにストアされたりしていないか)、ちゃんとチェックしたかった
- 欲を言えば、S3へのアクセスなどAWSリソースの権限(IAM)を取り回すところまでやりたかった
などなどですが、とくにログアウトの仕方を考えていて、Auth0のこちらのドキュメントを見つけました。
「Node.js」
こっちをベースにログインその他も作ったほうが早くてスマートだったんじゃないかと思ったり…。
また実験してみます。
ひとまず、Qiita初投稿は、これにて終了です!
ご静聴ありがとうございましたm(_ _;)m