OSDN Git Service

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