OSDN Git Service

b6632be044b03c83238c13a4e82d883103df8138
[pettanr/clientJs.git] / 0.6.x / js / 06_net / 10_XOAuth2.js
1
2 //{+oauth2"OAuth2 サービスの定義"(OAuth2外部サービスを定義し、認可プロセス・xhrの署名を自動化します)[+xhr,+window]
3 var X_OAUTH2_authWindow,
4         X_OAUTH2_authTimerID;
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_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                          * <dl>
97                          * <dt>1 : <dd>認可用 window がポップアップ中(自身)
98                          * <dt>2 : <dd>コードを認可中
99                          * <dt>3 : <dd>トークンのリフレッシュ中
100                          * <dt>4 : <dd>接続
101                          * <dt>5 : <dd>他のOAuth2サービスの認可用 window がポップアップ中
102                          * </dl>
103                          * @return {number}
104                          */
105                         'requestAuth' : function(){
106                                 var e = X_EventDispatcher_CURRENT_EVENTS[ X_EventDispatcher_CURRENT_EVENTS.length - 1 ],
107                                         w, h;
108                                 
109                                 // TODO pointer event 内か?チェック
110                                 if( !e || !e[ 'pointerType' ] ){
111                                         alert( 'タッチイベント以外での popup! ' + ( e ? e.type : '' ) );
112                                         return;
113                                 };
114                                 
115                                 // 二つ以上の popup を作らない
116                                 if( X_OAUTH2_authWindow ) return;
117                                 
118                                 pair = X_Pair_get( this );
119                                 
120                                 if( pair.net || pair.oauth2State ) return;
121
122                                 w   = pair[ 'authorizeWindowWidth' ]  || 500;
123                                 h   = pair[ 'authorizeWindowHeight' ] || 500;
124
125                                 X_OAUTH2_authWindow = X_Window( {
126                                         'url' : X_URL_create( pair[ 'authorizeEndpoint' ],
127                                                         {
128                                                                 'response_type' : 'code',
129                                                                 'client_id'     : pair[ 'clientID' ],
130                                                                 'redirect_uri'  : pair[ 'redirectURI' ],
131                                                                 'scope'         : ( pair[ 'scopes' ] || [] ).join( ' ' )
132                                                         }
133                                                 ),
134                                         'name'   : 'oauthauthorize',
135                                         'params' : 'width=' + w
136                                                 + ',height=' + h
137                                                 + ',left=' + ( screen.width  - w ) / 2
138                                                 + ',top='  + ( screen.height - h ) / 2
139                                                 + ',menubar=no,toolbar=no'
140                                 } )[ 'listen' ]( X_EVENT_UNLOAD, this, X_OAuth2_detectAuthPopup );
141                                 
142                                 X_OAUTH2_authTimerID = X_Timer_add( 333, 0, this, X_OAuth2_detectAuthPopup );
143                                 
144                                 pair.oauth2State = 1;
145                                 
146                                 this[ 'asyncDispatch' ]( { type : X_EVENT_PROGRESS, message : 'Start to auth.' } );
147                         },
148                         
149                         /**
150                          * 認可プロセスのキャンセル。ポップアップを閉じて認可用の通信は中断する。
151                          */
152                         'cancelAuth' : function(){
153                                 var pair = X_Pair_get( this );
154                                 
155                                 if( pair.net ){
156                                         pair.net[ 'kill' ]();
157                                         delete pair.net;
158                                 };
159                                 
160                                 if( pair.oauth2State !== 1 ){
161                                         return;
162                                 };
163                                 
164                                 // http://kojikoji75.hatenablog.com/entry/2013/12/15/223839
165                                 if( X_OAUTH2_authWindow ){
166                                         X_OAUTH2_authWindow[ 'kill' ]();
167                                         X_OAUTH2_authWindow = null;                                     
168                                 };
169                                 
170                                 X_OAUTH2_authTimerID && X_Timer_remove( X_OAUTH2_authTimerID );
171                                 X_OAUTH2_authTimerID = 0;
172                                 
173                                 this[ 'asyncDispatch' ]( X_EVENT_CANCELED );
174                         },
175                         
176                         /**
177                          * アクセストークンのリフレッシュ。
178                          */
179                         'refreshToken' : function(){
180                                 var pair = X_Pair_get( this ),
181                                         refreshToken = X_OAuth2_getRefreshToken( this );
182                                 
183                                 if( !refreshToken ){
184                                         pair.oauth2State = 0;
185                                         this[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
186                                         return;
187                                 };
188                                 
189                                 if( pair.net ) return;
190                                 
191                                 if( pair.refreshTimerID ){
192                                         X_Timer_remove( pair.refreshTimerID );
193                                         delete pair.refreshTimerID;
194                                 };
195                                 
196                                 pair.oauth2State = 3;
197                                 
198                                 pair.net = X.Net( {
199                                         'xhr'      : pair[ 'tokenEndpoint' ],
200                                         'postdata' : X_URL_objToParam({
201                                                 'client_id'     : pair[ 'clientID' ],
202                                                 'client_secret' : pair[ 'clientSecret' ],
203                                                 'grant_type'    : 'refresh_token',
204                                                 'refresh_token' : refreshToken
205                                         }),
206                                         'dataType' : 'json',
207                                         'headers'  : {
208                                                                         'Accept'       : 'application/json',
209                                                                         'Content-Type' : 'application/x-www-form-urlencoded'
210                                                                 },
211                                         'test'     : 'gadget' // canuse
212                                 } ).listenOnce( [ X_EVENT_SUCCESS, X_EVENT_ERROR ], this, X_OAuth2_responceHandler );
213                                 
214                                 this[ 'asyncDispatch' ]( { type : X_EVENT_PROGRESS, message : 'Start to refresh token.' } );
215                         }
216                 }
217         );
218
219 function X_OAUTH2_handleEvent( e ){
220         var pair = X_Pair_get( this );
221         
222         switch( e.type ){
223                 case X_EVENT_KILL_INSTANCE :
224                         this[ 'cancelAuth' ]();
225                 
226                 case X_EVENT_ERROR :
227                 case X_EVENT_NEED_AUTH :
228                         pair.refreshTimerID && X_Timer_remove( pair.refreshTimerID );
229                         break;
230                         
231                 case X_EVENT_SUCCESS :
232                         pair.refreshTimerID && X_Timer_remove( pair.refreshTimerID );
233                         if( X_OAuth2_getRefreshToken( this ) ){
234                                 // 自動リフレッシュ
235                                 pair.refreshTimerID = X_Timer_once( X_OAuth2_getAccessTokenExpiry( this ) - X_Timer_now() - pair[ 'refreshMargin' ], this, this[ 'refreshToken' ] );
236                         };
237         };
238 };
239
240 function X_OAuth2_detectAuthPopup( e ){
241         var pair = X_Pair_get( this ),
242                 status, search;
243
244         if( X_OAUTH2_authWindow[ 'closed' ]() ){
245                 status = 0;
246                 this[ 'asyncDispatch' ]( X_EVENT_CANCELED );
247         } else
248         if( search = X_OAUTH2_authWindow[ 'find' ]( 'location>search' ) ){
249                 pair = X_Pair_get( this );
250                 pair.code = X_URL_paramToObj( search.slice( 1 ) )[ 'code' ];
251                 status = 2;
252                 X_OAuth2_authorizationCode( this, pair );
253                 this[ 'asyncDispatch' ]( { type : X_EVENT_PROGRESS, message : 'Get code success, then authorization code.' } );
254         };
255         
256         if( 0 <= status ){
257                 pair = pair || X_Pair_get( this );
258                 pair.oauth2State = status;
259                 
260                 X_OAUTH2_authWindow[ 'kill' ]();
261                 X_OAUTH2_authWindow  = null;
262                 X_OAUTH2_authTimerID = X_Timer_remove( X_OAUTH2_authTimerID );
263                 
264                 return X_CALLBACK_UN_LISTEN;
265         };
266 };
267
268 function X_OAuth2_authorizationCode( oauth2, pair ){    
269         pair.net = X.Net( {
270                 'xhr'      : pair[ 'tokenEndpoint' ],
271                 'postdata' : X_URL_objToParam({
272                         'client_id'     : pair[ 'clientID' ],
273                         'client_secret' : pair[ 'clientSecret' ],
274                         'grant_type'    : 'authorization_code',
275                         'code'          : pair.code,
276                         'redirect_uri'  : pair[ 'redirectURI' ]
277                 }),
278                 'dataType' : 'json',
279                 'headers'  : {
280                         'Accept'       : 'application/json',
281                         'Content-Type' : 'application/x-www-form-urlencoded'
282                 },
283                 'test'     : 'gadget'
284         } ).listenOnce( [ X_EVENT_SUCCESS, X_EVENT_ERROR ], oauth2, X_OAuth2_responceHandler );
285 };
286
287 function X_OAuth2_responceHandler( e ){
288         var data = e.response,
289                 pair = X_Pair_get( this ),
290                 isRefresh = pair.oauth2State === 3;
291         
292         delete pair.net;
293         
294         switch( e.type ){
295                 case X_EVENT_SUCCESS :
296                         if( isRefresh && data.error ){
297                                 X_OAuth2_removeRefreshToken( this );
298                                 pair.oauth2State = 0;
299                                 this[ 'asyncDispatch' ]( { type : X_EVENT_ERROR, message : 'Refresh access token error.' } );
300                                 this[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
301                                 return;
302                         } else
303                         if( data.error ){
304                                 pair.oauth2State = 0;
305                                 this[ 'asyncDispatch' ]( { type : X_EVENT_ERROR, message : 'Get new access token error.' } );
306                                 this[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
307                                 return;
308                         };
309                         
310                         X_OAuth2_setAccessToken( this, data[ 'access_token' ] || '' );
311                         ( !isRefresh || data[ 'refresh_token' ] ) && X_OAuth2_setRefreshToken( this, data[ 'refresh_token' ] || '' );
312                         
313                         if( data[ 'expires_in' ] ){
314                                 X_OAuth2_setAccessTokenExpiry( this, X_Timer_now() + data[ 'expires_in' ] * 1000 );
315                         } else
316                         if( X_OAuth2_getAccessTokenExpiry( this ) ){
317                                 X_OAuth2_removeAccessTokenExpiry( this );
318                         };
319                         
320                         pair.oauth2State = 4;
321                         
322                         if( pair.lazyRequests && pair.lazyRequests.length ){
323                                 //X_NET_QUEUE_LIST.push.apply( X_NET_QUEUE_LIST, pair.lazyRequests );
324                                 //pair.lazyRequests.length = 0;                         
325                         };
326
327                         this[ 'asyncDispatch' ]( { type : X_EVENT_SUCCESS, message : isRefresh ? 'Refresh access token success.' : 'Get new access token success.' } );
328                         break;
329                         
330                 case X_EVENT_ERROR :
331                         if( isRefresh ){
332                                 // other error, not auth
333                                 pair.oauth2State = 0;
334                                 this[ 'asyncDispatch' ]( { type : X_EVENT_ERROR, message : 'Refresh access token error.' } );
335                                 X_OAuth2_removeRefreshToken( this );
336                                 this[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
337                         } else
338                         if( X_OAuth2_getAuthMechanism( this ) === 'param' ){
339                                 pair.oauth2State = 0;
340                                 this[ 'asyncDispatch' ]( { type : X_EVENT_ERROR, message : 'network-error' } );
341                         } else {
342                                 pair.oauth2State = 0;
343                                 X_OAuth2_setAuthMechanism( this, 'param' );
344                                 this[ 'asyncDispatch' ]( { type : X_EVENT_PROGRESS, message : 'Refresh access token failed. retry header -> param. ' } );
345                                 // retry
346                                 X_OAuth2_authorizationCode( this, pair );
347                         };
348                         break;
349         };
350 };
351
352 function X_NET_OAUTH2_onXHR401Error( oauth2, e ){
353         var pair = this,
354                 headers = e[ 'headers' ],
355                 bearerParams, headersExposed = false;
356         
357         if( X_OAuth2_getAuthMechanism( oauth2 ) !== 'param' ){
358                 headersExposed = !X_NET_currentWrapper.isXDR || !!headers; // this is a hack for Firefox and IE
359                 bearerParams   = headersExposed && ( headers[ 'WWW-Authenticate' ] || headers[ 'www-authenticate' ] );
360                 X_Type_isArray( bearerParams ) && ( bearerParams = bearerParams.join( '\n' ) );
361         };
362         
363         // http://d.hatena.ne.jp/ritou/20110402/1301679908
364         if( bearerParams && bearerParams.indexOf( ' error=' ) === -1 ){ // bearerParams.error == undefined
365                 pair.oauth2State = 0;
366                 oauth2[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
367         } else
368         if( ( ( bearerParams && bearerParams.indexOf( 'invalid_token' ) !== -1 ) || !headersExposed ) && X_OAuth2_getRefreshToken( oauth2 ) ){
369                 X_OAuth2_removeAccessToken( oauth2 ); // It doesn't work any more.
370                 pair.oauth2State = 3;
371                 oauth2[ 'refreshToken' ]();
372         } else {
373                 X_OAuth2_removeAccessToken( oauth2 ); // It doesn't work any more.
374                 pair.oauth2State = 0;
375                 oauth2[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
376         };
377 };
378
379 function X_NET_OAUTH2_updateRequest( oauth2, request ){
380         var token     = X_OAuth2_getAccessToken( oauth2 ),
381                 mechanism = X_OAuth2_getAuthMechanism( oauth2 ),
382                 url       = request[ 'url' ],
383                 headers;
384
385         if( token && mechanism === 'param' ){
386                 request[ 'url' ] = X_URL_create( url, { 'bearer_token' : encodeURIComponent( token ) } );
387         };
388         
389         if( token && ( !mechanism || mechanism === 'header' ) ){
390                 headers = request[ 'headers' ] || ( request[ 'headers' ] = {} );
391                 headers[ 'Authorization' ] = 'Bearer ' + token;
392         };
393 };
394
395 function X_OAuth2_getAccessToken( that ){ return X_OAuth2_updateLocalStorage( '', that, 'accessToken' ); }
396 function X_OAuth2_getRefreshToken( that ){ return X_OAuth2_updateLocalStorage( '', that, 'refreshToken' ); }
397 function X_OAuth2_getAccessTokenExpiry( that ){ return parseFloat( X_OAuth2_updateLocalStorage( '', that, 'tokenExpiry' ) ) || 0; }
398 function X_OAuth2_getAuthMechanism( that ){
399                 // TODO use gadget | flash ...
400                 // IE's XDomainRequest doesn't support sending headers, so don't try.
401                 return ( X_NET_currentWrapper === X_XHR ) && X_XHR_createXDR ? 'param' : X_OAuth2_updateLocalStorage( '', that, 'AuthMechanism' );
402         }
403 function X_OAuth2_setAccessToken( that, value ){ X_OAuth2_updateLocalStorage( '+', that, 'accessToken' , value); }
404 function X_OAuth2_setRefreshToken( that, value ){ X_OAuth2_updateLocalStorage( '+', that, 'refreshToken', value); }
405 function X_OAuth2_setAccessTokenExpiry( that, value ){ X_OAuth2_updateLocalStorage( '+', that, 'tokenExpiry', value); }
406 function X_OAuth2_setAuthMechanism( that, value ){ X_OAuth2_updateLocalStorage( '+', that, 'AuthMechanism', value); }
407
408 function X_OAuth2_removeAccessToken( that ){ X_OAuth2_updateLocalStorage( '-', that, 'accessToken' ); }
409 function X_OAuth2_removeRefreshToken( that ){ X_OAuth2_updateLocalStorage( '-', that, 'refreshToken' ); }
410 function X_OAuth2_removeAccessTokenExpiry( that ){ X_OAuth2_updateLocalStorage( '-', that, 'tokenExpiry' ); }
411 function X_OAuth2_removeAuthMechanism( that ){ X_OAuth2_updateLocalStorage( '-', that, 'AuthMechanism' ); }
412         
413 function X_OAuth2_updateLocalStorage( cmd, that, name, value ){
414         var action = cmd === '+' ? 'setItem' : cmd === '-' ? 'removeItem' : 'getItem',
415                 pair;
416         
417         if( window.localStorage ){
418                 return window.localStorage[ action ]( X_Pair_get( that )[ 'clientID' ] + name, value );
419         };
420         
421         pair = X_Pair_get( that );
422         switch( cmd ){
423                 case '+' :
424                         pair[ name ] = value;
425                         break;
426                 case '-' :
427                         if( pair[ name ] !== undefined ) delete pair[ name ];
428         };
429         return pair[ name ];
430 };
431
432 //}+oauth2