#はじめに
これは、SORACOM Technology Camp 2018のナイトイベントLTで発表した内容を、ハンズオン風味で追体験が出来るように記事化したものです。
【スライド】 もういい加減、パスワード使うの止めにしません?
本作品は、 SIM付きボタン(SORACOM LTE-M Button)を使って、簡単かつ安全なパスワードレス認証の可能性を実証したものです。
ボタンを押すだけで、セキュアにログインすることが可能なソリューションとなっています。
その特徴は次の通りです。
① 簡単: 箱から開けて電池を入れるだけで動作開始
② 安全: SIMそのものが持つ仕組み(耐ダンパー性)を使って認証するため、非常にセキュア
具体的には、一つ目の認証キーとしてユニークなIDとなるお客様番号を、二つ目の認証キーとして(あの)ボタンを使っています。
一旦認証が完了した後のページ間のやり取りには、ハッシュ化したお客様番号に紐付くワンタイムのGUIDが振り出され、それをURLのクエリとして用いることで、更なるセキュリティを担保しています。
またログイン用のWebページは、ログイン認証からログイン後のメニューまでを同一ページ内で動的に生成しているため、基本的にフィッシングの被害に合いようがありません。
それを実際に複数のブラウザで試してみた様子がコチラです。
【動画】 多要素認証のキーとして、LTE-Mボタンを使ってみた(音声は流れません)
#今回使ったもの
- SORACOM LTE-M Button powered by AWS
- Visual Studio 2017
#C#でLambdaを書くために必要なこと
C#でLambdaを扱ったことがない方は、事前に準備が必要です。
こちらに解りやすい記事がありますので、先に一通り実践されることをオススメします。
#AWSでやること
###Lambda関数を作る
AWSマネジメントコンソールから「Lambda」を選択して、左側ペインから①の「関数」を選び、②の「関数の作成」ボタンを押します。
次の画面に遷移しますので、③の「一から作成」を選択し、④に適当な名前(当記事では"button-login")を入力、⑤のランタイムで「.NET Core 2.1 (C#/PowerShell)」を選択、⑥のロールではドロップダウンボタンを押してリストを表示させた後で、⑦で「カスタムロールの作成」を選択します。
すると次の画面が別のタブとして開きますので、IAM ロールが「lambda_basic_execution」となっていることを確認して、⑧の「許可」ボタンを押します。
⑨名前、⑩ランタイム、⑪ロール、⑫既存のロール、が次のように全て埋まっていることを確認して、⑬の「関数の作成」ボタンを押します。
次の画面のように、正常に作成されましたのメッセージが表示されたら、関数の作成は無事完了です。
###あのボタンを登録する
AWSマネジメントコンソールから「IoT 1-Click」を選択して、①の左側ペインから「オンボード」を選び、②の「デバイスの登録」ボタンを押します。
③の「登録コードまたは登録するデバイスの ID」にあのボタンのDSN(DeviceSerialNumber)を入力して、④の「登録」ボタンを押します。
ちなみにDSNは、あのボタンのリアカバーを開けると確認できます。
次の画面が表示されたら、あのボタンに電池を入れて一回クリックします。
10秒程で次の画面に変わりますので、⑤の「完了」ボタンを押します。
すると次の画面(管理 → デバイス)に自動的に戻りますので、設定したデバイスIDの列中にある⑥「・・・」をクリックします。
ちなみに「デバイスリージョン」はAWS側で自動的に割り振られるため、任意で指定することは出来ません。
これで、あのボタンの登録が完了しました。
画面に表示されているデバイスIDは、後でSORACOMユーザーコンソールからデバイス登録する時に使いますので、控えておいてください。
###プロジェクトを作る
左側のペインから、①の「管理 → プロジェクト」を選び、②の「プロジェクトの作成」ボタンを押します。
③に任意の「プロジェクト名」(当記事では"login-portal")を入力して、④の「次へ」ボタンを押します。
画面が遷移すると、オレンジ色の文字で名前が必要ですと表示されていますので、⑦に任意の「デバイステンプレート名」を入力、⑧のアクションに「Lambda関数の選択」を選択、⑨のAWSリージョンはLambda関数を作るの項で設定したリージョン(当記事では"東京")を選択、⑩で同じくその時に設定したLambda関数の名前(当記事では"button-login")を入力します。
⑪の属性の名前は「customer_code」を入力、⑫のデフォルト値に「0」を入力して、⑬の「プロジェクトの作成」ボタンを押します。
###プレイスメントを作る
プレイスメントとは、ボタン毎に固有に設定できるタグのようなものです。
今回は、ここに生のお客様番号を設定します。
AWSマネジメントコンソールから「IoT 1-Click」を選択して、左側のペインから①の「管理 → プロジェクト」を選び、②の名前(当記事では"login-portal")部分をクリックします。
次の画面に替わりましたら、③の「プレイスメント」を選択し、④の「プレイスメントの作成」ボタンを押します。
更に画面が替わりますので、必要な項目を入力していきます。
まずは⑤に適当な「デバイスのプレイスメント名」を入力します。
次に⑥の「デバイスの選択」をクリックすると、先程登録したあのボタンのDSNが現れますので、その右側にある「選択」をクリックします。
そして⑦の「customer_code」にはデフォルト値の「0」が設定されていますので、適当なお客様番号(最大9桁まで)を入力して上書きします。お客様番号はこの後何度も使いますので、とりあえず覚えやすい番号(当記事では"100")にしておくことをオススメします。
全ての入力が問題なく完了しますと、⑧の「プレイスメントの作成」ボタンが押せるようになりますので、そのままクリックします。
#SORACOMでやること
以下の手順を端折ったままの状態で1500回クリックしたボタンは見事に文鎮化しますので、末永く使いたい方は要注意です。
####デバイスの登録
SORACOMユーザーコンソールから、画面左上にある「Menu」ボタンを押して「ガジェット → Buttons」を選択します。
すると次の画面が表示されますので、①の「+デバイス登録」ボタンを押します。
ウィンドウが開きますので、既に控えてあるデバイスIDを②の「製造番号」に入力して、③の「登録」ボタンを押します。
次の画面のように、該当のデバイスIDが追加されていたら成功です。
#Visual Studio でやること
ここからは AWS Toolkit for Visual Studio のインストールが完了していることが前提ですので、ご注意ください。
詳しくは、次の記事をご参照ください。
###Visual Studio プロジェクトの作成
Lambda関数をデプロイするための「新しいプロジェクト」を「ファイル → 新規作成 → プロジェクト」の手順で作成します。
①左側のペインから「Visual C# → AWS Lambda」を選び、②右側のペインで「AWS Lambda Project (.NET core)」を選択、③Visual Studio プロジェクトの名前を設定して(当記事では"AWSLambda1")、④「OK」ボタンを押します。
自動的にNew AWS Lambda C# Project のウィンドウが開きますので、⑤「Empty Function」を選択して、⑥「Finish」ボタンを押します。
###コードの入れ換え
ソリューション エクスプローラーで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
の中身を、まるごと下記のコードと入れ替えます。
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…」を選びます。
すると次のウィンドウが開きますので、「Region:」表示が実際にLambda関数を作ったリージョンと同じかどうかを確認しつつ、①「Function Name:」で作成済みのLambda関数の名前(当記事では"button-login")を選択し、②「Next」ボタンを押します。
③「Role Name:」のドロップダウンボタンを押してリストを表示させ、その中の④「Exisiting IAM Roles → lambda_basic_execution」を選択し、⑤「Upload」ボタンを押します。
完了すると、次の画面が表示されます。
デプロイの成否を確認するには、「Sample Input ペイン」の①に下記のJSON(貼り付ける.js)をコピペして、②の「Invoke」ボタンを押します。
(貼り付けるJSONは ↓ です)
{
"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サイトにアクセスして、実際の挙動を試してみることが出来ます。
※ 大人の(懐の)事情で、初回起動がヒジョーに遅いです <(_ _)>
Webサイトの使い方は、プレイスメントの項で設定したお客様番号を入力してログインボタンを押すだけです。 詳しくは、記事冒頭のYouTube動画をご覧くださいませ
#まとめ
一旦認証が完了した後のページ間のやり取りには、ハッシュ化したお客様番号に紐付くワンタイムのGUIDが振り出され、それをURLのクエリとして用いることで、更なるセキュリティを担保しています。
当記事ではURLを短く表現したいためにSHA-1を用いていますが、強度に問題があるため実利用は非推奨です。
また、AWSとAzure間の接続にはSSL(TLS)を使っていますが、更にVPNを使うなどの配慮が要るかもしれません。
今回の仕組みのキモは、いついかなる時でも一対一でしか繋がらないことをロジックで保証して(強制的に排他的な状況を作り出して)いるところなのですが、当記事は既に長文となってしまいましたので、認証の内部的な仕組み(特にAzureの部分)については、また別の機会に紹介させていただければと思います。
(ツイッターフォローはこちら)
#参考
SORACOM公式:
SORACOM LTE-M Button powered by AWS ユーザーガイド
SORACOM LTE-M Button powered by AWS をソラコムのガジェット管理に登録する
SORACOM LTE-M Button の始め方
SORACOM LTE-M Button powered by AWS を用いた開発 TIPS
SORACOM LTE-M Button powered by AWS をクリックしてSlackに通知する
AWS公式:
AWS IoT 1-Click – Lambda 関数のトリガーにシンプルなデバイスを使用する
その他:
Visual Studio for Mac で AWS Lambda(C#)ファンクションを作成してみた
[AWS Lambda関数をC#で書いて実行するまで]
(https://www.aruse.net/entry/2018/10/18/183346)
↑ これがなければLambda周りでハマっていたと思います。ありがとうございます ;)
SHA2/SHA1/MD5ハッシュ生成(Hash Generator)