SPAを作っていて、ユーザーの認証にTwitterのOAuthを利用しようとした場合、認証情報を得る為に、下記手順が必要になります。
- OAuth認証用のAPIを新規windowで開く
- Twitter<->認証APIの間で認証情報をやりとり
- クライアント(ブラウザ)に結果を返す
今回作っていたアプリケーションは状態毎のURLを持たないものだったため、window.open()
で新規windowを開き、認証後、window.opener.callback()
を呼びだす事でクライアント側にデータを返すよう試んだのですが…
(index.js)
window.callback = function(oauthInfo){
}
window.open('http://example.com/api/oauth');
(oauth.js)
var match,
search = /([^&=]+)=?([^&]*)/g,
decode = function (s) { return decodeURIComponent(s.replace('+', " ")); },
query = window.location.search.substring(1);
var params = {};
while (match = search.exec(query)){
params[decode(match[1])] = decode(match[2]);
}
// 認証情報を親windowへ渡す
window.opener.callback(params);
widnow.close();
大体、こんな感じの実装で、iOS Safariなどメジャーなブラウザで動作していました。
が、Chrome for iOSでは、window.opener
が存在しない事が発覚。。
https://code.google.com/p/chromium/issues/detail?id=136610
対策
SPAでも、URLと状態が紐付いているなら、わざわざwindow.open()
で別windowを開かず、現在のwindowを使い、URLに認証情報を含める事で対応できそう。
が、今回は状態毎のURLを持たせたくなかったので、localStorageを使った小技を使いました。
(index.js)
window.open('http://example.com/api/oauth');
checkStorage();
function checkStorage(){
if (localStorage.OAUTH){
var authInfo = JSON.parse(localStorage.OAUTH);
localStorage.removeItem('OAUTH');
}else{
setTimeout(checkStorage, 1000); // 毎秒チェック
}
}
(oauth.js)
var match,
search = /([^&=]+)=?([^&]*)/g,
decode = function (s) { return decodeURIComponent(s.replace('+', " ")); },
query = window.location.search.substring(1);
var params = {};
while (match = search.exec(query)){
params[decode(match[1])] = decode(match[2]);
}
// クエリストリングをlocalStorageへ
localStorage.OAUTH = JSON.stringify(params);
window.close();
認証開始後、毎秒localStorageをチェックする事で、認証完了を検知します。 cookieを使えばAPIサーバ側で値を埋め込めるのですが、少し遠回りをしてlocalStorageを使えば、ドメインが異なっていても動作するようになります。
どうにか対応できたものの、なんだかスッキリしない…。