OSDN Git Service

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