OSDN Git Service

Version 0.6.156, add X.UI.Repeater.
[pettanr/clientJs.git] / 0.6.x / js / 06_net / 10_XOAuth2.js
1
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;
5
6 /**
7  * イベント
8  * <dl>
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 に切替
14  * </dl>
15  * 
16  * original :
17  *  oauth2.js , <opendata@oucs.ox.ac.uk>
18  * 
19  * @alias X.OAuth2
20  * @class OAuth2 サービスを定義し接続状況をモニタする。適宜にトークンのアップデートなどを行う
21  * @constructs OAuth2
22  * @extends {EventDispatcher}
23  * @example // OAuth2 サービスの定義
24 oauth2 = X.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 );
34
35 // XHR 時に oauth2 を渡す
36 X.Net( {
37         xhr      : 'https://www.googleapis.com/blogger/v3/users/self/blogs',
38         dataType : 'json',
39         auth     : oauth2,
40         test     : 'gadget' // http -> https:xProtocol なリクエストのため、google ガジェットを proxy に使用
41         } )
42         .listen( [ X.Event.SUCCESS, X.Event.ERROR, X.Event.PROGRESS ], updateOAuth2State );
43  */
44 X[ 'OAuth2' ] = X_EventDispatcher[ 'inherits' ](
45                 'X.OAuth2',
46                 X_Class.NONE,
47                 
48                 /** @lends OAuth2.prototype */
49                 {
50                         'Constructor' : function( obj ){
51                                 var expires_at;
52                                 
53                                 obj = X_Object_clone( obj );
54                                 
55                                 X_Pair_create( this, obj );
56                                 
57                                 obj.onAuthError   = X_NET_OAUTH2_onXHR401Error;
58                                 obj.updateRequest = X_NET_OAUTH2_updateRequest;                         
59                                 
60                                 if( _getAccessToken( this ) && ( expires_at = _getAccessTokenExpiry( this ) ) ){
61                                         if( expires_at < X_Timer_now() + 300000 ){ // 寿命が5分を切った
62                                                 this[ 'refreshToken' ]();
63                                         } else {
64                                                 obj.oauth2State = 4;
65                                                 this[ 'asyncDispatch' ]( X_EVENT_SUCCESS );                                             
66                                         };
67                                 } else {
68                                         this[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
69                                 };
70                                 
71                                 // TODO canUse
72                                 // TODO kill の cancel
73                         },
74
75                         /**
76                          * OAuth2 の状態。
77                          * <dl>
78                          * <dt>0 : <dd>未接続
79                          * <dt>1 : <dd>認可用 window がポップアップ中
80                          * <dt>2 : <dd>コードを認可中
81                          * <dt>3 : <dd>トークンのリフレッシュ中
82                          * <dt>4 : <dd>接続
83                          * </dl>
84                          * @return {number}
85                          */
86                         'state' : function(){
87                                 return X_Pair_get( this ).oauth2State || 0;
88                         },
89                         
90                         /**
91                          * 認可用 window をポップアップする。ポップアップブロックが働かないように必ず pointer event 内で呼ぶこと。
92                          */
93                         'requestAuth' : function(){
94                                 var url, w, h;
95                                 // TODO pointer event 内か?チェック
96                                 // 二つ以上の popup を作らない
97                                 if( X_NET_OAUTH2_authorizationWindow ) return;
98                                 
99                                 pair = X_Pair_get( this );
100                                 
101                                 if( pair.net || pair.oauth2State ) return;
102                                 
103                                 url = pair[ 'authorizeEndpoint' ];
104                                 w   = pair[ 'authorizeWindowWidth' ]  || 500;
105                                 h   = pair[ 'authorizeWindowHeight' ] || 500;
106                                 
107                                 X_NET_OAUTH2_authorizationWindow = window.open(
108                                         url + ( ( url.indexOf( '?' ) !== -1 ) ? '&' : '?' ) + X_URL_objToParam(
109                                                 {
110                                                         'response_type' : 'code',
111                                                         'client_id'     : pair[ 'clientID' ],
112                                                         'redirect_uri'  : pair[ 'redirectURI' ],
113                                                         'scope'         : ( pair[ 'scopes' ] || []).join(' ')
114                                                 }
115                                         ), 'oauthauthorize',
116                                         'width=' + w
117                                         + ',height=' + h
118                                         + ',left=' + ( screen.width  - w ) / 2
119                                         + ',top='  + ( screen.height - h ) / 2
120                                         + ',menubar=no,toolbar=no');
121                                 
122                                 X_NET_OAUTH2_authorizationTimerID = X_Timer_add( 333, 0, this, X_Net_OAuth2_detectAuthPopup );
123                                 
124                                 pair.oauth2State = 1;
125                                 
126                                 this[ 'asyncDispatch' ]( { type : X_EVENT_PROGRESS, message : 'Start to auth.' } );
127                         },
128                         
129                         /**
130                          * 認可プロセスのキャンセル。ポップアップを閉じて認可用の通信は中断する。
131                          */
132                         'cancelAuth' : function(){
133                                 var pair = X_Pair_get( this );
134                                 
135                                 if( pair.net ){
136                                         pair.net[ 'kill' ]();
137                                         delete pair.net;
138                                 };
139                                 
140                                 // http://kojikoji75.hatenablog.com/entry/2013/12/15/223839
141                                 X_NET_OAUTH2_authorizationWindow && X_NET_OAUTH2_authorizationWindow.open( 'about:blank', '_self' ).close();
142                                 X_NET_OAUTH2_authorizationWindow  = null;
143                                 
144                                 X_NET_OAUTH2_authorizationTimerID && X_Timer_remove( X_NET_OAUTH2_authorizationTimerID );
145                                 X_NET_OAUTH2_authorizationTimerID = 0;
146                                 
147                                 this[ 'asyncDispatch' ]( X_EVENT_CANCELED );
148                         },
149                         
150                         /**
151                          * アクセストークンのリフレッシュ。
152                          */
153                         'refreshToken' : function(){
154                                 // TODO 自動リフレッシュ
155
156                                 var pair = X_Pair_get( this );
157                                 
158                                 if( pair.net ) return;
159                                 
160                                 pair.oauth2State = 3;
161                                 
162                                 pair.net = X.Net( {
163                                         'xhr'      : pair[ 'tokenEndpoint' ],
164                                         'postdata' : X_URL_objToParam({
165                                                 'client_id'     : pair[ 'clientID' ],
166                                                 'client_secret' : pair[ 'clientSecret' ],
167                                                 'grant_type'    : 'refresh_token',
168                                                 'refresh_token' : _getRefreshToken( this )
169                                         }),
170                                         'dataType' : 'json',
171                                         'headers'  : {
172                                                                         'Accept'       : 'application/json',
173                                                                         'Content-Type' : 'application/x-www-form-urlencoded'
174                                                                 },
175                                         'test'     : 'gadget'
176                                 } ).listenOnce( [ X_EVENT_SUCCESS, X_EVENT_ERROR ], this, X_Net_OAuth2_responceHandler );
177                                 
178                                 this[ 'asyncDispatch' ]( { type : X_EVENT_PROGRESS, message : 'Start to refresh token.' } );
179                         }
180                 }
181         );
182
183 function X_Net_OAuth2_detectAuthPopup(){
184         var closed, search, pair = X_Pair_get( this );
185         
186         if( X_NET_OAUTH2_authorizationWindow.closed ){
187                 pair.oauth2State = 0;
188                 closed = true;
189
190                 this[ 'asyncDispatch' ]( X_EVENT_CANCELED );
191         } else
192         if( search = X_NET_OAUTH2_detection( X_NET_OAUTH2_authorizationWindow ) ){
193                 pair      = X_Pair_get( this );
194                 pair.code = X_URL_ParamToObj( search.slice( 1 ) )[ 'code' ];
195
196                 X_NET_OAUTH2_authorizationWindow.open( 'about:blank', '_self' ).close();
197                 closed = true;
198
199                 X_Net_OAuth2_authorizationCode( this, pair );
200                 
201                 pair.oauth2State = 2;
202                 this[ 'asyncDispatch' ]( { type : X_EVENT_PROGRESS, message : 'Get code success, then authorization code.' } );
203         };
204         
205         if( closed ){
206                 X_NET_OAUTH2_authorizationWindow  = null;
207                 X_NET_OAUTH2_authorizationTimerID = 0;
208                 
209                 return X_Callback_UN_LISTEN;    
210         };
211 };
212
213 function X_Net_OAuth2_authorizationCode( oauth2, pair ){        
214         pair.net = X.Net( {
215                 'xhr'      : pair[ 'tokenEndpoint' ],
216                 'postdata' : X_URL_objToParam({
217                         'client_id'     : pair[ 'clientID' ],
218                         'client_secret' : pair[ 'clientSecret' ],
219                         'grant_type'    : 'authorization_code',
220                         'code'          : pair.code,
221                         'redirect_uri'  : pair[ 'redirectURI' ]
222                 }),
223                 'dataType' : 'json',
224                 'headers'  : {
225                         'Accept'       : 'application/json',
226                         'Content-Type' : 'application/x-www-form-urlencoded'
227                 },
228                 'test'     : 'gadget'
229         } ).listenOnce( [ X_EVENT_SUCCESS, X_EVENT_ERROR ], oauth2, X_Net_OAuth2_responceHandler );
230 };
231
232 function X_Net_OAuth2_responceHandler( e ){
233         var data = e.data,
234                 pair = X_Pair_get( this ),
235                 isRefresh = pair.oauth2State === 3;
236         
237         delete pair.net;
238         
239         switch( e.type ){
240                 case X_EVENT_SUCCESS :
241                         if( isRefresh && data.error ){
242                                 _removeRefreshToken( this );
243                                 pair.oauth2State = 0;
244                                 this[ 'asyncDispatch' ]( { type : X_EVENT_ERROR, message : 'Refresh access token error.' + data.error, data : data } );
245                                 this[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
246                                 return;
247                         } else
248                         if( data.error ){
249                                 pair.oauth2State = 0;
250                                 this[ 'asyncDispatch' ]( { type : X_EVENT_ERROR, message : 'Get new access token error.' + data.error, data : data } );
251                                 this[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
252                                 return;
253                         };
254                         
255                         _setAccessToken( this, data[ 'access_token' ] || '' );
256                         _setRefreshToken( this, data[ 'refresh_token' ] || '' );
257                         
258                         if( data[ 'expires_in' ] ){
259                                 _setAccessTokenExpiry( this, X_Timer_now() + data[ 'expires_in' ] * 1000 );
260                         } else
261                         if( _getAccessTokenExpiry( this ) ){
262                                 _removeAccessTokenExpiry( this );
263                         };
264                         
265                         pair.oauth2State = 4;
266                         this[ 'asyncDispatch' ]( { type : X_EVENT_SUCCESS, message : isRefresh ? 'Refresh access token success.' : 'Get new access token success.' } );
267                         break;
268                         
269                 case X_EVENT_ERROR :
270                         if( isRefresh ){
271                                 // other error, not auth
272                                 pair.oauth2State = 0;
273                                 this[ 'asyncDispatch' ]( { type : X_EVENT_ERROR, message : 'Refresh access token error.' } );
274                                 _removeRefreshToken( this );
275                                 this[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
276                         } else
277                         if( _getAuthMechanism( this ) === 'param' ){
278                                 pair.oauth2State = 0;
279                                 this[ 'asyncDispatch' ]( { type : X_EVENT_ERROR, message : 'network-error' } );
280                         } else {
281                                 pair.oauth2State = 0;
282                                 _setAuthMechanism( this, 'param' );
283                                 this[ 'asyncDispatch' ]( { type : X_EVENT_PROGRESS, message : 'Refresh access token failed. retry header -> param. ' } );
284                                 // retry
285                                 X_Net_OAuth2_authorizationCode( this, pair );
286                         };
287                         break;
288         };
289 };
290
291 function X_NET_OAUTH2_onXHR401Error( oauth2, e ){
292         var pair = this,
293                 headers = e[ 'headers' ],
294                 xhr, bearerParams, headersExposed = false;
295         
296         if( _getAuthMechanism( oauth2 ) !== 'param' ){
297                 xhr            = X_NET_currentWrapper[ '_rawObject' ];
298                 headersExposed = !X_Net_XHR_createXDR || !!headers; // this is a hack for Firefox and IE
299                 bearerParams   = headersExposed && ( headers[ 'WWW-Authenticate' ] || headers[ 'www-authenticate' ] );
300                 X_Type_isArray( bearerParams ) && ( bearerParams = bearerParams.join( '\n' ) );
301         };
302         
303         // http://d.hatena.ne.jp/ritou/20110402/1301679908
304         if ( bearerParams && bearerParams.indexOf( ' error=' ) === -1 ) {
305                 pair.oauth2State = 0;
306                 oauth2[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
307         } else
308         if ((( bearerParams && bearerParams.indexOf( 'invalid_token' ) !== -1 ) || !headersExposed) && _getRefreshToken( oauth2 ) ) {
309                 _removeAccessToken( oauth2 ); // It doesn't work any more.
310                 pair.oauth2State = 3;
311                 oauth2[ 'refreshToken' ]();
312         } else {
313         //if (!headersExposed && !_getRefreshToken( oauth2 )) {
314                 _removeAccessToken( oauth2 ); // It doesn't work any more.
315                 pair.oauth2State = 0;
316                 oauth2[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
317         };
318 };
319
320 function X_NET_OAUTH2_updateRequest( oauth2, request ){
321         var token     = _getAccessToken( oauth2 ),
322                 mechanism = _getAuthMechanism( oauth2 ),
323                 url       = request[ 'url' ],
324                 headers;
325
326         if( token && mechanism === 'param' ){
327                 request[ 'url' ] = url + ((url.indexOf('?') !== -1) ? '&' : '?') + 'bearer_token=' + encodeURIComponent( token );
328         };
329         
330         if( token && ( !mechanism || mechanism === 'header' ) ){
331                 headers = request[ 'headers' ] || ( request[ 'headers' ] = {} );
332                 headers[ 'Authorization' ] = 'Bearer ' + token;
333         };
334 };
335
336 function _getAccessToken( that ){ return updateLocalStorage( '', that, 'accessToken' ); }
337 function _getRefreshToken( that){ return updateLocalStorage( '', that, 'refreshToken' ); }
338 function _getAccessTokenExpiry( that ){ return parseInt( updateLocalStorage( '', that, 'tokenExpiry' ) ) || 0; }
339 function _getAuthMechanism( that ){
340                 // TODO use gadget | flash ...
341                 // IE's XDomainRequest doesn't support sending headers, so don't try.
342                 return X_Net_XHR_createXDR ? 'param' : updateLocalStorage( '', that, 'AuthMechanism' );
343         }
344 function _setAccessToken( that, value ){ updateLocalStorage( '+', that, 'accessToken' , value); }
345 function _setRefreshToken( that, value ){ updateLocalStorage( '+', that, 'refreshToken', value); }
346 function _setAccessTokenExpiry( that, value ){ updateLocalStorage( '+', that, 'tokenExpiry', value); }
347 function _setAuthMechanism( that, value ){ updateLocalStorage( '+', that, 'AuthMechanism', value); }
348
349 function _removeAccessToken( that ){ updateLocalStorage( '-', that, 'accessToken' ); }
350 function _removeRefreshToken( that ){ updateLocalStorage( '-', that, 'refreshToken' ); }
351 function _removeAccessTokenExpiry( that ){ updateLocalStorage( '-', that, 'tokenExpiry' ); }
352 function _removeAuthMechanism( that ){ updateLocalStorage( '-', that, 'AuthMechanism' ); }
353         
354 function updateLocalStorage( cmd, that, name, value ){
355         var action = cmd === '+' ? 'setItem' : cmd === '-' ? 'removeItem' : 'getItem',
356                 pair;
357         
358         if( window.localStorage ){
359                 return window.localStorage[ action ]( X_Pair_get( that )[ 'clientID' ] + name, value );
360         };
361         
362         pair = X_Pair_get( that );
363         switch( cmd ){
364                 case '+' :
365                         pair[ name ] = value;
366                         break;
367                 case '-' :
368                         if( pair[ name ] !== undefined ) delete pair[ name ];
369         };
370         return pair[ name ];
371 };
372