23
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ZOZOテクノロジーズ #1Advent Calendar 2019

Day 7

Firebaseを使ったTwitter認証をクライアント(Swift)からサーバ(Rails)までまるっと実装する

Last updated at Posted at 2019-12-07

この記事は ZOZOテクノロジーズ #1 Advent Calendar 2019 7日目の記事になります。
ZOZOテクノロジーズでバックエンドのエンジニアをしている @calorie です。よろしくお願いします。

昨日の記事は @s_nagasawa さんによる「vue-composition-apiを使って機能単位のコード分割と合成を試してみた」でした。
ちょうど年末にフロントエンドを勉強しようと思っていたので、
とても興味深い記事でした。こちらもぜひご覧ください。

この記事について

Firebase Authentication を用いたiOSにおけるTwitter認証を、
クライアントで認証を行い、サーバでユーザを作成するまで通して実装してみたので、
その実装方法と得られた知見を、
サンプルコードを用いながら共有します。

やってみると、意外とうまく行かなかったり、
クライアントからサーバまで実装しようとすると、
ドキュメントが分散してしまっていて、
実装に想像以上に時間がかかってしまったため、
この記事でまとめることで、同じような境遇の方々が、
より、サービスの実装に集中する時間が長くなることを目的にしています。
指摘等ありましたら、ぜひコメントにてお願いします。

以降は、以下のサンプルコードを用いますので、こちらもご覧下さい。

筆者がSwiftを初めて触ったのと、短い時間で作ってしまったものなので、
現状では雑な部分があるはご容赦ください。
流用する際にはご注意ください。

Firebaseを用いた認証フロー

この記事で対象とする、
Firebaseを用いた認証フローを、
以下のシーケンス図に記載します。
これに沿って実装を見ていければと思います。

firebase_auth.jpg

1. クライアントからFirebaseへ認証

firebase_auth3.gif

公式ドキュメントの「はじめに」のあたりは、
ドキュメントも豊富で詰まりにくいので割愛させていただきます。

クライアント上で、ログイン用のViewControllerが作成されて、
何もないログイン画面が表示されている状態を前提に進めていきます。

基本的にはこちらの公式ドキュメントに沿って実装していく形になります。
ログインボタンに対して、signIn のメソッドを紐付けます。

該当コード

class LoginViewController: UIViewController {
    var twitterProvider : OAuthProvider?

    override func viewDidLoad() {
        super.viewDidLoad()
        self.twitterProvider = OAuthProvider(providerID:"twitter.com");
    }

    @IBAction func tappedSignInWithTwitter(_ sender: Any) {
        self.twitterProvider?.getCredentialWith(_: nil){ (credential, error) in
            if error != nil {
                // Handle error.
            }
            if let credential = credential {
                Auth.auth().signIn(with: credential) { (authResult, error) in
                    if error != nil {
                        // Handle error.
                    }

                    Auth.auth().currentUser?.getIDToken(completion: { (token, error) in
                        APIClient.sharedInstance.createUser(token: token!, success: { () in
                            let vc = UIStoryboard.main().instantiateInitialViewController()
                            UIApplication.shared.windows.filter { $0.isKeyWindow }.first?.rootViewController = vc
                        }) {
                        }
                    })
                }
            }
        }
    }
}

ここで注意すべきなのは、
self.twitterProvider = OAuthProvider(providerID:"twitter.com"); この処理を、
viewDidLoad 内で行わなければならないことです。
筆者はIBAction内でやってしまい、ここで2時間くらい詰まりました。

また、公式ドキュメントのサンプルコードと若干書き方が異なるのにお気づきでしょうか。
公式では Auth().signIn のところが、 Auth.auth().signIn となっています。
そうなんです。公式のサンプルコード、そのまま用いるとビルドが通りません。
おそらく Auth()Auth.auth() を返すコードがどこかにある前提で書かれているのかなと考えていますが、
ここも若干はまりました。。 公式ドキュメントは参考程度に捉えると良いのかもしれません。

2. ID tokenの取得

signInに成功すると、 Twitter OAuth access token などを返してくれるので、
これによってAPI経由でTwitterの情報を取得できるようになります。

ただ、今必要なのはFirebase Authenticationが管理するID tokenで、
これは返してくれていないようなので、 getIDToken で取得します。

該当コード

Auth.auth().currentUser?.getIDToken(completion: { (token, error) in
    ...
})

3. ID tokenを用いてサーバへリクエスト

2で取得したID tokenをリクエストヘッダーの
Authorization: Bearer ヘッダに埋め込んで送信します。

該当コード1

APIClient.sharedInstance.createUser(token: token!, success: { () in
    ...
}) {
}

該当コード2

func createUser(token: String, success: @escaping () -> Void, failure: @escaping () -> Void) {
    let baseURL = URL(string: API_HOST + "/v1/users")!
    let headers: HTTPHeaders = ["Authorization": String(format: "Bearer %@", token)]
    Alamofire.request(baseURL, method: .post, parameters: nil, headers: headers)
        .validate(statusCode: 200..<300)
        .responseJSON { response in
        switch response.result {
        case .success(_):
            success()
        case .failure(_):
            failure()
        }
    }
}

4. サーバでID tokenの検証

クライアントから送られてきたJWT形式のID tokenを、
サーバでFirebase Admin SDKを用いてデコードして検証します。
公式ドキュメント

ですが、残念なことに、RubyのFirebase Admin SDKはありません。

よって、このように自前で実装されている方や、
以下のように、いくつかgemが公開されています。

  • firebase-auth-rails
    • betaだが、一番高機能
    • RailsとRedisに依存していて、Google APIでのリクエストのキャッシュや、RailsでのModelの作成までカバーしている
  • firebase_id_token
    • pre-releaseだが、一番Starが多い
    • Redisに依存していて、Google APIでのリクエストをキャッシュしてくれる、バッチによるCertificatesの更新に対応している
  • firebase-auth-id_token_keeper
    • 一番シンプルだが、キャッシュなどの機構はない

今回は firebase-auth-id_token_keeper を使用しました。
シンプルで実装をすべて把握できるのと、
基本的なアプリケーションにおいては、
キャッシュを使わなくても十分に速度を出せることから、
今後において、あまりRedisを使う機会がなく、
このためだけにRedisを導入することに対してネガティブだったためです。

また、実装がシンプルなので、パッチがあてやすく、
認証でキャッシュが必要になったら、
この部分とかに、キャッシュを使うように
パッチをあてることで解決できると考えています。

Railsにおける実装は、
ApplicationController などで、
authenticate_with_http_token を用いてID tokenを取り出し、
Firebase::Auth::IDTokenKeeper::IDToken.new(token).verified_id_token で検証しています。

該当コード

before_action :authorize

private

def authorize
  access_token = extract_access_token_from_header
  # TODO if access_token.blank?
  @current_user_firebase_uid = access_token.first['sub']
end

def current_user
  return unless @current_user_firebase_uid
  @current_user ||= User.find_by(uuid: @current_user_firebase_uid)
end

def extract_access_token_from_header
  authenticate_with_http_token do |token, _|
    Firebase::Auth::IDTokenKeeper::IDToken.new(token).verified_id_token
  end
  ...
end

ID tokenをデコードしたあとに、ペイロードからFirebaseでのuidを取り出し、
ユーザの特定用にサーバ側で保存しています。
JWTのペイロードの詳細は、公式ドキュメントに記載があります。

def authorize
  # デコードしたID tokenからuidを取得
  access_token = extract_access_token_from_header
  @current_user_firebase_uid = access_token.first['sub']
end

def extract_access_token_from_header
  authenticate_with_http_token do |token, _|
    Firebase::Auth::IDTokenKeeper::IDToken.new(token).verified_id_token
  end
  ...
end

5. サーバでユーザの保存

ApplicationControllerで取得したuidをDBに保存します。

該当コード

def create
  user = User.new(uuid: @current_user_firebase_uid)
  user.save!
  render json: UserSerializer.new(user).serialized_json
end

ログイン後は、4と同様にID tokenをデコードしてuidを取り出し、
それに紐づくユーザをselectしてくる流れとなります。

該当コード

def authorize
  access_token = extract_access_token_from_header
  # TODO if access_token.blank?
  @current_user_firebase_uid = access_token.first['sub']
end

def current_user
  return unless @current_user_firebase_uid
  @current_user ||= User.find_by(uuid: @current_user_firebase_uid)
end

ここまでで、認証からユーザ作成までを一通り実装しました。

TODO

今回のサンプルアプリを、
少なくともハッカソンのようなところで
使えるくらいには仕上げたいと思っています。

  • Firebase Authorizationが提供している他の認証の実装
    • Sign In with Appleはやりたい
  • Docker化

ちょくちょく手を入れていこうと思いますので、
また知見がたまりましたら、続きを書きたいと思います。

最後に

Firebase Authenticationを用いたiOSにおけるTwitter認証を、
クライアントで認証を行い、サーバでユーザを作成するまで通して実装した流れをまとめました。
少しでも読まれた方のお役に立てれば幸いです。

明日は、 @hsawada さんが公開予定です。そちらもぜひご覧下さい。

23
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?