OSDN Git Service

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