Google、Microsoft、Facebook SDK を使用した Azure Mobile Services へのログイン


このポストは、10 月 27 日に投稿された Logging in with Google, Microsoft and Facebook SDKs to Azure Mobile Services の翻訳です。

Azure Mobile Services を使用する利点の 1 つは、非常にシンプルな API によってモバイル アプリケーションに認証を簡単に実装できる点です。サポートされている任意のプラットフォームのクライアント オブジェクト上で login 関数 (または同等の機能) を呼び出すと、シンプルな Web ベースのインターフェイスが表示され、Mobile Services にログインすることができます。これは「サーバー側認証フロー」と呼ばれます。このフローでは、サービスによってクライアントが (Web ページのリダイレクトを通じて) まずプロバイダーに誘導されてから、サービス自体に誘導されます。

サーバー側認証を使用すれば簡単にアプリケーションを開始できますが、使用する認証プロバイダーにログインをサポートするネイティブの SDK が用意されている場合は、そちらを利用した方がユーザーにとって便利です。たとえば、ユーザーが Android デバイスを利用している場合、デバイスに Google アカウントが関連付けられている可能性が非常に高いため、Google アカウントを利用することで資格情報の再入力を省略できれば、ユーザー エクスペリエンスの向上につながります。Android、iOS、Windows Phone デバイスにおける Facebook アカウントや、Windows または Windows Phone における Microsoft アカウントにも同じことが言えます。このシナリオは「クライアント側認証フロー」と呼ばれます。このフローでは、クライアント アプリケーションから直接 (ネイティブの SDK を通じて) プロバイダーに通信し、プロバイダーのトークンを Mobile Services と交換してサービス自体での認証を行います。

node.js バックエンドでは、以前から Facebook および Microsoft アカウントによるクライアント側認証フローが利用されていましたが、.NET ランタイムではサポートされていませんでした。また、Google SDK による認証はいずれのランタイムでもサポートされていませんでした。しかし、今回新たに、node.js バックエンドでの Google による認証と、.NET SDK での Facebook、Microsoft、Google SDK のデータを利用した認証がサポートされるようになりました。この機能には長らくご要望が寄せられていたため (参考: StackOverflow 1 (英語)StackOverflow 2 (英語)GitHub (英語)Azure フォーラム (英語))、リリースできることを嬉しく思います。今回の記事では、この新機能の使用方法についてご紹介します。

Microsoft アカウントによるログイン

node.js バックエンドでは、(node.js の) Mobile Services の先行リリースの時点から、Live SDK を利用して Windows Phone や Windows ストア アプリにログインすることが可能でした。今回、.NET バックエンドでも node.js とまったく同じ手順でこの認証を有効化できるようになりました。認証用のアプリケーションを登録し、Windows ストアのダッシュボードの情報を Mobile Services の [IDENTITY] タブにコピーして、アプリケーションに Live SDK を追加します。その後、Live SDK 使用してログインすると、SDK から受信した認証トークンが Mobile Services に送信されます。この手順はすべて、http://azure.microsoft.com/ja-jp/documentation/articles/mobile-services-windows-store-dotnet-single-sign-on/ のチュートリアルに記載されています。この手順に従って、.NET バックエンドを使用するアプリケーションに Live SDK によるログインを追加できます。

Facebook SDK によるログイン

Microsoft アカウントの場合と同様に、node.js バックエンドでは、以前から Facebook SDK のトークンを利用してログインすることも可能でした。今回、.NET バックエンドでもこのクライアント フローがサポートされるようになりました。前のセクションで述べたように、クライアントのコード (あるいはサーバー内で記述する必要のあるコード) はまったく同じものであるため、node.js 向けのチュートリアルはすべて .NET バックエンドにも該当します。Facebook SDK によるログインをアプリケーションに追加する方法の詳細については、iOS アプリ (英語) および Android アプリ (英語) に関するブログ記事をご覧ください。

Google SDK によるログイン

Google アカウントを利用したクライアント側認証フローは node.js と .NET バックエンドに共通の新機能で、どちらのバックエンドでも同様に機能します。ここでは、Android アプリにログイン操作を追加するシナリオをご紹介します。

認証用のアプリケーションを登録する

Google SDK を利用したネイティブの Android アプリへの認証を開始する前に、「下準備」が必要です。バックエンドの Mobile Services を Google デベロッパー コンソール (英語) のプロジェクトに接続します。これを実行するには、(Mobile Services での Google ログイン用のアプリケーションの登録の後に) Mobile Services アプリへの認証の追加 (英語) のチュートリアルの最初のセクションの手順に従います。下準備が完了したら、ネイティブ アプリ側の設定に進みます。

Google+ を Android アプリに統合する

アプリケーションに Google サインインを追加するには、Google Play Services ライブラリをアプリケーションに追加する必要があります。これを実行する手順は、Google ドキュメントのスタート ガイドに記載されています。このガイドのはじめには、Google デベロッパー コンソール (英語) で新規プロジェクトを作成すると書かれていますが、その代わりに、前の手順で作成したプロジェクトを使用します。プロジェクトには既に Web アプリケーションのクライアント ID が設定されており、Android アプリ用に新しい OAuth クライアント ID を追加します。

Android アプリにサインインを追加する

Google Play の設定が適切に完了したら、Google 自体にログインしてから Mobile Services にログインするために Google Play を使用することができます。Google Play Services を使用する前に、存在するかどうかを必ず確認することをお勧めします (筆者はエミュレーターが適切に設定されていなかったために長時間かけてデバッグすることとなりましたが、この手間を省くことができます)。

    btn.setOnClickListener(new View.OnClickListener() {

        @Override
        public void onClick(View v) {
            int isAvailableResult = GooglePlayServicesUtil.isGooglePlayServicesAvailable(activity);
            if (isAvailableResult == ConnectionResult.SUCCESS) {
                Log.d("msg", "Result for isGooglePlayServicesAvailable: SUCCESS");
                pickUserAccount();
            } else {
                Log.e("error", "Google play services is not available: " + isAvailableResult);                
            }
        }
    });

(物理または仮想) デバイスでサービスが実行されていることを確認したら、ユーザーが使用するデバイスに関連付けられているアカウントを選択します。これには、AccountPicker (英語) クラスを使用します。ユーザーが複数のアカウントを所有している場合には、アカウントを選択する UI が表示されます。1 つのアカウントのみを所有している場合には、何も表示されずに自動的にそのアカウントが選択されます (ユーザー エクスペリエンスが向上)。

    static final int REQUEST_CODE_PICK_ACCOUNT = 1000;
    private void pickUserAccount() {
        String[] accountTypes = new String[] { "com.google" };
        Intent intent = AccountPicker.newChooseAccountIntent(null, null, accountTypes, false, null, null, null, null);
        startActivityForResult(intent, REQUEST_CODE_PICK_ACCOUNT);
    }

アカウントを選択する UI は個別のアクティビティとして表示されるため、結果を取得するには onActivityResult メソッドをオーバーライドする必要があります。結果からはユーザーが使用するアカウントの名前を取得できます。アカウント名を取得したら、Mobile Services へのログインに必要なトークンを要求します。

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_CODE_PICK_ACCOUNT) {
            if (resultCode == RESULT_OK) {
                String accountName = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
                Log.d("msg", "Account name: " + accountName);
                getTokenAndLogin(accountName);
            } else if (resultCode == RESULT_CANCELED) {
                Log.d("msg", "Activity cancelled by user");
            }
        }
    }

Mobile Services にログインするために Google から取得する必要のあるトークンは ID トークンです。Android ID トークンは Web アプリケーションへのユーザーの認証に使用されます。それにより、ユーザーに再度ログインを要求することなく、Android アプリからリモート サービスへの通信を行うことができます。これはクライアント間認証の一種です。クライアント間認証では、ユーザーがデベロッパー コンソールで定義されたネイティブ アプリを利用して認証を行うと、リモート アプリ (Mobile Services) にトークンが提示され、その後 Google によってトークンが同じプロジェクトのものであることが検証されます。先ほど、同じプロジェクトに 2 つの OAuth クライアント ID を作成したのはこのためです。これにより、Mobile Services と Google が通信を行い、ユーザーがそのアプリケーションのみに認証されていることが確認されます。

ID トークンを取得するには、デバイスがオンラインになっている必要があります (これもユーザー エクスペリエンスの向上につながります)。次に、バックグラウンドのスレッドからトークンを取得します。これを実行するには、AsyncTask (英語) を使用します。

    static final String GOOGLE_SCOPE_TAKE2 = "audience:server:client_id:";
    static final String CLIENT_ID_WEB_APPS = "0000000000000-0aaaaaaa00aa00aa0aaa0a0aaaaaaa0a.apps.googleusercontent.com";
    static final String GOOGLE_ID_TOKEN_SCOPE = GOOGLE_SCOPE_TAKE2 + CLIENT_ID_WEB_APPS;

    private void getTokenAndLogin(String accountName) {
        if (mAccountName == null) {
            pickUserAccount();
        } else {
            if (isDeviceOnline()) {
                new GetTokenAndLoginTask(this, GOOGLE_ID_TOKEN_SCOPE, accountName).execute((Void)null);
            } else {
                Toast.makeText(this, R.string.not_online, Toast.LENGTH_LONG).show();
            }
        }
    }

    private boolean isDeviceOnline() {
        ConnectivityManager mgr = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo netInfo = mgr.getActiveNetworkInfo();
        return netInfo != null && netInfo.isConnected();
    }

デバイスがオンラインになっていることを確認する場合は、GoogleAuthUtil.getToken (英語) メソッドを使用できます。このメソッドには使用するアカウントと要求の範囲を指定します。クライアント間認証の場合、範囲は “audience:server:client_id:” というプレフィックスの後に Web アプリケーションのクライアント ID (このセクションの最初の手順で作成したもの) を追加して定義します。ここでは、Android アプリのクライアント ID は使用しないでください。ユーザーがまだアプリケーションによるデータの使用を承認していない場合は、UserRecoverableAuthException がスローされます。この例外の処理方法については後述します。ユーザーがアプリケーションによるデータの使用を承認している場合は、トークンが返されます。“id_token” というプロパティを利用してオブジェクト内にラップすることで、このトークンを Mobile Services の login 関数に渡します。関数が呼び出されると、Mobile Services へのログインが完了し、保護されているリソースにアクセスできるようになります。

    class GetTokenAndLoginTask extends AsyncTask<Void, Void, Void> {

        MainActivity mActivity;
        String mScope;
        String mEmail;

        public GetTokenAndLoginTask(MainActivity activity, String scope, String email) {
            this.mActivity = activity;
            this.mScope = scope;
            this.mEmail = email;
        }

        @Override
        protected Void doInBackground(Void... params) {
            try {
                final String token = fetchIdToken();
                if (token != null) {
                    loginToMobileService(token);
                }
            } catch (IOException e) {
                Log.e("error", "Exception: " + e);
            }

            return null;
        }

        protected void loginToMobileService(final String idToken) {
            runOnUiThread(new Runnable(){

                @Override
                public void run() {
                    mActivity.updateTextView("Token: " + idToken);
                    JsonObject loginBody = new JsonObject();
                    loginBody.addProperty("id_token", idToken);
                    mClient.login(MobileServiceAuthenticationProvider.Google, loginBody, new UserAuthenticationCallback() {

                        @Override
                        public void onCompleted(MobileServiceUser user, Exception error,
                                ServiceFilterResponse response) {
                            if (error != null) {
                                Log.e("error", "Login error: " + error);
                            } else {
                                Log.d("msg", "Logged in to the mobile service as " + user.getUserId());
                            }
                        }
                    });
                }
            });
        }

        protected String fetchIdToken() throws IOException {
            try {
                return GoogleAuthUtil.getToken(mActivity, mEmail, mScope);
            } catch (UserRecoverableAuthException urae) {
                mActivity.handleException(urae);
            } catch (GoogleAuthException gae) {
                Log.e("error", "Unrecoverable exception: " + gae);
            }
            return null;
        }
    }

最後に、getToken メソッドにより回復可能な例外が返された場合の処理を行います。この場合、例外によって返されたアクティビティを開始すると、使用するアプリケーションの承認を求めるダイアログが表示されます。次に、アプリケーションがその承認画面から戻ったときに、トークンを取得する関数を再び呼び出せるように (そして、ユーザーがアプリケーションを承認している場合はトークンの取得が正常に行われるように)、onActivityResult を更新する必要があります。

    static final int REQUEST_CODE_PICK_ACCOUNT = 1000;
    static final int REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR = 1001;

    public void handleException(final Exception e) {
// この呼び出しは AsyncTask から実行されるため、
// UI スレッドでは代わりに次のコードを実行する必要があります。
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (e instanceof GooglePlayServicesAvailabilityException) {
// Google Play Services APK が古いか、無効になっているか、存在しません。
// Google Play Services によって作成されたダイアログを表示して、
// APK を更新するように促します。
                    int statusCode = ((GooglePlayServicesAvailabilityException)e)
                            .getConnectionStatusCode();
                    Dialog dialog = GooglePlayServicesUtil.getErrorDialog(statusCode,
                            MainActivity.this,
                            REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR);
                    dialog.show();
                } else if (e instanceof UserRecoverableAuthException) {
// ユーザーがアプリケーションによるアカウントへのアクセスを許可していない場合など、
// 承認できないものの、ユーザーが修復することができる場合に
// ユーザーを Google Play Services のアクティビティに転送します。
                    Intent intent = ((UserRecoverableAuthException)e).getIntent();
                    startActivityForResult(intent,
                            REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR);
                }
            }
        });
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_CODE_PICK_ACCOUNT) {
            if (resultCode == RESULT_OK) {
                String accountName = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
                Log.d("msg", "Account name: " + accountName);
                getTokenAndLogin(accountName);
            } else if (resultCode == RESULT_CANCELED) {
                Log.d("msg", "Activity cancelled by user");
            }
        } else if (requestCode == REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR && resultCode == RESULT_OK) {
            getTokenAndLogin();
        }
    }

上記の方法で、モバイル アプリケーションにネイティブの Google 認証を追加し、Mobile Services へのログインに使用することができます。既に述べたように、これは node.js と .NET バックエンドで同様に機能します。

まとめ

今回のように、長らくご要望が寄せられていた機能をリリースできることをとても嬉しく思います。この機能を利用してログインに関するユーザー エクスペリエンスを向上させることで、Azure Mobile Services が皆様のアプリケーションにさらに多くのメリットを提供できるようになれば幸いです。

ご意見がございましたら、この記事のコメント欄、Twitter アカウント @AzureMobileMSDN フォーラムまでお寄せください。


Comments (0)

Skip to main content