OSDN Git Service

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