SORACOM Endorseの認証の仕組みを理解する

はじめに

SORACOM Endorseを利用してSIMの認証を外部サーバーの認証と連携するでもアプリケーションを作成し、各ノードの役割についてまとめます。

Endorseは SIMごとにユニークかつ改ざん検知可能トークンの払い出しを受けることができるサービスです。
このトークンを利用することでSIMを利用した通信元を特定し、アプリケーションの認証が可能となります。

構成

CLIだけのデモだけでは味気がないのでAngularでSPA(Single Page Application)を作りました。
SPAをPC上で動作させEndorse Tokenの取得を行い、また同じPC上で動作させているGoのAPIサーバーへのログインを行います。

SORACOM Endorseデモ構成
SORACOM Endorseデモ構成

シーケンスとしては以下の通りです。

  • ①SPAからEndorse Tokenを取得する(Air SIM経由の通信が必要)
  • ②取得したEndorse Tokenを利用してAPIサーバーへ認証要求(Air SIM経由の通信でなくても良い)
  • APIサーバーはSORACOMの公開鍵を取得してEndorse Tokenを検証
  • ④正しいトークンであればAPIサーバーからSPAへ認証OKを返す

それでは環境構築から動作確認までを行なっていきます。

環境構築

SPAとAPIサーバーを動作させるコンテナ環境を準備しました。

github.com

以下のコマンドで SPA: localhost:4200 API Server: localhost:8080 で立ち上げます。

$ git clone https://github.com/naoyamaguchi/endorse-demo.git
$ cd endorse-demo
$ docker-compose up -d

また、SORACOMのコンソール上からグループ設定→Endorseの設定をONにし、そのグループにSIMを紐づけておきます。

SORACOM Endorseコンフィグ
SORACOM Endorseコンフィグ

動作確認

ブラウザから localhost:4200/logintoken へアクセスすると以下のような画面へ遷移します。

SinglePageApplication画面

ブラウザのデベロッパーツールを開き Get Endorse Token ボタンを押します。
するとコンソールに以下のようなEndorse Tokenが表示されます。(後に改ざん検証を行うのでlocalstrageにも保存してあります。)

もし表示されない場合や402が返ってくる場合は、Endorseの設定がONになっているか、SIM経由の通信となっているかを確認してみてください。

このトークンについては以降で解説します。

次に Login ボタンを押すことでログイン完了画面に遷移し、ログインしているデバイスのIMSIとIMEIとMSISDNが表示されたかと思います。

f:id:nananao_dev:20190120095900p:plain

Basic認証のかかったWebサービスのログインを考えると、本人しか知らないID/PWを入力することで認証を行いますが、今回は本人しか持っていない(偽装されることのない)SIMカードを使って認証を行なっています。

トークンについて

このトークンはJSON Web Token(JWT)と呼ばれているフォーマットで、秘密鍵で署名され改ざんの検知が可能となっています。
JWTは2つのピリオドで区切られた3つの領域からなっており [ヘッダー].[ペイロード].[署名] という構造です。
このトークンを https://jwt.io/ のページへ貼り付け中身をみてみます。

jwt

JWT ペイロードについて

予約済クレーム名に加えて soracom-endorse-claim というカスタムクレームがのっています。 カスタムクレームにはIMSIやIMEI、MSISDN(電話番号)などのSIM、デバイス特有のキーがのっているため、送信元を一意に特定することができそうです。

JWT ヘッダーについて

ヘッダーには検証に利用できる公開鍵と暗号化方式がのっています。
kid: v1-f2fea060b93f510bfb722f2cd4b3774e-x509.pem がSORACOMから提供されている公開鍵となっており、この公開鍵を利用することでこのトークンが改ざんされていないかを確認することができます。
具体的にはドキュメントにある通り https://s3-ap-northeast-1.amazonaws.com/soracom-public-keys/ の配下に格納されているようです。
ブラウザからhttps://s3-ap-northeast-1.amazonaws.com/soracom-public-keys/v1-f2fea060b93f510bfb722f2cd4b3774e-x509.pem を開き、公開鍵を https://jwt.io/ の public key と書かれたエリアにコピペすることで改ざん検知が簡単に可能となっています。

SPAからEndorse Tokenを取得する

Get Endorse Token ボタンを押すことで ①SPAからEndorse Tokenを取得するapp/services/endorse.service.ts で実行されています。(エラーハンドリングが皆無。。。)

getEndorseToken(): Observable<EndorseToken>  {
    const endorseUrl = 'https://endorse.soracom.io';
    return this.http.get<EndorseToken>(endorseUrl);
}

コンポーネント app/login-token/login-token.component.ts 側で以下のようにlocalstorageに格納しています。
localstorageへの保存は必須ではありませんが、編集(改ざん)しやすくするために一度保存しています。

getEndorseToken() {
  this.endorseService.getEndorseToken()
  .subscribe(result => {
    console.log('endorseToken: ', result.token);
    localStorage.setItem('endorseToken', result.token);
  });
}

取得したEndorse Tokenを利用してAPIサーバーへ認証要求

Lgoin ボタンを押すことで ②取得したEndorse Tokenを利用してAPIサーバーへ認証要求app/services/auth.service.ts で実行されています。
HttpHeaders の Authorization ヘッダーに先ほど取得したEndorse Tokenを格納します。
API Serverが待ち受けている http://localhost:8080/tokenlogin へhttp.postします。
紛らわしいですがAPI Serverへのログインが完了次第、responseとしてtokenをもらい token としてlocalstrageに格納しています。Endorse Tokenとは別物です。

tokenlogin(endorseToken: string): Observable<boolean> {
  const url = 'http://localhost:8080/tokenlogin';
  const httpOptions = {
    headers: new HttpHeaders({
    'Content-Type': 'application/json',
    'Authorization': endorseToken
    })
  };
  return this.http.post(
    url, JSON.stringify({dummy: 'dummy body message'}), httpOptions
  ).pipe(map(response => {
        const token = response['token'];
        if (token) {
          localStorage.setItem('token', token);
          return true;
        } else {
          return false;
        }
      }),
      catchError(err => {
        return of(false);
      })
    );
}

APIサーバーはSORACOMの公開鍵を取得してEndorse Tokenを検証 & APIサーバーからSPAへ認証OKを返す

API Serverではリクエストヘッダー付与されたEndorse Tokenをパースしkidから公開鍵を取得、検証を行いAPI Serverとしてのトークンを返却することでログインOKとしています。

func tokenlogin(c echo.Context) error {
    // リクエストヘッダーから "Authorization"ヘッダーを取得
    endorseTokenString := c.Request().Header["Authorization"][0]

    // jwtをパース
    endorseToken, _ := jwt.ParseWithClaims(endorseTokenString, &jwtCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
            return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
        }

        // jwtヘッダのkidを取得し公開鍵取得用のURLを組み立てる
        publicKeyURL := "https://s3-ap-northeast-1.amazonaws.com/soracom-public-keys/" + token.Header["kid"].(string)

        // 公開鍵取得用のhttp GET
        response, err := http.Get(publicKeyURL)
        if err != nil {
            return nil, fmt.Errorf("http.Get(publicKeyURL) Error: ", publicKeyURL, err)
        }
        defer response.Body.Close()

        pubkeyData, _ := ioutil.ReadAll(response.Body)
        key, _ := jwt.ParseRSAPublicKeyFromPEM(pubkeyData)

        return key, nil
    })

    // 取得した公開鍵で検証を行い、改ざんがない(Valid)であれば ログインOKとし、Tokenを返却する
    if endorseClaims, ok := endorseToken.Claims.(*jwtCustomClaims); ok && endorseToken.Valid {
        token := jwt.New(jwt.SigningMethodHS256)
        claims := token.Claims.(jwt.MapClaims)
        claims["imsi"] = endorseClaims.EndorseClaim.IMSI
        claims["imei"] = endorseClaims.EndorseClaim.IMEI
        claims["msisdn"] = endorseClaims.EndorseClaim.MSISDN
        claims["exp"] = time.Now().Add(time.Hour * 2).Unix()

        t, err := token.SignedString([]byte("secret"))
        if err != nil {
            return err
        }
        return c.JSON(http.StatusOK, map[string]string{
            "token": t,
        })
    } else {
        return echo.ErrUnauthorized
    }
    return echo.ErrUnauthorized
}

検証

ブラウザのデベロッパーツールを開き、Applicationタブからlocalstrageを開きます。
試しにこのendoseTokenのValueを変更したのちに Login ボタンを押してみてください。
トークンが改ざんされているため、API Serverからエラーメッセージが返却されることが確認できると思います。

localstrage

まとめ

SORACOM Endorseを利用してSIMの認証を外部サーバーの認証と連携するでもアプリケーションを作成し、各ノードの役割についてまとめました。

SIM面白いですね。次回以降はKryptonも触っていこうと思います。

注意事項

今回のデモアプリケーションはEndorse Tokenのみでログイン完了とするAPI Serverの実装を行なっていますが、大体の場合これでは不十分です。
デモアプリケーションはローカル環境での動作を想定しているので暗号化も行なっていませんし、Endorse Token自体が盗難された場合、Endorse Tokenを失効させる仕組みが有効期限満了まで存在しません。

本番環境では暗号化はもちろん、トークンが盗難されない環境で利用する or トークンだけではログインできない実装を行う。等の工夫が必要となりそうです。