Hubsの拡張 別WEBサイトとのログイン連携

# hubsの別WEBサイトとのログイン連携
### やりたいこと
Hubs（主にReticulum）側を改造して、
既存の自社サイトとログイン連携させたい。

そして出来れば既存サイト側はあまり触らないこと。

# 実際の動きと、実装方針
登壇したときに使った資料があるので、下記にまとめてます
https://docs.google.com/presentation/d/1_v3fJRdMhr35paaPXGfI1xcSbkQj-NDA45Qdf-M3EOw/edit#slide=id.gc6f80d1ff_0_5


#実装
### DB拡張する
接続方法はいろいろあるが
AWSのWebコンソールのQuery EditorからSQLを叩くのが手っ取り早い。

#### まずは接続する為の必要情報を探す。
DB名とシークレットのARNが必要。
DB名はAWSのRDS > データベース > 該当のDB > 設定から拾う↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504562/26a6737a-a6be-cf17-9b62-9e8eb9f4dad9.png)


シークレットのARNは
AWS Secrets Manager > シークレット > 該当のシークレット から拾う↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504562/21fbb92a-7685-5f9e-7b77-a2d272a24f4b.png)

#### Query Editorを使用できる様にDB設定を変更
RDS > データベース > 該当のデータベース で右上にあるボタン「変更」から
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504562/1b2be581-5bed-7d5a-300a-79bbd4cd2c18.png)

画面中央あたりにある接続の
Data APIにチェックをつけて反映させる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504562/34820d9b-a8d5-9b95-0aaa-4b2d96d8ef69.png)


これで接続出来る状態になったので
RDS > Query Editorを開いて
先ほどの情報を入力していく
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504562/12612c79-48f5-6cfb-f75c-fbb7f636bc50.png)

すると接続出来た↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504562/fc0bab96-edb5-f455-bf36-6ec0260b9858.png)

#### SQLでテーブル作成
エディタの個所にPOSTGRESのSQLを叩けばDDLも効くし、
基本何でもいける。
今回は下記のCREATE文でワンタイムトークン管理用のテーブルを追加する。

```
create table onetimepass (
  id serial not null,
  onetimepass varchar(80),
  used boolean,![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504562/1e29161b-068c-16d0-7747-e21c87343fe2.png)

  created_at timestamp,
  updated_at timestamp,
  primary key (id)
);
```

これでDB側の準備はOK。

### Lambda & API GatewayでAPI作成して、RDSにSQLを実施
やりたいことは、
先ほどのRDBとやり取りするAPIの作成。
とにかくLambdaを使うのから初めてなので手探りでやってく。

#### まずはLambdaで関数構築（要RDBに接続出来るネットワーク設定）
Webコンソールで
サービス： Lambda　＞ ナビゲーションメニュー：関数　＞　画面右上：関数の作成
で関数作成画面へ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504562/698ef7ec-83bb-aa2e-aab5-f3bda2f51c6f.png)

「一から作成」を選択して、適当な関数名を付けて、
ランタイムは何でも良いと思うが今回は公式チュートリアルで使われていたPythonを使用。
バージョンはHubsの既存のLambdaに合わせて3.6にしておいた。

ロールについては、前もってIAMで作成したものを適用。
こちら↓はIAM画面でその内容を表示したものだが、一応RDBにアクセスできるように
それっぽいポリシーをアタッチしておいた。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504562/7658ada9-ef4d-7603-1155-276abf193da5.png)
＜追加したポリシー＞
・AmazonRDSFullAccess
・AmazonRDSDataFullAccess
・AWSLambdaVPCAccessExecutionRole

で先ほどのLambda作成画面にて上記で作成したロールに設定↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504562/d450a7aa-0a5d-a1f3-fa75-d0d058d0e20d.png)


さらに、当該画面の下の方にいくと「詳細設定」というのがあり、
VPC、サブネット、セキュリティグループといったネットワーク周りの設定をする必要がある。
ここで結構はまった。
ネットワークを適切に設定してやらないと作ったLambdaがRDBに接続出来ないので大変。。
でも、よく考えたら既に稼働しているEC2（アプリの方）がガンガンDBに接続している感じだったので、
その設定を作成するLambdaに適用してやれば良いだけだ。

ちなみにHubsのアーキテクチャはこんな感じ↓by 公式ドキュメント
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504562/bd09e1c5-2577-ca63-086d-7616e85be547.png)
詳しくは公式ページ参照↓
https://hubs.mozilla.com/docs/hubs-cloud-aws-architecture.html

で、つまりは下記の様に設定する
VPC⇒　他のEC2やRDSと同じもの
サブネット⇒　publicになっている既存サブネットを2つ指定
セキュリティグループ⇒appとstreamを担当しているEC2と同様のものを指定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504562/12edd96b-7ece-1c54-756f-6ab995b5e204.png)


これで画面下部のボタン「関数を作成」を押下
そしたら一旦Lambdaの出来上がり。

#### 先ほど作った関数をAPI Gatewayで「API化
上記で作成した関数画面の左上部にあるボタン「トリガーを作成」を押下
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504562/045fefb1-e8b8-edb8-188f-a659aaa1b5fe.png)

一番上のプルダウンでAPI Gatewayを選択
その下のプルダウンでは「APIを作成する」を選択。
その下に現れたAPIタイプでは「HTTP API」を選択。
セキュリティは一旦「オープン」で。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504562/348fda58-24b7-7fb3-046a-e4bdc1ad8495.png)

その下部の追加の設定では、
Cross-origin resource sharingにチェック、
詳細なメトリクスの有効化にもチェックを入れる

これでボタン「追加」を押下

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504562/bf7ec722-98e7-954b-4d54-cd5a25b412b9.png)

これで出来たAPI Gatewayのエンドポイントを叩くと、あら、エラー出る（出ずにうまくいくこともあった）。
一旦、このAPI Gateway消して再作成。
すると次はHTTP APIを作成する際に、プルダウンで先ほど作成した名前のHTTP TRIGGERが見えるのでそれを選択すると、今度は上手くいった。
理由不明。

API Gatewayのエンドポイントを叩いたら下記画面でHello　Lambda！と言ってくれるので、
API作成は一旦上手くいったようだ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504562/2321a616-40d8-3a19-4715-20f336cbbb3f.png)

#### お次はLambdaにがりがりコードを書く
合計三本API作ったが、まずここで紹介するのは下記処理を行うAPI
1. トークン発行
2. トークン含んだレコードをRDSのDBに挿入
3. トークン返す

先程のlambda作成時のネットワーク環境設定ではapp担当のEC2と同じ位置にlambdaを配置したので、RDSに接続できる環境にはなっている。

なんで、あとはこのlambdaのコードでホストやらDBやらの接続情報を明記してDBに接続してSQLを書けば良い。

pythonはそんなに書いたことなかったが、postgreのRDBにLambdaから接続してくれていた先人の記事「[AWS-Lambda + PythonでAWS-RDS/PostgreSQLのテーブルを読み込む](https://qiita.com/nsuhara/items/dd780c2622258d10f961)」があったのでpythonコードを少しだけ改変する形で下記の様に実装。


```app/lambda_function.py

import os
import sys

import psycopg2


class Database():
    """Database
    """
    class Parameter():
        """Parameter
        """

        def __init__(self, host, port, dbname, table, user, password, query):
            self.host = host
            self.port = port
            self.dbname = dbname
            self.table = table
            self.user = user
            self.password = password
            self.query = query

    def __init__(self, param):
        self.db = param
        self.header = tuple()
        self.records = list()
        self.counts = int()

    def _connection(self):
        """_connection
        """
        print('connect to db: {}/{}'.format(self.db.host, self.db.dbname))
        return psycopg2.connect(
            host=self.db.host,
            port=self.db.port,
            dbname=self.db.dbname,
            user=self.db.user,
            password=self.db.password
        )

    def query(self):
        """query
        """
        with self._connection() as conn:
            with conn.cursor() as cursor:
                try:
                    cursor.execute(self.db.query)
                    self.header = cursor.description
                    self.records = cursor.fetchall()
                    self.counts = len(self.records)
                except psycopg2.Error as e:
                    print(e)
                    sys.exit()
        return True


def lambda_handler(event, context):
    """lambda_handler
    """
    print('event: {}'.format(event))
    print('context: {}'.format(context))

    param = Database.Parameter(
        host=os.getenv('DB_HOST', ''),
        port=os.getenv('DB_PORT', ''),
        dbname=os.getenv('DB_DBNAME', ''),
        table=os.getenv('DB_TABLE', ''),
        user=os.getenv('DB_USER', ''),
        password=os.getenv('DB_PASSWORD', ''),
        query=os.getenv('DB_QUERY', '')
    )

    db = Database(param=param)
    db.query()

    return {
        'status_code': 200,
        'records': str(db.records),
        'counts': db.counts
    }


if __name__ == '__main__':
    print(lambda_handler(event=None, context=None))

```

そしてコードから読み込むDB情報は同画面中央あたりで設定が可能で、下記の様に設定した。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504562/85a37a07-391a-723b-32f2-a73604e00aa0.png)

ちなみにDB情報は前にも触ったSecret Manager > 該当のシークレット　の画面中央あたりにある
「シークレットの値」という欄から一式確認可能↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504562/9813f655-cb7b-eb5a-30d6-3e79014464e8.png)


しかしこれでコードをテスト実行すると、
コード序盤のimportでpostgresqlのアダプターのpsycopg2の読み込みに失敗する。

それを打破する記事は[AWS Lambdaでpsycopg2を使う](https://qiita.com/takayuki_tk/items/76479e86286d4a0f1029)にあった。

今回はここのgithubのものを利用した
https://github.com/jkehler/awslambda-psycopg2

ちなみにローカルでVSCode使ってコード触りたかったので下記の様にして作業した
①Webコンソールから現行ソース取得
　　関数の画面上部のボタン「アクション」＞　関数のエクスポート ＞　デプロイパッケージのzipファイルをダウンロード
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504562/584c840d-05c6-62ce-b45e-e41c4c68d805.png)
②ローカルでソース編集
③Webコンソールの同画面からソースをアップロード
　　今回はこの2ファイルをzipにして
　　![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504562/6d973cab-e529-ee2a-182e-e4e01fddf88c.png)

関数画面の関数コード欄の右部「アクション」からアップロード
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504562/8786cf19-04ac-8252-deef-31f7b220a3f0.png)


あとは当該画面の上部にテストするボタンあるのでやってみると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504562/59d1f6b3-a69b-3178-3918-0f354dc3a6f7.png)

ほーらいい感じ。
#### GatewayからLambdaへのURLパラメータ渡し
別のAPIを作る際、URLパラメータを渡したかった。
特に設定は必要無く、Lambdaのコード側で下記の様にすればURLパラメータを取り出せる。
例えば、「urlparameter」というURLパラメータが欲しい場合はこんな感じで書くと取れる↓
event['queryStringParameters']['urlparameter']

### Hubs内からのAPI呼び出し
Hubsのコードのhub.js内にあるファンクションhandleHubChannelJoinedは、部屋に入ったときにいつも呼ばれるので
このコードの上部でURLパラメータとして渡されたワンタイムトークンと、部屋のIDを取り出して

⓪部屋IDが予め用意した特定のものだったら下記の②～④の処理にかける
①パスワードがマッチしているかAPI呼び出し判定
②OKならcookieにフラグ立て
③Cookieにフラグ立ってなかったらホーム画面に飛ばす。立ってたらそのまま部屋表示する

で実際のコードの一部は下記の通り

```hub.js
function handleHubChannelJoined(entryManager, hubChannel, messageDispatch, data) {
  const scene = document.querySelector("a-scene");
  const isRejoin = NAF.connection.isConnected();

  if (isRejoin) {
    // Slight hack, to ensure correct presence state we need to re-send the entry event
    // on re-join. Ideally this would be updated into the channel socket state but this
    // would require significant changes to the hub channel events and socket management.
    if (scene.is("entered")) {
      hubChannel.sendEnteredEvent();
    }

    // Send complete sync on phoenix re-join.
    NAF.connection.entities.completeSync(null, true);
    return;
  }

  // Turn off NAF for embeds as an optimization, so the user's browser isn't getting slammed
  // with NAF traffic on load.
  if (isEmbed) {
    hubChannel.allowNAFTraffic(false);
  }

  const hub = data.hubs[0];

  // start
  // get url parameter of challenge code
  const roomId = hub.hub_id;
  const challenge = qs.get("challenge")
  const challengeCookieKey = "onetime";
  const lockTargetRoomId = 'QvKfTpC';

  if(roomId == lockTargetRoomId)
  {
    if(challenge)
    {
      var hitCnt = judgeIfMatchPassword();
      //alert('hitCnt: ' + hitCnt);
      if(hitCnt == 1)
      {
        //alert('パスワードマッチ成功したのでクッキーをセット');
        // write cookie
        Cookies.set(challengeCookieKey, '1',  {expires: 1 / 24 / 20}); // limit 3 minuite

        //upUsedFlg('3')
      }
    }

    // auth by cookie
    const cookieAuthValue = Cookies.get(challengeCookieKey);
    if(cookieAuthValue == undefined || cookieAuthValue != 1)
    {
      alert('認証失敗');
      window.location.href = 'https://your-rooms.com/';
    }
    else{
      alert('認証成功');
    }
  }
  // end

  function judgeIfMatchPassword(){
    var URL = 'https://s6dt3yyxzl.execute-api.ap-northeast-1.amazonaws.com/default/get_onetimepass_jedgement?challenge=' + challenge;
    var request = require('sync-request');
    var response = request(
      'GET',
      URL
      );
    
    console.log("Status Code (function) : "+response.statusCode);
    var a = JSON.parse(response.body);
      
    return a.counts;
  }
  
  function upUsedFlg(id){
    var URL = 'https://1d6z4jnx1l.execute-api.ap-northeast-1.amazonaws.com/default/update_used_flg?id=' + id;
    var request = require('sync-request'); // should be async
    var response = request(
      'POST',
      URL
      );
    
    alert("Status Code (function) : "+response.statusCode);
  }
```

〇APIコールは非同期だと結果処理を通りすぎてしまったので同期処理
https://designetwork.daichi703n.com/entry/2016/11/19/sync-request-post-node-js

〇Cookieの読み書きはもともと入ってたnpmパッケージを利用
https://chaika.hatenablog.com/entry/2019/02/05/083000


### マイページサイト側

これが別WEBサイト側のコード

``` door.html
<!DOCTYPE html>
<html>
<head>
<script src="js/jquery-3.5.1.min.js"></script>
<script>
$(document).ready(function(){
    const onetime = getOnetimePassword();
    const hubsRoomsUrl = 'https://your-rooms.com/QvKfTpC/premium-live-2020';
    const completeUrl = hubsRoomsUrl + "?challenge=" + onetime;

    var el = document.createElement("button");
    el.innerText = "GOTO!";
    el.addEventListener('click', event => {
        window.location.href = completeUrl;
    });
    var parent = document.getElementById("parent");
    parent.appendChild(el);
    parent.appendChild(document.createElement("br"));
})

    function getOnetimePassword()
    {
        const remoteUrl = 'https://5qnuawkffl.execute-api.ap-northeast-1.amazonaws.com/default/insert_pass_api';
        const res = callApi(remoteUrl);

        const resParsed = JSON.parse(res);
        return resParsed.hash;
    }

    function callApi(theUrl){
        var result =  $.ajax({
            type: 'GET',
            url: theUrl,
            dataType: "text",
            async: false
        });

        return result.responseText;
    }
</script>
</head>
<body>
    <h1>Premium Live 2020</h1>
    <h2>Thanks for buying!</h2>
    <p><b>Let's GOTO the premium live!!</b></p>
    <p>
        Click the bellow button to go there!
    </p>
    <div id="parent"></div>
</body>
</html>
```

<head>内に差し込んだJSスクリプト35行が、
当該画面ロード時に<div id="parent"></div>の場所に
Hubsページの部屋への入り口となるボタンを生成してくれる。

ちなみにロード時にやっていることはこんな感じ↓

予めlambda + API Gatewayで作っておいたAPIが下記してくれるので
　1. 期限付きトークン発行
　2. トークンをReticulumに保存
　3. トークンを返す

そのトークンをボタンの遷移先URLのURLパラメータとしてセット

以上で完了。

### 制作物
下記のURLで今も動いてます↓
・Hubs↓
https://ds-rooms.com/
※Premium Live 2020 という部屋がそれ。クリックしても認証失敗してホーム画面に飛ばされる

・他WEBサイトページのサンプル
https://testhubs.z31.web.core.windows.net/
※英語表示になってます（サーバーの日本語対応さぼっただけ）
※ここからボタンをクリックして進むと、先ほど入れなかった部屋に入室出来ます
※ds-shopという部屋では、カスタマイズによって決済が出来るページが確認できます（ログイン認証無）
　詳しくはこちらの記事で⇒　https://docs.google.com/presentation/d/1iJsFaUwU6vGS9foSQTm38gLvnW0_DG9ey3n59rwy0mI/edit?usp=sharing

# 余談
DBに接続する際にいくつか方法は試みたので、以下メモとして残しておく。

### EC2への接続方法
RDSに接続する為には、EC2にSSH接続してそれを踏み台にしてからRDSのエンドポイントに接続する必要がある。
直接RDSに接続することはおそらく出来ない。
参考）https://cloud5.jp/windows-amazon-rds-for-postgresql/

まずは下記のものを準備
1. EC2生成時に使用した秘密鍵（拡張子pem）のパスを確認（これはCloud Formationでhubsをデプロイする前に作成しているもの）
2. 接続先のEC2のエンドポイントを確認
　　これはWEBコンソールでEC2を開けば確認可能。
3. Powershellから下記コマンドを打つ
　　ssh -i "NakaDigiYourRooms.pem" ubuntu@[エンドポイント]

※本EC2はUbuntuなのでユーザー名はubuntuであることに注意
※pemファイルの権限は400（ユーザーが読み取りのみ可能）

ここで実はもう一つ必要なのがGoogle Authenticationによる2重認証の通過対応。
いろいろやったがうまくいかなかったので決して良い方法では無いが、
Google Authenticatonを外す対応をした。

本来はちゃんと二重認証を通過すべきなのだから、メモはいろいろ残す。
Google Authenticationは要するに秘密鍵と時間ベースのワンタイムパスワードでの認証。
30秒ごとくらいにどんどんワンタイムパスワードが切られる。
概要の基本は[こちら](https://qiita.com/viptakechan/items/6d19aee635b2ab189e47)
構築手順は[こちら](https://qiita.com/keitakn/items/3a540b4d61146063ad91)
ちなみにこの認証パスワードはアプリをスマホにインストールするのがおそらく王道なのだが、
Windowsアプリケーションも存在しており、それを用いての確認方法は[こちら](https://qiita.com/tsukamoto/items/1019381984b8b4735515)

とりあえず二重認証の外し方もメモっておくと、
まずAWSWEBコンソールで任意のEC2（アプリ側）にセッションマネージャーで接続。
（セッションマネージャーを使用すれば特に認証は必要ない。）

これで仮想マシンに入ったので、
/etc/pam.d/sshd　をviで編集して、下記内容をコメントアウト
前）auth       substack     google-auth
後）# auth       substack     google-auth

あと/etc/ssh/sshd_configも同様に編集して
変更前）
ChallengeResponseAuthentication yes
AuthenticationMethods publickey,keyboard-interactive

を下記の様に
変更後）
ChallengeResponseAuthentication no
'#AuthenticationMethods publickey,keyboard-interactive

これでサーバーからGoogle Authenticationを利用した認証機構は外せたので後は先ほどのssh接続コマンドで接続できることを確認する

### RDBに接続する
RDBのDBはpostgreで出来ている。
今回はpgAdminをインストールして利用。
SSHトンネルでまずは先ほどのEC2に接続して踏み台にし、
RDSに接続する。

先ほどの情報に加えて必要なのはDBのエンドポイントとDB名とユーザー名とパスワード。
エンドポイントのDB名、ユーザー名はAWS　WEBコンソールのRDB、DBインスタンスから簡単に拾える。
パスワードについては、AWSのサービス名のSystem Managerというものをまずは開く
そこでDBのキーの様な名前をしたものをあるので開き、その先の画面中央部分に
「シークレットキーの値」というのがあるので、その右側にあるシークレット「シークレットの値を取得する」というボタンを押下するとパスワードが拾える。

あとはそれらをpgAdminに入力する。