2 var X_NET_OAUTH2_detection = new Function( 'w', 'try{return w.location.search}catch(e){}' ),
3 X_NET_OAUTH2_authorizationWindow,
4 X_NET_OAUTH2_authorizationTimerID;
9 * <dt>X.Event.NEED_AUTH<dd>window を popup して認可を行う必要あり。ポインターイベント内で oauth2.requestAuth() を呼ぶ。
10 * <dt>X.Event.CANCELED<dd>認可 window が閉じられた。([x]等でウインドウが閉じられた、oauth2.cancelAuth() が呼ばれた)
11 * <dt>X.Event.SUCCESS<dd>認可 window でユーザーが認可し、続いてコードの認可が済んだ。
12 * <dt>X.Event.ERROR<dd>コードの認可のエラー、リフレッシュトークンのエラー、ネットワークエラー
13 * <dt>X.Event.PROGRESS<dd>コードを window から受け取った、リフレッシュトークンの開始、コードの認可を header -> params に切替
17 * oauth2.js , <opendata@oucs.ox.ac.uk>
20 * @class OAuth2 サービスを定義し接続状況をモニタする。適宜にトークンのアップデートなどを行う
22 * @extends {EventDispatcher}
23 * @example // OAuth2 サービスの定義
25 'clientID' : 'xxxxxxxx.apps.googleusercontent.com',
26 'clientSecret' : 'xxxxxxxx',
27 'authorizeEndpoint' : 'https://accounts.google.com/o/oauth2/auth',
28 'tokenEndpoint' : 'https://accounts.google.com/o/oauth2/token',
29 'redirectURI' : X.URL.cleanup( document.location.href ), // 専用の軽量ページを用意してもよいが、現在のアドレスでも可能
30 'scopes' : [ 'https://www.googleapis.com/auth/blogger' ],
31 'authorizeWindowWidth' : 500,
32 'authorizeWindowHeight' : 500
33 }).listen( [ X.Event.NEED_AUTH, X.Event.CANCELED, X.Event.SUCCESS, X.Event.ERROR, X.Event.PROGRESS ], updateOAuth2State );
37 xhr : 'https://www.googleapis.com/blogger/v3/users/self/blogs',
40 test : 'gadget' // http -> https:xProtocol なリクエストのため、google ガジェットを proxy に使用
42 .listen( [ X.Event.SUCCESS, X.Event.ERROR, X.Event.PROGRESS ], updateOAuth2State );
44 X[ 'OAuth2' ] = X_EventDispatcher[ 'inherits' ](
48 /** @lends OAuth2.prototype */
50 'Constructor' : function( obj ){
53 obj = X_Object_clone( obj );
55 X_Pair_create( this, obj );
57 obj.onAuthError = X_NET_OAUTH2_onXHR401Error;
58 obj.updateRequest = X_NET_OAUTH2_updateRequest;
60 if( _getAccessToken( this ) && ( expires_at = _getAccessTokenExpiry( this ) ) ){
61 if( expires_at < X_Timer_now() + ( obj[ 'refreshMargin' ] || 300000 ) ){ // 寿命が5分を切った
62 this[ 'refreshToken' ]();
65 this[ 'asyncDispatch' ]( X_EVENT_SUCCESS );
68 this[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
71 // TODO canUse gadgetProxy
72 this[ 'listen' ]( [ X_EVENT_KILL_INSTANCE, X_EVENT_SUCCESS, X_EVENT_ERROR, X_EVENT_NEED_AUTH ], X_NET_OAUTH2_handleEvent );
79 * <dt>1 : <dd>認可用 window がポップアップ中
81 * <dt>3 : <dd>トークンのリフレッシュ中
87 return X_Pair_get( this ).oauth2State || 0;
91 * 認可用 window をポップアップする。ポップアップブロックが働かないように必ず pointer event 内で呼ぶこと。
93 'requestAuth' : function(){
95 // TODO pointer event 内か?チェック
97 if( X_NET_OAUTH2_authorizationWindow ) return;
99 pair = X_Pair_get( this );
101 if( pair.net || pair.oauth2State ) return;
103 url = pair[ 'authorizeEndpoint' ];
104 w = pair[ 'authorizeWindowWidth' ] || 500;
105 h = pair[ 'authorizeWindowHeight' ] || 500;
107 X_NET_OAUTH2_authorizationWindow = window.open(
108 url + ( ( url.indexOf( '?' ) !== -1 ) ? '&' : '?' ) + X_URL_objToParam(
110 'response_type' : 'code',
111 'client_id' : pair[ 'clientID' ],
112 'redirect_uri' : pair[ 'redirectURI' ],
113 'scope' : ( pair[ 'scopes' ] || []).join(' ')
118 + ',left=' + ( screen.width - w ) / 2
119 + ',top=' + ( screen.height - h ) / 2
120 + ',menubar=no,toolbar=no');
122 X_NET_OAUTH2_authorizationTimerID = X_Timer_add( 333, 0, this, X_Net_OAuth2_detectAuthPopup );
124 pair.oauth2State = 1;
126 this[ 'asyncDispatch' ]( { type : X_EVENT_PROGRESS, message : 'Start to auth.' } );
130 * 認可プロセスのキャンセル。ポップアップを閉じて認可用の通信は中断する。
132 'cancelAuth' : function(){
133 var pair = X_Pair_get( this );
136 pair.net[ 'kill' ]();
140 if( pair.oauth2State !== 1 ){
144 // http://kojikoji75.hatenablog.com/entry/2013/12/15/223839
145 X_NET_OAUTH2_authorizationWindow && X_NET_OAUTH2_authorizationWindow.open( 'about:blank', '_self' ).close();
146 X_NET_OAUTH2_authorizationWindow = null;
148 X_NET_OAUTH2_authorizationTimerID && X_Timer_remove( X_NET_OAUTH2_authorizationTimerID );
149 X_NET_OAUTH2_authorizationTimerID = 0;
151 this[ 'asyncDispatch' ]( X_EVENT_CANCELED );
157 'refreshToken' : function(){
158 var pair = X_Pair_get( this );
160 if( pair.net ) return;
162 if( pair.refreshTimerID ){
163 X_Timer_remove( pair.refreshTimerID );
164 delete pair.refreshTimerID;
167 pair.oauth2State = 3;
170 'xhr' : pair[ 'tokenEndpoint' ],
171 'postdata' : X_URL_objToParam({
172 'client_id' : pair[ 'clientID' ],
173 'client_secret' : pair[ 'clientSecret' ],
174 'grant_type' : 'refresh_token',
175 'refresh_token' : _getRefreshToken( this )
179 'Accept' : 'application/json',
180 'Content-Type' : 'application/x-www-form-urlencoded'
183 } ).listenOnce( [ X_EVENT_SUCCESS, X_EVENT_ERROR ], this, X_Net_OAuth2_responceHandler );
185 this[ 'asyncDispatch' ]( { type : X_EVENT_PROGRESS, message : 'Start to refresh token.' } );
190 function X_NET_OAUTH2_handleEvent( e ){
191 var pair = X_Pair_get( this );
194 case X_EVENT_KILL_INSTANCE :
195 this[ 'cancelAuth' ]();
198 case X_EVENT_NEED_AUTH :
199 pair.refreshTimerID && X_Timer_remove( pair.refreshTimerID );
202 case X_EVENT_SUCCESS :
203 pair.refreshTimerID && X_Timer_remove( pair.refreshTimerID );
204 if( _getRefreshToken( this ) ){
206 pair.refreshTimerID = X_Timer_once( _getAccessTokenExpiry( this ) - X_Timer_now() - pair[ 'refreshMargin' ], this, this[ 'refreshToken' ] );
211 function X_Net_OAuth2_detectAuthPopup(){
212 var closed, search, pair = X_Pair_get( this );
214 if( X_NET_OAUTH2_authorizationWindow.closed ){
215 pair.oauth2State = 0;
218 this[ 'asyncDispatch' ]( X_EVENT_CANCELED );
220 if( search = X_NET_OAUTH2_detection( X_NET_OAUTH2_authorizationWindow ) ){
221 pair = X_Pair_get( this );
222 pair.code = X_URL_ParamToObj( search.slice( 1 ) )[ 'code' ];
224 X_NET_OAUTH2_authorizationWindow.open( 'about:blank', '_self' ).close();
227 X_Net_OAuth2_authorizationCode( this, pair );
229 pair.oauth2State = 2;
230 this[ 'asyncDispatch' ]( { type : X_EVENT_PROGRESS, message : 'Get code success, then authorization code.' } );
234 X_NET_OAUTH2_authorizationWindow = null;
235 X_NET_OAUTH2_authorizationTimerID = 0;
237 return X_Callback_UN_LISTEN;
241 function X_Net_OAuth2_authorizationCode( oauth2, pair ){
243 'xhr' : pair[ 'tokenEndpoint' ],
244 'postdata' : X_URL_objToParam({
245 'client_id' : pair[ 'clientID' ],
246 'client_secret' : pair[ 'clientSecret' ],
247 'grant_type' : 'authorization_code',
249 'redirect_uri' : pair[ 'redirectURI' ]
253 'Accept' : 'application/json',
254 'Content-Type' : 'application/x-www-form-urlencoded'
257 } ).listenOnce( [ X_EVENT_SUCCESS, X_EVENT_ERROR ], oauth2, X_Net_OAuth2_responceHandler );
260 function X_Net_OAuth2_responceHandler( e ){
262 pair = X_Pair_get( this ),
263 isRefresh = pair.oauth2State === 3;
268 case X_EVENT_SUCCESS :
269 if( isRefresh && data.error ){
270 _removeRefreshToken( this );
271 pair.oauth2State = 0;
272 this[ 'asyncDispatch' ]( { type : X_EVENT_ERROR, message : 'Refresh access token error.' + data.error, data : data } );
273 this[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
277 pair.oauth2State = 0;
278 this[ 'asyncDispatch' ]( { type : X_EVENT_ERROR, message : 'Get new access token error.' + data.error, data : data } );
279 this[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
283 _setAccessToken( this, data[ 'access_token' ] || '' );
284 _setRefreshToken( this, data[ 'refresh_token' ] || '' );
286 if( data[ 'expires_in' ] ){
287 _setAccessTokenExpiry( this, X_Timer_now() + data[ 'expires_in' ] * 1000 );
289 if( _getAccessTokenExpiry( this ) ){
290 _removeAccessTokenExpiry( this );
293 pair.oauth2State = 4;
294 this[ 'asyncDispatch' ]( { type : X_EVENT_SUCCESS, message : isRefresh ? 'Refresh access token success.' : 'Get new access token success.' } );
299 // other error, not auth
300 pair.oauth2State = 0;
301 this[ 'asyncDispatch' ]( { type : X_EVENT_ERROR, message : 'Refresh access token error.' } );
302 _removeRefreshToken( this );
303 this[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
305 if( _getAuthMechanism( this ) === 'param' ){
306 pair.oauth2State = 0;
307 this[ 'asyncDispatch' ]( { type : X_EVENT_ERROR, message : 'network-error' } );
309 pair.oauth2State = 0;
310 _setAuthMechanism( this, 'param' );
311 this[ 'asyncDispatch' ]( { type : X_EVENT_PROGRESS, message : 'Refresh access token failed. retry header -> param. ' } );
313 X_Net_OAuth2_authorizationCode( this, pair );
319 function X_NET_OAUTH2_onXHR401Error( oauth2, e ){
321 headers = e[ 'headers' ],
322 xhr, bearerParams, headersExposed = false;
324 if( _getAuthMechanism( oauth2 ) !== 'param' ){
325 xhr = X_NET_currentWrapper[ '_rawObject' ];
326 headersExposed = !X_Net_XHR_createXDR || !!headers; // this is a hack for Firefox and IE
327 bearerParams = headersExposed && ( headers[ 'WWW-Authenticate' ] || headers[ 'www-authenticate' ] );
328 X_Type_isArray( bearerParams ) && ( bearerParams = bearerParams.join( '\n' ) );
331 // http://d.hatena.ne.jp/ritou/20110402/1301679908
332 if ( bearerParams && bearerParams.indexOf( ' error=' ) === -1 ) {
333 pair.oauth2State = 0;
334 oauth2[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
336 if ((( bearerParams && bearerParams.indexOf( 'invalid_token' ) !== -1 ) || !headersExposed) && _getRefreshToken( oauth2 ) ) {
337 _removeAccessToken( oauth2 ); // It doesn't work any more.
338 pair.oauth2State = 3;
339 oauth2[ 'refreshToken' ]();
341 //if (!headersExposed && !_getRefreshToken( oauth2 )) {
342 _removeAccessToken( oauth2 ); // It doesn't work any more.
343 pair.oauth2State = 0;
344 oauth2[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
348 function X_NET_OAUTH2_updateRequest( oauth2, request ){
349 var token = _getAccessToken( oauth2 ),
350 mechanism = _getAuthMechanism( oauth2 ),
351 url = request[ 'url' ],
354 if( token && mechanism === 'param' ){
355 request[ 'url' ] = url + ((url.indexOf('?') !== -1) ? '&' : '?') + 'bearer_token=' + encodeURIComponent( token );
358 if( token && ( !mechanism || mechanism === 'header' ) ){
359 headers = request[ 'headers' ] || ( request[ 'headers' ] = {} );
360 headers[ 'Authorization' ] = 'Bearer ' + token;
364 function _getAccessToken( that ){ return updateLocalStorage( '', that, 'accessToken' ); }
365 function _getRefreshToken( that){ return updateLocalStorage( '', that, 'refreshToken' ); }
366 function _getAccessTokenExpiry( that ){ return parseInt( updateLocalStorage( '', that, 'tokenExpiry' ) ) || 0; }
367 function _getAuthMechanism( that ){
368 // TODO use gadget | flash ...
369 // IE's XDomainRequest doesn't support sending headers, so don't try.
370 return X_Net_XHR_createXDR ? 'param' : updateLocalStorage( '', that, 'AuthMechanism' );
372 function _setAccessToken( that, value ){ updateLocalStorage( '+', that, 'accessToken' , value); }
373 function _setRefreshToken( that, value ){ updateLocalStorage( '+', that, 'refreshToken', value); }
374 function _setAccessTokenExpiry( that, value ){ updateLocalStorage( '+', that, 'tokenExpiry', value); }
375 function _setAuthMechanism( that, value ){ updateLocalStorage( '+', that, 'AuthMechanism', value); }
377 function _removeAccessToken( that ){ updateLocalStorage( '-', that, 'accessToken' ); }
378 function _removeRefreshToken( that ){ updateLocalStorage( '-', that, 'refreshToken' ); }
379 function _removeAccessTokenExpiry( that ){ updateLocalStorage( '-', that, 'tokenExpiry' ); }
380 function _removeAuthMechanism( that ){ updateLocalStorage( '-', that, 'AuthMechanism' ); }
382 function updateLocalStorage( cmd, that, name, value ){
383 var action = cmd === '+' ? 'setItem' : cmd === '-' ? 'removeItem' : 'getItem',
386 if( window.localStorage ){
387 return window.localStorage[ action ]( X_Pair_get( that )[ 'clientID' ] + name, value );
390 pair = X_Pair_get( that );
393 pair[ name ] = value;
396 if( pair[ name ] !== undefined ) delete pair[ name ];