OSDN Git Service

43a34d6ffbb42e793e144ce0bedfa615fd551f5a
[pettanr/clientJs.git] / 0.6.x / js / 06_net / 10_XOAuth2.js
1
2 var X_NET_OAUTH2_detection      = new Function( 'w', 'try{return w.location.search}catch(e){}' ),
3         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  * OAuth2 state
17  * <dl>
18  * <dt>0 : <dd>disconnected
19  * <dt>1 : <dd>now authentication ...
20  * <dt>2 : <dd>authorization_code
21  * <dt>3 : <dd>refresh_token
22  * <dt>4 : <dd>hasAccessToken
23  * </dl>
24  * 
25  * original :
26  *  oauth2.js , <opendata@oucs.ox.ac.uk>
27  */
28 X[ 'OAuth2' ] = X_EventDispatcher[ 'inherits' ](
29                 'X.OAuth2',
30                 X_Class.NONE,
31                 
32                 /** @lends OAuth2.prototype */
33                 {
34                         'Constructor' : function( obj ){
35                                 obj = X_Object_clone( obj );
36                                 
37                                 X_Pair_create( this, obj );
38                                 
39                                 if( _getAccessToken( this ) ){
40                                         obj.oauth2State = 4;
41                                         this[ 'asyncDispatch' ]( X_EVENT_SUCCESS );
42                                 } else {
43                                         this[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
44                                 }
45                                 
46                                 obj.onAuthError   = X_NET_OAUTH2_onXHR401Error;
47                                 obj.updateRequest = X_NET_OAUTH2_updateRequest;
48                                 
49                                 // TODO canUse
50                                 // TODO kill の cancel
51                         },
52
53                         'state' : function(){
54                                 return X_Pair_get( this ).oauth2State || 0;
55                         },
56                         
57                         'requestAuth' : function(){
58                                 // pointer event 内で呼ぶこと
59                                 // 二つ以上の popup を作らない
60                                 if( X_NET_OAUTH2_authorizationWindow ) return;
61                                 
62                                 pair = X_Pair_get( this );
63                                 
64                                 if( pair.net || pair.oauth2State ) return;
65                                 
66                                 X_NET_OAUTH2_authorizationWindow = window.open(
67                                         pair[ 'authorizeEndpoint' ] + '?' + X_URL_objToParam(
68                                                 {
69                                                         'response_type' : 'code',
70                                                         'client_id'     : pair[ 'clientID' ],
71                                                         'redirect_uri'  : pair[ 'redirectURI' ],
72                                                         'scope'         : ( pair[ 'scopes' ] || []).join(' ')
73                                                 }
74                                         ), 'oauthauthorize',
75                                         'width=' + pair[ 'authorizeWindowWidth' ]
76                                         + ',height=' + pair[ 'authorizeWindowHeight' ]
77                                         + ',left=' + (screen.width  - pair[ 'authorizeWindowWidth'  ] ) / 2
78                                         + ',top='  + (screen.height - pair[ 'authorizeWindowHeight' ] ) / 2
79                                         + ',menubar=no,toolbar=no');
80                                 
81                                 X_NET_OAUTH2_authorizationTimerID = X_Timer_add( 333, 0, this, X_Net_OAuth2_detectAuthPopup );
82                                 
83                                 pair.oauth2State = 1;
84                                 
85                                 this[ 'asyncDispatch' ]( { type : X_EVENT_PROGRESS, message : 'Start to auth.' } );
86                         },
87                         
88                         'cancelAuth' : function(){
89                                 var pair = X_Pair_get( this );
90                                 
91                                 if( pair.net ){
92                                         pair.net[ 'kill' ]();
93                                         delete pair.net;
94                                 };
95                                 
96                                 // http://kojikoji75.hatenablog.com/entry/2013/12/15/223839
97                                 X_NET_OAUTH2_authorizationWindow && X_NET_OAUTH2_authorizationWindow.open( 'about:blank', '_self' ).close();
98                                 X_NET_OAUTH2_authorizationWindow  = null;
99                                 
100                                 X_NET_OAUTH2_authorizationTimerID && X_Timer_remove( X_NET_OAUTH2_authorizationTimerID );
101                                 X_NET_OAUTH2_authorizationTimerID = 0;
102                                 
103                                 this[ 'asyncDispatch' ]( X_EVENT_CANCELED );
104                         },
105                         
106                         'refreshToken' : function(){
107                                 /* TODO 自動リフレッシュ
108                                  *                              var expires_at = this._getAccessTokenExpiry();
109                                 if (expires_at && Date.now() + millis > expires_at)
110                                         this._refreshAccessToken({replay: false});
111                                  */
112                                 
113                                 pair = X_Pair_get( this );
114                                 
115                                 if( pair.net ) return;
116                                 
117                                 pair.oauth2State = 3;
118                                 
119                                 pair.net = X.Net( {
120                                         'xhr'      : pair[ 'tokenEndpoint' ],
121                                         'postdata' : X_URL_objToParam({
122                                                 'client_id'     : pair[ 'clientID' ],
123                                                 'client_secret' : pair[ 'clientSecret' ],
124                                                 'grant_type'    : 'refresh_token',
125                                                 'refresh_token' : _getRefreshToken( this )
126                                         }),
127                                         'dataType' : 'json',
128                                         'headers'  : {
129                                                                         'Accept'       : 'application/json',
130                                                                         'Content-Type' : 'application/x-www-form-urlencoded'
131                                                                 },
132                                         'test'     : 'gadget'
133                                 } ).listenOnce( [ X_EVENT_SUCCESS, X_EVENT_ERROR ], this, X_Net_OAuth2_responceHandler );
134                                 
135                                 this[ 'asyncDispatch' ]( { type : X_EVENT_PROGRESS, message : 'Start to refresh token.' } );
136                         }
137                 }
138         );
139
140 function X_Net_OAuth2_detectAuthPopup(){
141         var closed, search, pair = X_Pair_get( this );
142         
143         if( X_NET_OAUTH2_authorizationWindow.closed ){
144                 pair.oauth2State = 0;
145                 closed = true;
146
147                 this[ 'asyncDispatch' ]( X_EVENT_CANCELED );
148         } else
149         if( search = X_NET_OAUTH2_detection( X_NET_OAUTH2_authorizationWindow ) ){
150                 pair      = X_Pair_get( this );
151                 pair.code = X_URL_ParamToObj( search.slice( 1 ) )[ 'code' ];
152
153                 X_NET_OAUTH2_authorizationWindow.open( 'about:blank', '_self' ).close();
154                 closed = true;
155
156                 X_Net_OAuth2_authorizationCode( this, pair );
157                 
158                 pair.oauth2State = 2;
159                 this[ 'asyncDispatch' ]( { type : X_EVENT_PROGRESS, message : 'Get code success, then authorization code.' } );
160         };
161         
162         if( closed ){
163                 X_NET_OAUTH2_authorizationWindow  = null;
164                 X_NET_OAUTH2_authorizationTimerID = 0;
165                 
166                 return X_Callback_UN_LISTEN;    
167         };
168 };
169
170 function X_Net_OAuth2_authorizationCode( oauth2, pair ){        
171         pair.net = X.Net( {
172                 'xhr'      : pair[ 'tokenEndpoint' ],
173                 'postdata' : X_URL_objToParam({
174                         'client_id'     : pair[ 'clientID' ],
175                         'client_secret' : pair[ 'clientSecret' ],
176                         'grant_type'    : 'authorization_code',
177                         'code'          : pair.code,
178                         'redirect_uri'  : pair[ 'redirectURI' ]
179                 }),
180                 'dataType' : 'json',
181                 'headers'  : {
182                         'Accept'       : 'application/json',
183                         'Content-Type' : 'application/x-www-form-urlencoded'
184                 },
185                 'test'     : 'gadget'
186         } ).listenOnce( [ X_EVENT_SUCCESS, X_EVENT_ERROR ], oauth2, X_Net_OAuth2_responceHandler );
187 };
188
189 function X_Net_OAuth2_responceHandler( e ){
190         var data = e.data,
191                 pair = X_Pair_get( this ),
192                 isRefresh = pair.oauth2State === 3;
193         
194         delete pair.net;
195         
196         switch( e.type ){
197                 case X_EVENT_SUCCESS :
198                         if( isRefresh && data.error ){
199                                 _removeRefreshToken( this );
200                                 pair.oauth2State = 0;
201                                 this[ 'asyncDispatch' ]( { type : X_EVENT_ERROR, message : 'Refresh access token error.' + data.error, data : data } );
202                                 this[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
203                                 return;
204                         } else
205                         if( data.error ){
206                                 pair.oauth2State = 0;
207                                 this[ 'asyncDispatch' ]( { type : X_EVENT_ERROR, message : 'Get new access token error.' + data.error, data : data } );
208                                 this[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
209                                 return;
210                         };
211                         
212                         _setAccessToken( this, data[ 'access_token' ] || '' );
213                         _setRefreshToken( this, data[ 'refresh_token' ] || '' );
214                         
215                         if( data[ 'expires_in' ] ){
216                                 _setAccessTokenExpiry( this, X_Timer_now() + data[ 'expires_in' ] * 1000 );
217                         } else
218                         if( _getAccessTokenExpiry( this ) ){
219                                 _removeAccessTokenExpiry( this );
220                         };
221                         
222                         pair.oauth2State = 4;
223                         this[ 'asyncDispatch' ]( { type : X_EVENT_SUCCESS, message : isRefresh ? 'Refresh access token success.' : 'Get new access token success.' } );
224                         break;
225                         
226                 case X_EVENT_ERROR :
227                         if( isRefresh ){
228                                 // other error, not auth
229                                 pair.oauth2State = 0;
230                                 this[ 'asyncDispatch' ]( { type : X_EVENT_ERROR, message : 'Refresh access token error.' } );
231                                 _removeRefreshToken( this );
232                                 this[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
233                         } else
234                         if( _getAuthMechanism( this ) === 'param' ){
235                                 pair.oauth2State = 0;
236                                 this[ 'asyncDispatch' ]( { type : X_EVENT_ERROR, message : 'network-error' } );
237                         } else {
238                                 pair.oauth2State = 0;
239                                 _setAuthMechanism( this, 'param' );
240                                 this[ 'asyncDispatch' ]( { type : X_EVENT_PROGRESS, message : 'Refresh access token failed. retry header -> param. ' } );
241                                 // retry
242                                 X_Net_OAuth2_authorizationCode( this, pair );
243                         };
244                         break;
245         };
246 };
247
248 function X_NET_OAUTH2_onXHR401Error( oauth2, e ){
249         var pair = this,
250                 headers = e[ 'headers' ],
251                 xhr, bearerParams, headersExposed = false;
252         
253         if( _getAuthMechanism( oauth2 ) !== 'param' ){
254                 xhr            = X_NET_currentWrapper[ '_rawObject' ];
255                 headersExposed = !X_Net_XHR_createXDR || !!headers; // this is a hack for Firefox and IE
256                 bearerParams   = headersExposed && ( headers[ 'WWW-Authenticate' ] || headers[ 'www-authenticate' ] );
257                 X_Type_isArray( bearerParams ) && ( bearerParams = bearerParams.join( '\n' ) );
258         };
259         
260         // http://d.hatena.ne.jp/ritou/20110402/1301679908
261         if ( bearerParams && bearerParams.indexOf( ' error=' ) === -1 ) {
262                 pair.oauth2State = 0;
263                 oauth2[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
264         } else
265         if ((( bearerParams && bearerParams.indexOf( 'invalid_token' ) !== -1 ) || !headersExposed) && _getRefreshToken( oauth2 ) ) {
266                 _removeAccessToken( oauth2 ); // It doesn't work any more.
267                 pair.oauth2State = 3;
268                 oauth2[ 'refreshToken' ]();
269         } else {
270         //if (!headersExposed && !_getRefreshToken( oauth2 )) {
271                 _removeAccessToken( oauth2 ); // It doesn't work any more.
272                 pair.oauth2State = 0;
273                 oauth2[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
274         };
275 };
276
277 function X_NET_OAUTH2_updateRequest( oauth2, request ){
278         var token     = _getAccessToken( oauth2 ),
279                 mechanism = _getAuthMechanism( oauth2 ),
280                 url       = request[ 'url' ],
281                 headers;
282
283         if( token && mechanism === 'param' ){
284                 request[ 'url' ] = url + ((url.indexOf('?') !== -1) ? '&' : '?') + 'bearer_token=' + encodeURIComponent( token );
285         };
286         
287         if( token && ( !mechanism || mechanism === 'header' ) ){
288                 headers = request[ 'headers' ] || ( request[ 'headers' ] = {} );
289                 headers[ 'Authorization' ] = 'Bearer ' + token;
290         };
291 };
292
293 function _getAccessToken( that ){ return updateLocalStorage( '', that, 'accessToken' ); }
294 function _getRefreshToken( that){ return updateLocalStorage( '', that, 'refreshToken' ); }
295 function _getAccessTokenExpiry( that ){ return updateLocalStorage( '', that, 'tokenExpiry' ); }
296 function _getAuthMechanism( that ){
297                 // TODO use gadget | flash ...
298                 // IE's XDomainRequest doesn't support sending headers, so don't try.
299                 return X_Net_XHR_createXDR ? 'param' : updateLocalStorage( '', that, 'AuthMechanism' );
300         }
301 function _setAccessToken( that, value ){ updateLocalStorage( '+', that, 'accessToken' , value); }
302 function _setRefreshToken( that, value ){ updateLocalStorage( '+', that, 'refreshToken', value); }
303 function _setAccessTokenExpiry( that, value ){ updateLocalStorage( '+', that, 'tokenExpiry', value); }
304 function _setAuthMechanism( that, value ){ updateLocalStorage( '+', that, 'AuthMechanism', value); }
305
306 function _removeAccessToken( that ){ updateLocalStorage( '-', that, 'accessToken' ); }
307 function _removeRefreshToken( that ){ updateLocalStorage( '-', that, 'refreshToken' ); }
308 function _removeAccessTokenExpiry( that ){ updateLocalStorage( '-', that, 'tokenExpiry' ); }
309 function _removeAuthMechanism( that ){ updateLocalStorage( '-', that, 'AuthMechanism' ); }
310         
311 function updateLocalStorage( cmd, that, name, value ){
312         var action = cmd === '+' ? 'setItem' : cmd === '-' ? 'removeItem' : 'getItem',
313                 pair;
314         
315         if( window.localStorage ){
316                 return window.localStorage[ action ]( X_Pair_get( that )[ 'clientID' ] + name, value );
317         };
318         
319         pair = X_Pair_get( that );
320         switch( cmd ){
321                 case '+' :
322                         pair[ name ] = value;
323                         break;
324                 case '-' :
325                         if( pair[ name ] !== undefined ) delete pair[ name ];
326         };
327         return pair[ name ];
328 };
329