はじめに
ネイティブアプリ開発をしていて、部分的にwebview実装にする場合は多い。一方でその際にセッション共有など、ログイン状態をネイティブアプリとウェブの間で引き継ぐ必要が出てくる場合がある。
webview session share
などでググると、そこそこ記事は出てくる↓
しかし、Firebase Authを使ってユーザ認証をしている場合にどうやるのかというのがなかなかなかったので、今回はその部分をやってみた。
手順としては、以下の記事とほぼ同じになる。
概要
構成
- iOS: Swift 4.2
- Web: Vue.js 2.5
- Backend: Node Express 4.16
- BaaS: Firebase admin SDK 6.0, Firebase iOS SDK 4.13, Firebase web SDK 5.2
目的
iOS側でFirebase Authを使ってログイン済みの状態で、同じくFirebase Authを使って認証を行うweb app (Vue.js)を、iOS側と同じアカウントでログインした状態でwebviewしたい
セッション共有をするわけではなく、Firebase Authのcustom token loginをweb側クライアントサイドで行うというもの。 Swift側での認証方法がなんであれ(Firebase Authはパスワード認証のほかSMS認証やOAuthに対応している)web側でのログインには影響がない。
手順
1. idTokenの発行
最初のステップは、サーバサイドでFirebase AuthのCustomTokenを発行することだが、その発行にはFirebaseでのuid (user id)
が必要となる。そのままuidをサーバサイドに送るようにすると、uidさえわかれば誰でもCustomTokenが発行できる状態になってしまう。uidが、Firebase Authを使ってログインしているユーザから送信されていることを確認するため、uidをトークン化(idToken
)する機能がFirebase Auth には実装されている。
このidTokenをサーバへ送り、サーバサイドでこのトークンが正しいものか検証をした上で、ログインのためのCustomTokenを発行する。まずはそのidTokenの発行の部分の仕組みを実装する。
iOS側で以下の実装をすると、idTokenが発行される。
let currentUser = Auth.auth().currentUser currentUser?.getIDTokenForcingRefresh(true) { idToken, error in if let error = error { // error handling return } // continued ...
このidTokenを、バックエンドに送る。 Alamofireを使ってる場合はこんな感じになる。
let url = // your backend URL let params = ["idtoken": idToken] Alamofire.request(url, method: .post, parameters: params) .validate(statusCode: 200..<300) .responseJSON { response in // continued ...
2. idTokenの検証とcustomTokenの発行
ここからはサーバサイドの処理になる。Expressを使う場合は、以下のような実装でiOSからのidTokenを受け付ける。
(※API認証、エラーハンドリング等々余分な部分は省略した。)
const express = require("express"); const app = express(); const bodyParser = require('body-parser'); const admin = require('firebase-admin'); app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.json()); const defaultApp = admin.initializeApp({ credential: admin.credential.cert(/* your firebase admin serviceAccount */) }) app.post("/", (req, res) => { let idToken = req.body.token; admin.auth().verifyIdToken(idToken) .then(function(decodedToken) { let uid = decodedToken.uid; const expiresIn = 60 * 60 * 24 * 5 * 1000; admin.auth().createCustomToken(uid) .then(function(customToken) { const options = {maxAge: expiresIn, httpOnly: true, secure: true}; res.cookie('customtoken', customToken, options); res.end(JSON.stringify({status: 'success'})) }) .catch(function(error) { // error handling }); }); });
受け取ったidTokenは、admin.auth().verifyIdToken()
関数で検証できる。検証が成功すると、返り値はデコードされたuid
になっている。
admin.auth().verifyIdToken(idToken) .then(function(decodedToken) { ....(処理) });
デコードされたuid
を使って、customTokenを発行することができる。admin.auth().createCustomToken(uid)
関数を使う。そこで返却されたcustomToken
を使って、Firebase Authによるログインが可能となる。これをiOS側に返却する。今回はcookieにcustomTokenを持たせ、返却している。
admin.auth().createCustomToken(uid) .then(function(customToken) { const options = {maxAge: expiresIn, httpOnly: true, secure: true}; res.cookie('customtoken', customToken, options); res.end(JSON.stringify({status: 'success'})) }) .catch(function(error) { // error handling });
3. customTokenをWKWebViewにセットし、初期化する
続いてはiOS側の処理になる。バックエンドから返却されたcustomTokenを、webViewに反映させる。この部分は、以下のリンクをおおいに参考にさせてもらった。
qiita.com
// after fetch customToken setWkWebView(token) wkWebViewLoad(url: url, token: token) func setWkWebView(_ token: String) { let userContentController = WKUserContentController() let cookieScript = WKUserScript(source: "document.cookie='customtoken=\(token);path=/';", injectionTime: .atDocumentStart, forMainFrameOnly: true) userContentController.addUserScript(cookieScript) let wkWebViewConfig = WKWebViewConfiguration() wkWebViewConfig.userContentController = userContentController self.webView = WKWebView(frame: self.view.frame, configuration: wkWebViewConfig) self.webView.navigationDelegate = self self.view.addSubview(self.webView) } func wkWebViewLoad(url: URL, token: String) { let cookieStorage = HTTPCookieStorage.shared let cookieHeaderField = ["Set-Cookie": "customtoken="+token] let cookies = HTTPCookie.cookies(withResponseHeaderFields: cookieHeaderField, for: url) cookieStorage.setCookies(cookies, for: url, mainDocumentURL: url) let request = URLRequest(url: url, cachePolicy: URLRequest.CachePolicy.useProtocolCachePolicy, timeoutInterval: 10.0) self.webView.load(request) }
setWkWebView()
関数では、サーバから返却されたcustomTokenを引数にとっている。customTokenをWKWebViewでのページリクエストのcookieに含めて送信したいため、WKUserScript()
でjavascriptによってcookieを設定している。
func setWkWebView(_ token: String) { let userContentController = WKUserContentController() let cookieScript = WKUserScript(source: "document.cookie='customtoken=\(token);path=/';", injectionTime: .atDocumentStart, forMainFrameOnly: true) userContentController.addUserScript(cookieScript) let wkWebViewConfig = WKWebViewConfiguration() wkWebViewConfig.userContentController = userContentController self.webView = WKWebView(frame: self.view.frame, configuration: wkWebViewConfig) self.webView.navigationDelegate = self self.view.addSubview(self.webView) }
続いてwkWebViewLoad()
関数では、cookieの永続化のためにcookieStorage
へcustomTokenを記録した後、リクエストを実行している。
func wkWebViewLoad(url: URL, token: String) { let cookieStorage = HTTPCookieStorage.shared let cookieHeaderField = ["Set-Cookie": "customtoken="+token] let cookies = HTTPCookie.cookies(withResponseHeaderFields: cookieHeaderField, for: url) cookieStorage.setCookies(cookies, for: url, mainDocumentURL: url) let request = URLRequest(url: url, cachePolicy: URLRequest.CachePolicy.useProtocolCachePolicy, timeoutInterval: 10.0) self.webView.load(request) }
4. customTokenでログイン
最後は、web App上での処理になる。 ログイン処理を行う部分で、以下の実装をする。
let user = await firebase.auth().signInWithCustomToken(token).then( user => { // Log in !! }).catch(error => { // error handling })
適宜cookieのパース等が必要である。 今回はVue.jsでVue-routerを使っていたため、routerの処理内でこれを行うように実装した。
import Vue from 'vue' import Router from 'vue-router' import vueCookies from 'vue-cookies' import async from 'async' import firebase from 'firebase' Vue.use(Router) Vue.use(vueCookies) let router = new Router({ routes: [ { path: '/', name: 'main', component: /* your component */, meta: { requiresAuth: true } }, { path: '/signin', name: 'Signin', component: /* your signin component */ } ] }) router.beforeEach(async (to, from, next) => { let requiresAuth = to.matched.some(record => record.meta.requiresAuth) let currentUser = firebase.auth().currentUser if (requiresAuth) { if (!currentUser) { // not yet firebase login if (Vue.cookies.get('customtoken')) { let token = Vue.cookies.get('customtoken') await firebase.auth().signInWithCustomToken(token).catch(err => { // error handling next({path: "/signin"}) }) next() } else { // no token provided // continue process ... // ... })
まとめ
Firebase Authの情報をiOS・web間で引き継ぐ方法を紹介した。
Firebase Auth のadmin SDKには、以下のようにセッション管理に関するものも一応ある。しかしこれはあくまで外部連携サービスがセッション管理を行なっている場合に、それに準拠させるためのものらしい。このセッションでFirebase Authの状態を引き継ぐことはできないようであった。
セッション Cookie を管理する | Firebase
そのほか、まだベータ版のようだが、Service Workerなる実装もFirebase Authにはあるらしい。今後に期待。
Service Worker によるセッション管理 | Firebase
参考にしたサイト
- 【Swift】ユーザー認証APIを通した後、同一セッションとしてUIWebViewを表示する - Qiita
- WKWebViewでのSessionの共有 - Qiita
- ログイン認証したあとに、WKWebViewでCookieを使ってセッションを保つ方法と失敗例 - Qiita
- Firebase auth - login user from app in website - Stack Overflow
- Verify ID Tokens | Firebase
- ログイン認証したあとに、WKWebViewでCookieを使ってセッションを保つ方法と失敗例 - Qiita
- セッション Cookie を管理する | Firebase
- Service Worker によるセッション管理 | Firebase