多要素認証のキーとして、LTE-Mボタンを使ってみた

#はじめに
これは、SORACOM Technology Camp 2018のナイトイベントLTで発表した内容を、**ハンズオン風味**で追体験が出来るように記事化したものです。

　 [【スライド】　もういい加減、パスワード使うの止めにしません？](https://sway.office.com/aLvw76zMbP4CM4oW?ref=Link&loc=play)

本作品は、 SIM付きボタン(SORACOM LTE-M Button)を使って、簡単かつ安全なパスワードレス認証の可能性を実証したものです。
ボタンを押すだけで、セキュアにログインすることが可能なソリューションとなっています。
その特徴は次の通りです。

　① 簡単: 箱から開けて電池を入れるだけで動作開始
　② 安全: SIMそのものが持つ仕組み(耐ダンパー性)を使って認証するため、非常にセキュア

具体的には、一つ目の認証キーとしてユニークなIDとなる**お客様番号**を、二つ目の認証キーとして(あの)**ボタン**を使っています。
一旦認証が完了した後のページ間のやり取りには、ハッシュ化したお客様番号に紐付くワンタイムのGUIDが振り出され、それをURLのクエリとして用いることで、更なるセキュリティを担保しています。
またログイン用のWebページは、**ログイン認証からログイン後のメニューまでを同一ページ内で動的に生成しているため、基本的にフィッシングの被害に合いようがありません**。

それを実際に複数のブラウザで試してみた様子がコチラです。

　 [【動画】　多要素認証のキーとして、LTE-Mボタンを使ってみた（音声は流れません）](https://youtu.be/j0yQEK7JsTA)
　　[![多要素認証のキーとして、LTE-Mボタンを使ってみた](http://img.youtube.com/vi/j0yQEK7JsTA/0.jpg)](http://www.youtube.com/watch?v=j0yQEK7JsTA)

全体の構成は、次のようになっています。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/0d4843b7-3343-e9a5-465b-349754fd31c3.png" width=90%>



#今回使ったもの
* SORACOM LTE-M Button powered by AWS
* Visual Studio 2017



#C#でLambdaを書くために必要なこと
C#でLambdaを扱ったことがない方は、事前に準備が必要です。
こちらに解りやすい記事がありますので、先に一通り実践されることをオススメします。

　[AWS Lambda関数をC#で書いて実行するまで](https://www.aruse.net/entry/2018/10/18/183346)



#AWSでやること

###Lambda関数を作る

[AWSマネジメントコンソールから「Lambda」を選択](https://ap-northeast-1.console.aws.amazon.com/lambda/home)して、左側ペインから①の「関数」を選び、②の「関数の作成」ボタンを押します。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/a3e50a11-b14b-874a-5b53-d44571673056.png" width=90%>

次の画面に遷移しますので、③の「一から作成」を選択し、④に適当な名前（当記事では"button-login"）を入力、⑤のランタイムで「.NET Core 2.1 (C#/PowerShell)」を選択、⑥のロールではドロップダウンボタンを押してリストを表示させた後で、⑦で「カスタムロールの作成」を選択します。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/5a98b2d9-6a58-6f43-36af-f3851ee0ee0f.png" width=90%>

すると次の画面が別のタブとして開きますので、**IAM ロール**が「lambda_basic_execution」となっていることを確認して、⑧の「許可」ボタンを押します。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/f32c19f7-7e31-0701-f7db-77c31f53072c.png" width=90%>

⑨名前、⑩ランタイム、⑪ロール、⑫既存のロール、が次のように全て埋まっていることを確認して、⑬の「関数の作成」ボタンを押します。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/42548068-a2c0-571f-8b76-88136f28c609.png" width=90%>

次の画面のように、**正常に作成されました**のメッセージが表示されたら、関数の作成は無事完了です。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/03b81396-988b-42e4-673a-4d3b4007630d.png" width=90%>


###あのボタンを登録する

[AWSマネジメントコンソールから「IoT 1-Click」を選択](https://ap-northeast-1.console.aws.amazon.com/iot1click/home)して、①の左側ペインから「オンボード」を選び、②の「デバイスの登録」ボタンを押します。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/ace8f329-4b9d-9308-f44e-3963d202a4bd.png" width=80%>

③の「登録コードまたは登録するデバイスの ID」に**あのボタンのDSN**（DeviceSerialNumber）を入力して、④の「登録」ボタンを押します。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/87034362-d20b-017c-0d83-335bffaca0e4.png" width=50%>

ちなみにDSNは、あのボタンのリアカバーを開けると確認できます。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/69a7f98e-5979-de52-42ce-8cb38bc7c23c.png" width=50%>

次の画面が表示されたら、あのボタンに**電池を入れて一回クリック**します。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/43f6b3e7-48ff-63a8-b81e-76c2e88c9993.png" width=50%>

10秒程で次の画面に変わりますので、⑤の「完了」ボタンを押します。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/f38c04bb-e172-625c-8e78-3622e3832769.png" width=50%>

すると次の画面（管理 → デバイス）に自動的に戻りますので、設定したデバイスIDの列中にある⑥「・・・」をクリックします。
ちなみに「デバイスリージョン」はAWS側で自動的に割り振られるため、任意で指定することは出来ません。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/894aa750-ef04-db9c-8f3f-315d596a0708.png" width=80%>

⑦の「デバイスの**有効**化」を選択します。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/eec64d9e-62f9-a1b7-e46f-92a8665fabe7.png" width=80%>

これで、あのボタンの登録が完了しました。
画面に表示されている**デバイスID**は、後でSORACOMユーザーコンソールからデバイス登録する時に使いますので、控えておいてください。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/78960ff1-6d6f-6658-7588-34c76a31f7cc.png" width=80%>


###プロジェクトを作る
左側のペインから、①の「管理 → プロジェクト」を選び、②の「プロジェクトの作成」ボタンを押します。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/953c9100-09a9-0f2d-9915-c3e421524b2b.png" width=70%>

③に任意の「プロジェクト名」（当記事では"login-portal"）を入力して、④の「次へ」ボタンを押します。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/bef108d8-7f7a-e5db-bdbd-d3723e63f10b.png" width=70%>

次の画面に替わりましたら、⑤の「開始」をクリックします。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/b8ba4d79-1a1c-cb26-304f-c17822441c6c.png" width=70%>

⑥の「すべてのボタンタイプ」をクリックします。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/4a9faa54-ed68-d83c-d38a-06970fa18d13.png" width=70%>

画面が遷移すると、オレンジ色の文字で**名前が必要です**と表示されていますので、⑦に任意の「デバイステンプレート名」を入力、⑧のアクションに「Lambda関数の選択」を選択、⑨のAWSリージョンは**Lambda関数を作る**の項で設定したリージョン（当記事では"東京"）を選択、⑩で同じくその時に設定したLambda関数の名前（当記事では"button-login"）を入力します。
⑪の属性の名前は「customer_code」を入力、⑫のデフォルト値に「0」を入力して、⑬の「プロジェクトの作成」ボタンを押します。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/e1e00ee8-0a13-a8ae-48c4-8324bb05536e.png" width=70%>

次の画面が表示されたら、プロジェクトの作成は成功です。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/71a91338-1de6-f3ed-d661-83e1baf500f6.png" width=45%>


###プレイスメントを作る

プレイスメントとは、ボタン毎に固有に設定できるタグのようなものです。
今回は、ここに**生のお客様番号**を設定します。

[AWSマネジメントコンソールから「IoT 1-Click」を選択](https://ap-northeast-1.console.aws.amazon.com/iot1click/home)して、左側のペインから①の「管理 → プロジェクト」を選び、②の名前（当記事では"login-portal"）部分をクリックします。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/158c2a0a-9218-8c21-a3aa-45f460542310.png" width=70%>

次の画面に替わりましたら、③の「プレイスメント」を選択し、④の「プレイスメントの作成」ボタンを押します。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/17c3927c-5291-e050-bcef-429059ca212b.png" width=70%>

更に画面が替わりますので、必要な項目を入力していきます。

まずは⑤に適当な「デバイスのプレイスメント名」を入力します。
次に⑥の「デバイスの選択」をクリックすると、先程登録した**あのボタンのDSN**が現れますので、その右側にある「選択」をクリックします。
そして⑦の「customer_code」にはデフォルト値の「0」が設定されていますので、適当な**お客様番号**（最大９桁まで）を入力して上書きします。お客様番号はこの後何度も使いますので、とりあえず覚えやすい番号（当記事では"100"）にしておくことをオススメします。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/54db04bc-ae85-caee-a557-ee10bcde8b3f.png" width=70%>

全ての入力が問題なく完了しますと、⑧の「プレイスメントの作成」ボタンが押せるようになりますので、そのままクリックします。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/800ae05a-adb9-8082-65fd-2d051317a84e.png" width=70%>

次の画面が表示されたら、プレイスメントの作成は成功です。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/f4f93202-a1c1-c34e-c067-c49d0eddab70.png" width=45%>



#SORACOMでやること
以下の手順を端折ったままの状態で**1500回クリックしたボタンは見事に文鎮化**しますので、末永く使いたい方は要注意です。

####デバイスの登録
[SORACOMユーザーコンソール](https://console.soracom.io/)から、画面左上にある「Menu」ボタンを押して「ガジェット → Buttons」を選択します。
すると次の画面が表示されますので、①の「＋デバイス登録」ボタンを押します。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/b79a4b52-f8c6-7cce-ef5b-67ebe8ea6842.png" width=70%>

ウィンドウが開きますので、既に控えてある**デバイスID**を②の「製造番号」に入力して、③の「登録」ボタンを押します。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/6a12d6d0-486a-2499-3c2b-4e1aa1813a4e.png" width=50%>

次の画面のように、該当のデバイスIDが追加されていたら成功です。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/ad21ed27-8966-4d99-c3e0-dc38ab186cdc.png" width=70%>



#Visual Studio でやること

ここからは **AWS Toolkit for Visual Studio** のインストールが完了していることが前提ですので、ご注意ください。
詳しくは、次の記事をご参照ください。

　[AWS Lambda関数をC#で書いて実行するまで](https://www.aruse.net/entry/2018/10/18/183346)

###Visual Studio プロジェクトの作成
Lambda関数をデプロイするための「新しいプロジェクト」を「ファイル → 新規作成 → プロジェクト」の手順で作成します。
①左側のペインから「Visual C# → AWS Lambda」を選び、②右側のペインで「AWS Lambda Project (.NET core)」を選択、③**Visual Studio プロジェクトの名前**を設定して（当記事では"AWSLambda1"）、④「OK」ボタンを押します。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/51e4afdd-2432-86cf-318e-e3f13c6ed7fa.png" width=90%>

自動的に**New AWS Lambda C# Project** のウィンドウが開きますので、⑤「Empty Function」を選択して、⑥「Finish」ボタンを押します。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/27481f7f-f1d8-a747-a225-e80066a74b46.png" width=90%>


###コードの入れ換え
ソリューション エクスプローラーで`Function.cs`を開くと、次のコードが自動的に生成されているのが確認できます。

```Function.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using Amazon.Lambda.Core;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]

namespace AWSLambda1
{
    public class Function
    {
        
        /// <summary>
        /// A simple function that takes a string and does a ToUpper
        /// </summary>
        /// <param name="input"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public string FunctionHandler(string input, ILambdaContext context)
        {
            return input?.ToUpper();
        }
    }
}
```

上記のコード`Function.cs`の中身を、まるごと下記のコードと入れ替えます。

```Function.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Net.Http;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

using Amazon.Lambda.Core;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]

namespace AWSLambda1
{
	public class Function
	{
		private static HttpClient client = new HttpClient();

		/// Lambdaから呼ばれるメソッド
		public string FunctionHandler(JObject request, ILambdaContext context)
		{
			bool isSuccess = false;
			int customerCode = (int)(request["placementInfo"]["attributes"]["customer_code"]);	// 予め placementInfo → attributes → customer_code に設定済みの「お客様番号」をHash化
			// Azure上にある別のWebサービス(Login Portal)にアクセス
			string hashCode = GetHashCode(customerCode);
			string url = "https://loginto.azurewebsites.net/ThatButton/" + hashCode;
			string json = client.GetStringAsync(url).Result;
			// 応答(Json)の解析
			var response = JsonConvert.DeserializeObject<Dictionary<string, string>>(json);		// 単純な構造のため、KeyValue(Dictionary型)で応答をデシリアライズ
			if (response.ContainsKey("isSuccess"))												// 成否判定用のステータス"isSuccess"が(無事に)含まれているか？
			{
				isSuccess = Convert.ToBoolean(response["isSuccess"]);
			}
			return isSuccess.ToString();														// ログインの成否 (成功="True" or 失敗="False")
		}

		/// 引数をSHA-1でHash化して返すメソッド
		private string GetHashCode(int customerCode)
		{
			System.Security.Cryptography.HashAlgorithm ha1 = new System.Security.Cryptography.SHA1CryptoServiceProvider();
			byte[] hash = ha1.ComputeHash(Encoding.UTF8.GetBytes(customerCode.ToString()));
			return BitConverter.ToString(hash);
		}
	}
}
```

このコードでは、既にプレイスメントで設定済みの**お客様番号**`customer_code`の数字を大文字でSHA-1ハッシュ化します。次にその文字列を、`loginto.azurewebsites.net/ThatButton/`の後にパスの一部として付加してからGETします。
例えば、`customer_code`に「100」を設定している場合のURLは次の通りです。

　https://loginto.azurewebsites.net/ThatButton/310B86E0B62B828562FC91C7BE5380A992B2786A



###デプロイ

さて、`Function.cs`の入れ替えが完了しましたら、いよいよLambdaへデプロイしてみます。
ソリューション エクスプローラーでプロジェクトの名前を右クリックして、「Publish to AWS Lambda…」を選びます。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/0c5d0487-da44-5e3a-3889-fa3535b0b646.png" width=50%>

すると次のウィンドウが開きますので、「Region:」表示が実際に**Lambda関数を作ったリージョンと同じ**かどうかを確認しつつ、①「Function Name:」で**作成済みのLambda関数の名前**（当記事では"button-login"）を選択し、②「Next」ボタンを押します。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/7aa7216a-a427-b7e4-06f9-0d0e1ffbd2eb.png" width=75%>

③「Role Name:」のドロップダウンボタンを押してリストを表示させ、その中の④「Exisiting IAM Roles → **lambda_basic_execution**」を選択し、⑤「Upload」ボタンを押します。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/d1e1e471-be90-eb42-2cb6-2a2b208539a6.png" width=75%>
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/56eba39f-3603-37b0-1f36-6ebb637c3311.png" width=95%>

しばし待ちます。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/b33b25a0-cae0-d45d-ee60-18f102bb1c13.png" width=40%>

完了すると、次の画面が表示されます。
デプロイの成否を確認するには、「Sample Input ペイン」の①に下記のJSON（貼り付ける.js）をコピペして、②の「Invoke」ボタンを押します。
　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/e49b0159-7b3f-4a06-6bcd-149351d40c08.png" width=95%>
（貼り付けるJSONは ↓ です）

```貼り付ける.js
{ 
    "deviceInfo": { 
        "deviceId": "XXXXXXXXXXXXXXXX", 
        "type": "button", 
        "remainingLife": 99.933334, 
        "attributes": { 
            "projectRegion": "ap-northeast-1", 
            "projectName": "projectName", 
            "placementName": "placementName", 
            "deviceTemplateName": "deviceTemplateName" 
        } 
    }, 
    "deviceEvent": { 
        "buttonClicked": { 
            "clickType": "SINGLE", 
            "reportedTime": "2018-11-05T04:29:01.950Z" 
        } 
    }, 

    "placementInfo": { 
        "projectName": "projectName", 
        "placementName": "ButtonName", 
        "attributes": { 
            "customer_code": "100" 
        }, 
        "devices": { 
            "deviceTemplateName": "XXXXXXXXXXXXXXXX" 
        } 
    } 
} 
 ```

その結果、③の「Responseペイン」に **"False"** が帰ってきたら成功です。
ちなみに初回の起動時のみ、Lambda関数が動き出すまで若干の待ち時間があります。

※ もし、エラーメッセージ "errorMessage": "Unable to load type …" が返ってくる場合は、Visual Studio プロジェクトの名前とソースコードの`namespace `が同じかどうかを確認してみてください。



#試してみる！
ここまで問題なく完了しましたら、次の[Webサイト](https://loginto.azurewebsites.net/)にアクセスして、実際の挙動を試してみることが出来ます。
※ 大人の(懐の)事情で、初回起動がヒジョーに遅いです <(_ _)>

　 [ログインポータル 実験サイト](https://loginto.azurewebsites.net/)　　　<img src="https://qiita-image-store.s3.amazonaws.com/0/318815/e3f01d69-0f20-5e80-a574-4f0bccee16b9.png" width=15%>

Webサイトの使い方は、プレイスメントの項で設定した**お客様番号**を入力して**ログイン**ボタンを押すだけです。 詳しくは、記事冒頭のYouTube動画をご覧くださいませ :grin:



#まとめ
一旦認証が完了した後のページ間のやり取りには、ハッシュ化したお客様番号に紐付くワンタイムのGUIDが振り出され、それをURLのクエリとして用いることで、更なるセキュリティを担保しています。
当記事ではURLを短く表現したいためにSHA-1を用いていますが、強度に問題があるため実利用は非推奨です。
また、AWSとAzure間の接続にはSSL(TLS)を使っていますが、更にVPNを使うなどの配慮が要るかもしれません。

今回の仕組みのキモは、いついかなる時でも**一対一でしか繋がらないことをロジックで保証**して（強制的に排他的な状況を作り出して）いるところなのですが、当記事は既に長文となってしまいましたので、認証の内部的な仕組み（特にAzureの部分）については、また別の機会に紹介させていただければと思います。

（ツイッターフォローは<a href="https://twitter.com/SAMonodera?ref_src=twsrc%5Etfw" class="twitter-follow-button" data-show-count="false">**こちら**</a><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>）


#参考

SORACOM公式：
　[SORACOM LTE-M Button powered by AWS ユーザーガイド](https://dev.soracom.io/jp/aws_button/what-is-aws-button/)
　[SORACOM LTE-M Button powered by AWS をソラコムのガジェット管理に登録する](https://dev.soracom.io/jp/start/aws_button_registration/)
　[SORACOM LTE-M Button の始め方](https://www.slideshare.net/SORACOM/soracom-technology-camp-2018-4-soracom-ltem-button)
　[SORACOM LTE-M Button powered by AWS を用いた開発 TIPS](https://blog.soracom.jp/blog/2018/11/07/tips-of-soracom-lte-m-button/)
　[SORACOM LTE-M Button powered by AWS をクリックしてSlackに通知する](https://dev.soracom.io/jp/start/aws_button_slack/)

AWS公式：
　[AWS IoT 1-Click – Lambda 関数のトリガーにシンプルなデバイスを使用する](https://aws.amazon.com/jp/blogs/news/aws-iot-1-click-use-simple-devices-to-trigger-lambda-functions/)

その他：
　[Visual Studio for Mac で AWS Lambda（C#）ファンクションを作成してみた](https://dev.classmethod.jp/cloud/visual-studio-for-mac-lambda-csharp/)
　[AWS Lambda関数をC#で書いて実行するまで]
(https://www.aruse.net/entry/2018/10/18/183346)
　 ↑ これがなければLambda周りでハマっていたと思います。ありがとうございます ;)
　[SHA2/SHA1/MD5ハッシュ生成(Hash Generator)](https://tool-taro.com/hash/)