SORACOM Endorseの認証の仕組みを理解する
はじめに
SORACOM Endorseを利用してSIMの認証を外部サーバーの認証と連携するでもアプリケーションを作成し、各ノードの役割についてまとめます。
Endorseは SIMごとにユニークかつ改ざん検知可能なトークンの払い出しを受けることができるサービスです。
このトークンを利用することでSIMを利用した通信元を特定し、アプリケーションの認証が可能となります。
構成
CLIだけのデモだけでは味気がないのでAngularでSPA(Single Page Application)を作りました。
SPAをPC上で動作させEndorse Tokenの取得を行い、また同じPC上で動作させているGoのAPIサーバーへのログインを行います。
シーケンスとしては以下の通りです。
- ①SPAからEndorse Tokenを取得する(Air SIM経由の通信が必要)
- ②取得したEndorse Tokenを利用してAPIサーバーへ認証要求(Air SIM経由の通信でなくても良い)
- ③APIサーバーはSORACOMの公開鍵を取得してEndorse Tokenを検証
- ④正しいトークンであればAPIサーバーからSPAへ認証OKを返す
それでは環境構築から動作確認までを行なっていきます。
環境構築
SPAとAPIサーバーを動作させるコンテナ環境を準備しました。
以下のコマンドで 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を紐づけておきます。
動作確認
ブラウザから localhost:4200/logintoken
へアクセスすると以下のような画面へ遷移します。
ブラウザのデベロッパーツールを開き Get Endorse Token
ボタンを押します。
するとコンソールに以下のようなEndorse Tokenが表示されます。(後に改ざん検証を行うのでlocalstrageにも保存してあります。)
もし表示されない場合や402が返ってくる場合は、Endorseの設定がONになっているか、SIM経由の通信となっているかを確認してみてください。
このトークンについては以降で解説します。
次に Login
ボタンを押すことでログイン完了画面に遷移し、ログインしているデバイスのIMSIとIMEIとMSISDNが表示されたかと思います。
Basic認証のかかったWebサービスのログインを考えると、本人しか知らないID/PWを入力することで認証を行いますが、今回は本人しか持っていない(偽装されることのない)SIMカードを使って認証を行なっています。
トークンについて
このトークンはJSON Web Token(JWT)と呼ばれているフォーマットで、秘密鍵で署名され改ざんの検知が可能となっています。
JWTは2つのピリオドで区切られた3つの領域からなっており [ヘッダー].[ペイロード].[署名] という構造です。
このトークンを https://jwt.io/
のページへ貼り付け中身をみてみます。
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からエラーメッセージが返却されることが確認できると思います。
まとめ
SORACOM Endorseを利用してSIMの認証を外部サーバーの認証と連携するでもアプリケーションを作成し、各ノードの役割についてまとめました。
SIM面白いですね。次回以降はKryptonも触っていこうと思います。
注意事項
今回のデモアプリケーションはEndorse Tokenのみでログイン完了とするAPI Serverの実装を行なっていますが、大体の場合これでは不十分です。
デモアプリケーションはローカル環境での動作を想定しているので暗号化も行なっていませんし、Endorse Token自体が盗難された場合、Endorse Tokenを失効させる仕組みが有効期限満了まで存在しません。
本番環境では暗号化はもちろん、トークンが盗難されない環境で利用する or トークンだけではログインできない実装を行う。等の工夫が必要となりそうです。