OSDN Git Service

5bb2c1cad390c2a44bf2d5de4420ef6940857c23
[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_clone( obj );
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' ] || 300000 ) ){ // 寿命が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                                 X_NET_OAUTH2_authorizationWindow && X_NET_OAUTH2_authorizationWindow.open( 'about:blank', '_self' ).close();
151                                 X_NET_OAUTH2_authorizationWindow  = null;
152                                 
153                                 X_NET_OAUTH2_authorizationTimerID && X_Timer_remove( X_NET_OAUTH2_authorizationTimerID );
154                                 X_NET_OAUTH2_authorizationTimerID = 0;
155                                 
156                                 this[ 'asyncDispatch' ]( X_EVENT_CANCELED );
157                         },
158                         
159                         /**
160                          * アクセストークンのリフレッシュ。
161                          */
162                         'refreshToken' : function(){
163                                 var pair = X_Pair_get( this );
164                                 
165                                 if( pair.net ) return;
166                                 
167                                 if( pair.refreshTimerID ){
168                                         X_Timer_remove( pair.refreshTimerID );
169                                         delete pair.refreshTimerID;
170                                 };
171                                 
172                                 pair.oauth2State = 3;
173                                 
174                                 pair.net = X.Net( {
175                                         'xhr'      : pair[ 'tokenEndpoint' ],
176                                         'postdata' : X_URL_objToParam({
177                                                 'client_id'     : pair[ 'clientID' ],
178                                                 'client_secret' : pair[ 'clientSecret' ],
179                                                 'grant_type'    : 'refresh_token',
180                                                 'refresh_token' : X_OAuth2_getRefreshToken( this )
181                                         }),
182                                         'dataType' : 'json',
183                                         'headers'  : {
184                                                                         'Accept'       : 'application/json',
185                                                                         'Content-Type' : 'application/x-www-form-urlencoded'
186                                                                 },
187                                         'test'     : 'gadget' // canuse
188                                 } ).listenOnce( [ X_EVENT_SUCCESS, X_EVENT_ERROR ], this, X_Net_OAuth2_responceHandler );
189                                 
190                                 this[ 'asyncDispatch' ]( { type : X_EVENT_PROGRESS, message : 'Start to refresh token.' } );
191                         }
192                 }
193         );
194
195 function X_NET_OAUTH2_handleEvent( e ){
196         var pair = X_Pair_get( this );
197         
198         switch( e.type ){
199                 case X_EVENT_KILL_INSTANCE :
200                         this[ 'cancelAuth' ]();
201                 
202                 case X_EVENT_ERROR :
203                 case X_EVENT_NEED_AUTH :
204                         pair.refreshTimerID && X_Timer_remove( pair.refreshTimerID );
205                         break;
206                         
207                 case X_EVENT_SUCCESS :
208                         pair.refreshTimerID && X_Timer_remove( pair.refreshTimerID );
209                         if( X_OAuth2_getRefreshToken( this ) ){
210                                 // 自動リフレッシュ
211                                 pair.refreshTimerID = X_Timer_once( X_OAuth2_getAccessTokenExpiry( this ) - X_Timer_now() - pair[ 'refreshMargin' ], this, this[ 'refreshToken' ] );
212                         };
213         };
214 };
215
216 function X_Net_OAuth2_detectAuthPopup(){
217         var closed, search, pair = X_Pair_get( this );
218         
219         if( X_NET_OAUTH2_authorizationWindow.closed ){
220                 pair.oauth2State = 0;
221                 closed = true;
222
223                 this[ 'asyncDispatch' ]( X_EVENT_CANCELED );
224         } else
225         if( search = X_NET_OAUTH2_detection( X_NET_OAUTH2_authorizationWindow ) ){
226                 pair      = X_Pair_get( this );
227                 pair.code = X_URL_ParamToObj( search.slice( 1 ) )[ 'code' ];
228
229                 X_NET_OAUTH2_authorizationWindow.open( 'about:blank', '_self' ).close();
230                 closed = true;
231
232                 X_Net_OAuth2_authorizationCode( this, pair );
233                 
234                 pair.oauth2State = 2;
235                 this[ 'asyncDispatch' ]( { type : X_EVENT_PROGRESS, message : 'Get code success, then authorization code.' } );
236         };
237         
238         if( closed ){
239                 X_NET_OAUTH2_authorizationWindow  = null;
240                 X_NET_OAUTH2_authorizationTimerID = 0;
241                 
242                 return X_Callback_UN_LISTEN;
243         };
244 };
245
246 function X_Net_OAuth2_authorizationCode( oauth2, pair ){        
247         pair.net = X.Net( {
248                 'xhr'      : pair[ 'tokenEndpoint' ],
249                 'postdata' : X_URL_objToParam({
250                         'client_id'     : pair[ 'clientID' ],
251                         'client_secret' : pair[ 'clientSecret' ],
252                         'grant_type'    : 'authorization_code',
253                         'code'          : pair.code,
254                         'redirect_uri'  : pair[ 'redirectURI' ]
255                 }),
256                 'dataType' : 'json',
257                 'headers'  : {
258                         'Accept'       : 'application/json',
259                         'Content-Type' : 'application/x-www-form-urlencoded'
260                 },
261                 'test'     : 'gadget'
262         } ).listenOnce( [ X_EVENT_SUCCESS, X_EVENT_ERROR ], oauth2, X_Net_OAuth2_responceHandler );
263 };
264
265 function X_Net_OAuth2_responceHandler( e ){
266         var data = e.response,
267                 pair = X_Pair_get( this ),
268                 isRefresh = pair.oauth2State === 3;
269         
270         delete pair.net;
271         
272         switch( e.type ){
273                 case X_EVENT_SUCCESS :
274                         if( isRefresh && data.error ){
275                                 X_OAuth2_removeRefreshToken( this );
276                                 pair.oauth2State = 0;
277                                 this[ 'asyncDispatch' ]( { type : X_EVENT_ERROR, message : 'Refresh access token error.' } );
278                                 this[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
279                                 return;
280                         } else
281                         if( data.error ){
282                                 pair.oauth2State = 0;
283                                 this[ 'asyncDispatch' ]( { type : X_EVENT_ERROR, message : 'Get new access token error.' } );
284                                 this[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
285                                 return;
286                         };
287                         
288                         X_OAuth2_setAccessToken( this, data[ 'access_token' ] || '' );
289                         X_OAuth2_setRefreshToken( this, data[ 'refresh_token' ] || '' );
290                         
291                         if( data[ 'expires_in' ] ){
292                                 X_OAuth2_setAccessTokenExpiry( this, X_Timer_now() + data[ 'expires_in' ] * 1000 );
293                         } else
294                         if( X_OAuth2_getAccessTokenExpiry( this ) ){
295                                 X_OAuth2_removeAccessTokenExpiry( this );
296                         };
297                         
298                         pair.oauth2State = 4;
299                         
300                         if( pair.lazyRequests && pair.lazyRequests.length ){
301                                 X_NET_QUEUE_LIST.push.apply( X_NET_QUEUE_LIST, pair.lazyRequests );
302                                 pair.lazyRequests.length = 0;                           
303                         };
304
305                         this[ 'asyncDispatch' ]( { type : X_EVENT_SUCCESS, message : isRefresh ? 'Refresh access token success.' : 'Get new access token success.' } );
306                         break;
307                         
308                 case X_EVENT_ERROR :
309                         if( isRefresh ){
310                                 // other error, not auth
311                                 pair.oauth2State = 0;
312                                 this[ 'asyncDispatch' ]( { type : X_EVENT_ERROR, message : 'Refresh access token error.' } );
313                                 X_OAuth2_removeRefreshToken( this );
314                                 this[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
315                         } else
316                         if( X_OAuth2_getAuthMechanism( this ) === 'param' ){
317                                 pair.oauth2State = 0;
318                                 this[ 'asyncDispatch' ]( { type : X_EVENT_ERROR, message : 'network-error' } );
319                         } else {
320                                 pair.oauth2State = 0;
321                                 X_OAuth2_setAuthMechanism( this, 'param' );
322                                 this[ 'asyncDispatch' ]( { type : X_EVENT_PROGRESS, message : 'Refresh access token failed. retry header -> param. ' } );
323                                 // retry
324                                 X_Net_OAuth2_authorizationCode( this, pair );
325                         };
326                         break;
327         };
328 };
329
330 function X_NET_OAUTH2_onXHR401Error( oauth2, e ){
331         var pair = this,
332                 headers = e[ 'headers' ],
333                 bearerParams, headersExposed = false;
334         
335         if( X_OAuth2_getAuthMechanism( oauth2 ) !== 'param' ){
336                 headersExposed = !X_NET_currentWrapper.isXDR || !!headers; // this is a hack for Firefox and IE
337                 bearerParams   = headersExposed && ( headers[ 'WWW-Authenticate' ] || headers[ 'www-authenticate' ] );
338                 X_Type_isArray( bearerParams ) && ( bearerParams = bearerParams.join( '\n' ) );
339         };
340         
341         // http://d.hatena.ne.jp/ritou/20110402/1301679908
342         if ( bearerParams && bearerParams.indexOf( ' error=' ) === -1 ) { // bearerParams.error == undefined
343                 pair.oauth2State = 0;
344                 oauth2[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
345         } else
346         if ((( bearerParams && bearerParams.indexOf( 'invalid_token' ) !== -1 ) || !headersExposed) && X_OAuth2_getRefreshToken( oauth2 ) ) {
347                 X_OAuth2_removeAccessToken( oauth2 ); // It doesn't work any more.
348                 pair.oauth2State = 3;
349                 oauth2[ 'refreshToken' ]();
350         } else {
351                 X_OAuth2_removeAccessToken( oauth2 ); // It doesn't work any more.
352                 pair.oauth2State = 0;
353                 oauth2[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
354         };
355 };
356
357 function X_NET_OAUTH2_updateRequest( oauth2, request ){
358         var token     = X_OAuth2_getAccessToken( oauth2 ),
359                 mechanism = X_OAuth2_getAuthMechanism( oauth2 ),
360                 url       = request[ 'url' ],
361                 headers;
362
363         if( token && mechanism === 'param' ){
364                 request[ 'url' ] = X_URL_create( url, { 'bearer_token' : encodeURIComponent( token ) } );
365         };
366         
367         if( token && ( !mechanism || mechanism === 'header' ) ){
368                 headers = request[ 'headers' ] || ( request[ 'headers' ] = {} );
369                 headers[ 'Authorization' ] = 'Bearer ' + token;
370         };
371 };
372
373 function X_OAuth2_getAccessToken( that ){ return X_OAuth2_updateLocalStorage( '', that, 'accessToken' ); }
374 function X_OAuth2_getRefreshToken( that ){ return X_OAuth2_updateLocalStorage( '', that, 'refreshToken' ); }
375 function X_OAuth2_getAccessTokenExpiry( that ){ return parseInt( X_OAuth2_updateLocalStorage( '', that, 'tokenExpiry' ) ) || 0; }
376 function X_OAuth2_getAuthMechanism( that ){
377                 // TODO use gadget | flash ...
378                 // IE's XDomainRequest doesn't support sending headers, so don't try.
379                 return ( X_NET_currentWrapper === X_NET_XHRWrapper ) && X_Net_XHR_createXDR ? 'param' : X_OAuth2_updateLocalStorage( '', that, 'AuthMechanism' );
380         }
381 function X_OAuth2_setAccessToken( that, value ){ X_OAuth2_updateLocalStorage( '+', that, 'accessToken' , value); }
382 function X_OAuth2_setRefreshToken( that, value ){ X_OAuth2_updateLocalStorage( '+', that, 'refreshToken', value); }
383 function X_OAuth2_setAccessTokenExpiry( that, value ){ X_OAuth2_updateLocalStorage( '+', that, 'tokenExpiry', value); }
384 function X_OAuth2_setAuthMechanism( that, value ){ X_OAuth2_updateLocalStorage( '+', that, 'AuthMechanism', value); }
385
386 function X_OAuth2_removeAccessToken( that ){ X_OAuth2_updateLocalStorage( '-', that, 'accessToken' ); }
387 function X_OAuth2_removeRefreshToken( that ){ X_OAuth2_updateLocalStorage( '-', that, 'refreshToken' ); }
388 function X_OAuth2_removeAccessTokenExpiry( that ){ X_OAuth2_updateLocalStorage( '-', that, 'tokenExpiry' ); }
389 function X_OAuth2_removeAuthMechanism( that ){ X_OAuth2_updateLocalStorage( '-', that, 'AuthMechanism' ); }
390         
391 function X_OAuth2_updateLocalStorage( cmd, that, name, value ){
392         var action = cmd === '+' ? 'setItem' : cmd === '-' ? 'removeItem' : 'getItem',
393                 pair;
394         
395         if( window.localStorage ){
396                 return window.localStorage[ action ]( X_Pair_get( that )[ 'clientID' ] + name, value );
397         };
398         
399         pair = X_Pair_get( that );
400         switch( cmd ){
401                 case '+' :
402                         pair[ name ] = value;
403                         break;
404                 case '-' :
405                         if( pair[ name ] !== undefined ) delete pair[ name ];
406         };
407         return pair[ name ];
408 };
409
410 //}+oauth2