ナラベルWebサービス2.0 ブログ

ナラベル Webサービスに関する情報発信

Twitter連携ログインのOAuth2.0への対応

 これまでのTwitterAPI有料化の騒動について,多数の制限が設定されたことも問題ですが,開発者の視点では,「いつ何が起こるかが分からない」と常に心配しなければいけないことが,もっとも厳しいことではないかと思います。突然,サービスが停止する,APIが使えなくなる,有料になる,心配ごとは絶えないですが,数多くのユーザーとツールとして影響力を維持している限り,うまく付き合っていかなければならないことは変わりないところ。

 そこで,今回はTwitterAPI2のoauth2.0 に対応しました。TwitterIDとの連携ログインは,ナラベルにとって重要な機能なので,現状のoauth1.0aが非推奨になっても運用上の支障が発生しないようにしておく必要があります。ナラベルはgrailsフレームワークで構築されているので,言語はGroovyです。Javaでの実装も同じステップだと思いますので、参考になればなによりです。

 まずは,JavaSDKの利用環境を設定します。TwitterからAPIv2用のSDKがリリースされましますが,ステイタスはまだβ版です。

 

build.gradle

   implementation group: 'com.twitter', name: 'twitter-api-java-sdk', version: '2.0.3'

   implementation group: 'com.github.scribejava', name: 'scribejava-core', version: '8.3.3'

   implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.11.0'

   implementation group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib', version: '1.8.21'

4つのライブラリを読み込みます。

実際の連携ログインの手順としては,

    1. TwitterIDでログインボタンをユーザーがクリック
    2. Controllerの所定のActionへJump
    3. Action内でoauth2に必要なオブジェクトの生成と遷移先URLの生成
    4. Actionから遷移先URLへリダイレクト
    5. ユーザーが承認
    6. 前述のActionで設定されたCallBackURLへとリダイレクト
    7. callbackで指定されたActionで改めてTwitterAPIにアクセスしてユーザー情報取得
    8. OKであればログイン

となります。ここではoauth2の詳細については,専門ではないので割愛。

で,実装は以下のとおり。

 

最初に必要なものをimportします。

//*** twitter oauth2
import com.github.scribejava.core.model.OAuth2AccessToken;
import com.github.scribejava.core.pkce.PKCE;
import com.github.scribejava.core.pkce.PKCECodeChallengeMethod;
import com.twitter.clientlib.auth.TwitterOAuth20Service;
import com.twitter.clientlib.TwitterCredentialsOAuth2;
import com.twitter.clientlib.ApiException;
import com.twitter.clientlib.api.TwitterApi;
import com.twitter.clientlib.model.ResourceUnauthorizedProblem;
import com.twitter.clientlib.model.Get2UsersMeResponse;

 

controllerのActionは以下のとおり

    //*** twitterLogin :oauth2 ****************************************
    def twitterLogin2(){

        //*** start;
        log.info("*** twitterLogin2 : LoginController *** ");
 
        //*** build callback url;
        StringBuffer callbackURL = request.getRequestURL();
        int index = callbackURL.lastIndexOf("/");
        callbackURL.replace(index, callbackURL.length(), "").append("/twitterCallback2");
        log.info("  CallBack URL : " + callbackURL.toString());

        //*** build oauth2 object
        try{
        TwitterOAuth20Service service = new TwitterOAuth20Service(
              'twitter.ClientID', // your client id
              'twitter.ClientSecrets', // your client secrets
              callbackURL.toString(), // callback url
                "tweet.read users.read offline.access"); // tweet.readは必須

            String secretState = "state";
            PKCE pkce = new PKCE();
            pkce.setCodeChallenge("challenge");
            pkce.setCodeChallengeMethod(PKCECodeChallengeMethod.PLAIN);
            pkce.setCodeVerifier("challenge");
            String authorizationUrl = service.getAuthorizationUrl(pkce, secretState);
            log.info("  authorization URL : " + authorizationUrl);

            //*** save object; callbackで必要なobjectをsessionへ保存
            def twitteroauth2=[:];
            twitteroauth2.put("service",service);
            twitteroauth2.put("pkce",pkce);
            session.setAttribute("twitteroauth2",twitteroauth2);

            //*** redirect
            response.sendRedirect(authorizationUrl);
        }
        catch (TwitterException e) {
            log.error("  *** Twitter ERR : twitterLogin2 : " + e);
            return render(view:'index');
        }

        //*** return
        return render(view:'index');
    }
 
 

このActionでは、事前にTwitterDevから作成したキーなどを使って、認証先にアクセスするためのリダイレクトURLを生成して、実際にリダイレクトさせています。

 

次は、認証後のcallback。

  //**** twitter callback ******************************************
    def twitterCallback2(){

        //*** start
        log.info("*** twitterCallBack2 : LoginController *** ");
 
        //*** get object from session cookie
        def twitteroauth2 = session.getAttribute("twitteroauth2");
        TwitterOAuth20Service service = (TwitterOAuth20Service)twitteroauth2.get("service");
        PKCE pkce = twitteroauth2.get("pkce");
        session.removeAttribute("twitteroauth2")

        //*** null checking
        if(!service || !pkce){
            log.error("  *** Twitter object NULL ERR : twitterCallBack2 :");
            flash.errors.add(this.message(code:"mes.err.login.object",default:"Oauth object ERRPR"));
            return forward(controller:"login",action:"index");
        }

        //*** get twitter code;
        if(!params.code){
            log.error("  *** can not get code : callback object : twitterCallBack2 :");
            return forward(controller:"login",action:"index");          
        }

        //*** get access token
        OAuth2AccessToken accessToken = service.getAccessToken(pkce, params.code);
        if(!accessToken){
            log.error("  *** can not get AccessToken : callback object : twitterCallBack2 :");
            return forward(controller:"login",action:"index");          
        }

        //*** setup apiInstance;
        TwitterApi apiInstance = null;
        try{
            apiInstance = new TwitterApi(new TwitterCredentialsOAuth2(
              'twitter.ClientID', // your clientID
         'twitter.ConsumerSecret', // your clientsecrets
                accessToken.getAccessToken(),accessToken.getRefreshToken()));
        }
        catch(ApiException e){
            log.error("  *** can not build apiInstance  : twitterCallBack2 :");
            return forward(controller:"login",action:"index");          
        }


        //*** get user info
        Set<String> userFields = new HashSet<>(Arrays.asList());
        Set<String> expansions = new HashSet<>(Arrays.asList());
        Set<String> tweetFields = new HashSet<>(Arrays.asList());
        try {
            Get2UsersMeResponse result = apiInstance.users().findMyUser()
                .userFields(userFields)
                .expansions(expansions)
                .tweetFields(tweetFields)
                .execute();

            //** set user Object;
            preUser.put("authid",result.data.id);
            preUser.put("authnm",result.data.name);
        }
        catch (ApiException e) {
            log.error(e.printStackTrace());
            return forward(controller:"login",action:"index");
        }
 
      //*** return
        return forward(controller:'login',action:'preUser');
 

 

流れとしては、oauth1.0aとほとんど変わらないので、差異は使っているAPIの関数やライブラリ。これでAPI1系が非推奨になっても、慌てずに済みそう。当面は、oauth1.0aを利用する予定。

 

細かい話は、本家のDocを参照してください。

developer.twitter.com

 

動いてしまえば、そんなに難しくないのですが、いろいろと調べるのに時間がかかってしまったのと、やはりTwitterがどのような対応をしてくるのかの様子見していた関係で、計画から実装までは結構時間がかかってしまいました。コーディング&テストは、実質1日使っていない。

 

万が一、Twitter連携ログインが完全に停止(もしくは有料化)となった場合には、ユーザーデータの移行も考える必要があるが、そちらの方が悩ましい。

 

いずれにせよ、Twitter騒動が収まりを見せていることが何よりだ。