AWSのCognitoを利用したOpenID連携について

ネイティブエンジニアの古谷です。iOS,Androidのアプリ開発を中心に行なっております。 今回、絶賛開発中の案件で利用したCognitoを使ったOpenID連携の方法について記載したいと思います。 開発時にiOS側を担当していたので、iOS側の実装例を元に説明していきたいと思います。

利用した経緯

そのサービスでは下記の要件で開発をすることになってました。

  1. AWS AppSyncを利用したチャット機能を利用
  2. 認証機能を独自で作成

この2点から、AWS側にこちらが作成した認証機能を使って連携することができないかどうかというところで色々と調査を進めた形です。 調べたところウェブIdフェデレーションというものがあり、そちらを利用することでOpenID形式の認証であれば、その情報を元にAWSの権限を付与できるというものみたいです。 今回の方法は結構特殊なパターンだとは思うので、普通はユーザープールの管理をサーバ側でうまいことやって管理するものかなーとも思っています。

ということで、AWSのウェブIdフェデレーションという仕組みを利用して、AWSの機能を利用できる形のサンプルを作ってみました。 今回のサンプルではYahoo!JAPAN(以下、「Yahoo」という。)のOpenIDを利用してAWS側にログインするところまでをやってみます。

とりあえず、ざっくりの全体像

f:id:teco_furuya:20201012183223p:plain
全体像

こちらのページから拝借しました。 aws.amazon.com

こんな感じでOpenID形式のものであれば、認可を得るとAWS Serviceの機能が利用出来ます。また、利用できる機能はユーザロールの設定で制御可能です。 この機能を使って、そのサービスではAppSyncやS3などが利用できるようにしました。 ということで、各種設定をしていきたいと思います。

AWSコンソール側の設定

3点対応が必要です。

  1. プロジェクトでの初期設定
  2. IDプロバイダーの設定
  3. IDプールにIDプロバイダーを連携

今回ですが、色々書くと長くなってしまうこともあるので一部端折ります。。。すみません。

1.プロジェクトでの初期設定

まずはAWSのAmplify機能を使って、プロジェクト用にAuth機能を有効化します。 terminalで対象のプロジェクトフォルダで下記コマンドを実行します。

  • amplify init -> プロジェクトのAmplifyの初期化
  • amplify configure -> プロジェクトのAWSコンソールにアクセスするための設定
  • amplify add auth -> プロジェクトに認証機能を追加
  • amplify push -> AWSコンソール側に設定を反映

amplifyコマンド自体の利用にはAWS Amplify CLIのインストールが必要です。このあたりの設定については今回、端折ります。。。

ここまで行うと、AWSのCognito側にIDプールの設定が追加されています。画像のyahootestxxxxxとなっているのが追加されたIDプールになります。

f:id:teco_furuya:20201012181143p:plain
IDプール管理画面

こちらのIDプールを使って、設定を行っていきます。

2.IDプロバイダーの設定

まずは、IAMのIDプロバイダーから対象の認可で利用するプロバイダーを登録します。今回の場合ではYahoo連携になります。 Yahoo側の設定は端折りますが、アプリから利用するためアプリケーションの種類はクライアントサイドで作ってます。

登録する情報は下記です。

・プロバイダーのURL -> こちらはYahooのAPIのURL (https://auth.login.yahoo.co.jp/yconnect/v2)

・対象者 -> こちらはYahooの管理画面で取得できるClientIDになります。

下記の画像の赤枠部分がClientIDになります。

f:id:teco_furuya:20201012190741p:plain
Yahooの管理画面

こちらの情報をIAMのプロバイダーに追加して設定します。

f:id:teco_furuya:20201012191031p:plain
IAMのプロバイダー設定

情報入力したら、次のステップへを選択し設定を保存します。

3.IDプール側にIDプロバイダーを連携

2.で登録した情報をIDプール側に連携します。

Cognito->IDプール->対象のプロジェクト->右上にあるIDプールの編集を押下します。

f:id:teco_furuya:20201012191320p:plain
IDプール管理画面

編集画面から認証プロバイダー->OpenIDを選択し、先ほど作成したIDプロバイダーにチェックをつけて保存します。(下図の赤枠部分)

f:id:teco_furuya:20201012191623p:plain
IDプール編集画面

これでAWSコンソール側の設定は完了です。次はアプリ側の実装です!!

アプリ側の実装(iOS)

まず、amplifyコマンドで下記は実行しておく必要があります。

  • amplify pull

実装としては、まずYahoo側の認可画面を表示し、そちらで取得したIDトークンをAWS側に投げます。 こちらはソースコード見た方が早いので貼ります。 こちらです。

import Foundation
import UIKit
import AWSMobileClient
import AuthenticationServices
import AWSCognitoIdentityProvider

class TestViewController:  UIViewController{
    
    private let PROVIDER_NAME = "auth.login.yahoo.co.jp/yconnect/v2"
    private let CLIENT_ID = "yahooで取得できるトークン"
    private let REDIRECT_URL = "yj-4lod6:/"  // Yahooで設定できるスキーマ名です
    private let IDENTITY_POOL_ID = "作成したIdentityPoolID"
    
    private var awsClient: AWSMobileClient?
    private var credentialsProvider: AWSCognitoCredentialsProvider?
    
    @IBOutlet weak var mYahooButton: UIButton!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        AWSDDLog.add(AWSDDTTYLogger.sharedInstance)
        AWSDDLog.sharedInstance.logLevel = .verbose

        credentialsProvider = AWSCognitoCredentialsProvider(regionType:.USEast2,
           identityPoolId:IDENTITY_POOL_ID)

        let configuration = AWSServiceConfiguration(region:.USEast2, credentialsProvider:credentialsProvider)

        AWSServiceManager.default().defaultServiceConfiguration = configuration
        
       // AWSMobileClientの初期化
        AWSMobileClient.default().initialize { (state, error) in
        }
        
        // ボタン設定
        mYahooButton.addTarget(self, action: #selector(tapEvent), for: UIControl.Event.touchUpInside)
        
    }
    
    @objc func tapEvent() {
        awsClient?.signOut()
        userAuthentication()
    }
    
    /// ユーザログイン
    ///
    private func userLogin(idToken: String) {
        print(" token \(idToken )")
        AWSMobileClient.default().federatedSignIn(providerName: PROVIDER_NAME, token: idToken) {state,erro in
            switch state {
            case .signedIn:
                self.getIdentityId()
            case.signedOut:
                print(" sign out ")
            case .signedOutFederatedTokensInvalid:
                print(" token invalid")
            default:
                print(" other state")
            }
        }
    }
    
    /// IdentityId取得
    private func getIdentityId() {
        AWSMobileClient.default().getAWSCredentials { (_, error) in
            print("identityId \(String(describing: AWSMobileClient.default().identityId))")
        }
    }
    
    /// ユーザ認証
    ///
    private func userAuthentication() {
        var compnents = URLComponents(string: "https://auth.login.yahoo.co.jp/yconnect/v2/authorization")
        let state = String(Int.random(in: 1..<10000))
        compnents?.queryItems =  [URLQueryItem(name: "response_type", value: "id_token"),
                                  URLQueryItem(name: "client_id", value: CLIENT_ID),
                                  URLQueryItem(name: "nonce", value: String(Int.random(in: 1..<10000))),
                                  URLQueryItem(name: "state", value: state),
                                  URLQueryItem(name: "redirect_uri", value: REDIRECT_URL),
                                  URLQueryItem(name: "scope", value: "openid")
                                 ]
    
        let callBackScheme = "yj-4lod6"
        
        let authSesstion = ASWebAuthenticationSession(url: compnents!.url!,                                                      callbackURLScheme: callBackScheme) { url, error in
            if let urlString = url?.absoluteString {
                print(" url \(urlString)")
                 let idToken =
                urlString.replacingOccurrences(of: "yj-4lod6:/#state=\(state)&id_token=", with: "")
                
                print(" idToken \(idToken)")
                self.userLogin(idToken: idToken)
                
            }
            print(" error \(error.debugDescription)")
        }
        
        if #available(iOS 13.0, *) {
            //
            authSesstion.presentationContextProvider = self
        }
        
        authSesstion.start()
        
    }
}

@available(iOS 13.0, *)
extension TestViewController: ASWebAuthenticationPresentationContextProviding {

    func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
            return view.window!
    }
}

iOSではOpenID連携時に利用できるWebViewとしてASWebAuthenticationSession( https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession)っていうのがあります。こちらを利用すると認可画面から戻ってくる際のアプリ側でのフック処理もプログラム書かなくてもやってくれます。あと、メリットとしては端末のSafariとのログイン情報と連動できるため、そういった場面では使いやすいのではないかな?と思います。

プログラムの流れを簡単に説明しますと。。。

まず、Yahoo連携ボタンを押下すると

  1. userAuthenticationのメソッドでWebViewが開く
  2. WebViewで必要情報の入力が完了するとコールバックでアプリ側にIDトークンの情報がurlで返却
  3. Idトークンを使ってuserLoginメソッドを実施
  4. ログイン後、ユーザごとに振られるIdentityIdをgetIdentityIdメソッドで取得

こんな感じで画面が出てきて、ログインする流れになります。

f:id:teco_furuya:20201014232044p:plain
連携の流れ

その後、Cognito側からIdentityIdが発行されていれば、連携がうまく出来ている状態です。 コンソールを見るとIDがカウントされているかと思います。 この状態で、ユーザロールの設定で利用できるAWS Serviceにアクセスができる状態になっているので、あとはよしなにといったところです。

f:id:teco_furuya:20201014223937p:plain
IDプール管理画面

苦労したところ

  • ログの出力方法

比較的ちょちょいとやっている感じはしますが、ちょうどAWSのAmplifyを利用していたタイミングが初期の頃だったので、そもそもの使い方が分からなかったり、整備中なのかリンクが切れてたり、いろんなところにドキュメントがあったりという状況でした。 そんなこともあり、最初はAWS側のログを表示するところもよく分からずで探してなかなか見つからずでデバッグも大変でした。。 後々見つけたので、こちらに貼っときます。下記である程度のAWS側のログ吐いてくれます。ただ、これでも足りなかったりはしましたが。。。 下記をAWSの初期化時に設定しておけば、ログ吐いてくれます。

AWSDDLog.add(AWSDDTTYLogger.sharedInstance)
AWSDDLog.sharedInstance.logLevel = .verbose
  • IdentityIdの取得

IdentityIdというのはウェブIdフェデレーションで取得できるIDプールの識別子みたいなもので、ユーザごとに割り振られるIDです。 こちらのIDに関して、そのサービスではユーザID的な位置付けで利用していたのですが、ログアウト後にログインした際、前のユーザ情報が残ってしまうという事故が起こりました。 IdentityId取得は非同期ということもあったのか、ログアウトして、ログインのタイミングでは同期したとしてもgetIdentityIdといういかにもなメソッドがあるにもかかわらず、そちらで取得すると前の情報が残ってしまうことがあったので、少し荒技な感じですが、下記メソッドを先に呼び出すことで、確実にIdentityIdが取れるのを発見しました!そちら実施するようにしてます。その時にログ吐けるようになっていてよかった。

AWSMobileClient.default().getAWSCredentials({ (_, error) in
     if error != nil {
         // エラー処理
     } else {
        // このタイミングだと確実にidentityId取れる! 下記呼び出して取得
       AWSMobileClient.default().identityId!
    }
})
  • Yahoo連携

今回、Yahoo連携で行いましたが意外とハマってしまいました。 以前もテストでYahoo連携はやっていた認識だったんですが、一部はAPIを直叩きしてやっていたことに途中で気づきました。そのため、アプリだけですべて処理を終わらせるまでやっておらず、AuthorizationAPIで直接Idトークンを取得するところでまずはまってしまい。 そのあとはAWS側にIdトークンを渡したのにエラーが返ってきてなんだ?っと思ったら、IDトークン取得時にはnonce,stateも設定しとかないといけなかったりと。。。まだまだ認証周りも勉強不足だなと感じました。。。

最後に

ここまでつらつらと情報描きましたが、Amplifyだの色々出てきすぎてしまい情報がいうほどまとまらなかったかもしれない。 実際はAmplifyの導入手順とかも載せた方が良かったとも思いますが、あまりにも長くなりそうだったので端折りましたすみません。 また、別の機会にでもまとめようかなとも思います。 プロジェクトのものではなく、自分で再度一から実装もしたので思ったよりも時間が掛かりましたが、理解が深まって良かったです! 次回はもっとiOS,Android側にフォーカスした内容が書ければいいなと思います。ありがとうございました!!

Amazon Web Servicesおよびかかる資料で使用されるその他の AWS 商標 は、米国および/またはその他の諸国における、Amazon.com, Inc. またはその関連会社の商標です。

tecotec.co.jp