この記事は ZOZOテクノロジーズ #1 Advent Calendar 2019 7日目の記事になります。
ZOZOテクノロジーズでバックエンドのエンジニアをしている @calorie です。よろしくお願いします。
昨日の記事は @s_nagasawa さんによる「vue-composition-apiを使って機能単位のコード分割と合成を試してみた」でした。
ちょうど年末にフロントエンドを勉強しようと思っていたので、
とても興味深い記事でした。こちらもぜひご覧ください。
この記事について
Firebase Authentication を用いたiOSにおけるTwitter認証を、
クライアントで認証を行い、サーバでユーザを作成するまで通して実装してみたので、
その実装方法と得られた知見を、
サンプルコードを用いながら共有します。
やってみると、意外とうまく行かなかったり、
クライアントからサーバまで実装しようとすると、
ドキュメントが分散してしまっていて、
実装に想像以上に時間がかかってしまったため、
この記事でまとめることで、同じような境遇の方々が、
より、サービスの実装に集中する時間が長くなることを目的にしています。
指摘等ありましたら、ぜひコメントにてお願いします。
以降は、以下のサンプルコードを用いますので、こちらもご覧下さい。
筆者がSwiftを初めて触ったのと、短い時間で作ってしまったものなので、
現状では雑な部分があるはご容赦ください。
流用する際にはご注意ください。
Firebaseを用いた認証フロー
この記事で対象とする、
Firebaseを用いた認証フローを、
以下のシーケンス図に記載します。
これに沿って実装を見ていければと思います。
1. クライアントからFirebaseへ認証
公式ドキュメントの「はじめに」のあたりは、
ドキュメントも豊富で詰まりにくいので割愛させていただきます。
クライアント上で、ログイン用の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
ヘッダに埋め込んで送信します。
APIClient.sharedInstance.createUser(token: token!, success: { () in
...
}) {
}
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 さんが公開予定です。そちらもぜひご覧下さい。