2 //{+oauth2"OAuth2 サービスの定義"(OAuth2外部サービスを定義し、認可プロセス・xhrの署名を自動化します)[+xhr,+window]
3 var X_OAUTH2_authWindow,
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 に切替
17 * oauth2.js , <opendata@oucs.ox.ac.uk>
18 * https://github.com/ox-it/javascript-oauth2/blob/master/oauth2/oauth2.js
21 * @class OAuth2 サービスを定義し接続状況をモニタする。適宜にトークンのアップデートなどを行う
23 * @extends {EventDispatcher}
24 * @example // 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,
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 );
40 xhr : 'https://www.googleapis.com/blogger/v3/users/self/blogs',
43 test : 'gadget' // http -> https:xProtocol なリクエストのため、google ガジェットを proxy に使用
45 .listen( [ X.Event.SUCCESS, X.Event.ERROR, X.Event.PROGRESS ], updateOAuth2State );
47 X[ 'OAuth2' ] = X_EventDispatcher[ 'inherits' ](
51 /** @lends OAuth2.prototype */
53 'Constructor' : function( obj ){
56 obj = X_Object_copy( obj );
57 obj[ 'refreshMargin' ] = obj[ 'refreshMargin' ] || 300000;
59 X_Pair_create( this, obj );
61 obj.onAuthError = X_NET_OAUTH2_onXHR401Error;
62 obj.updateRequest = X_NET_OAUTH2_updateRequest;
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' ]();
69 this[ 'asyncDispatch' ]( X_EVENT_SUCCESS );
72 this[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
75 // TODO canUse gadgetProxy
76 this[ 'listen' ]( [ X_EVENT_KILL_INSTANCE, X_EVENT_SUCCESS, X_EVENT_ERROR, X_EVENT_NEED_AUTH ], X_OAUTH2_handleEvent );
83 * <dt>1 : <dd>認可用 window がポップアップ中
85 * <dt>3 : <dd>トークンのリフレッシュ中
91 return X_Pair_get( this ).oauth2State || 0;
95 * 認可用 window をポップアップする。ポップアップブロックが働かないように必ず pointer event 内で呼ぶこと。
97 * <dt>1 : <dd>認可用 window がポップアップ中(自身)
99 * <dt>3 : <dd>トークンのリフレッシュ中
101 * <dt>5 : <dd>他のOAuth2サービスの認可用 window がポップアップ中
105 'requestAuth' : function(){
106 var e = X_EventDispatcher_CURRENT_EVENTS[ X_EventDispatcher_CURRENT_EVENTS.length - 1 ],
109 // TODO pointer event 内か?チェック
110 if( !e || !e[ 'pointerType' ] ){
111 alert( 'タッチイベント以外での popup! ' + ( e ? e.type : '' ) );
116 if( X_OAUTH2_authWindow ) return;
118 pair = X_Pair_get( this );
120 if( pair.net || pair.oauth2State ) return;
122 w = pair[ 'authorizeWindowWidth' ] || 500;
123 h = pair[ 'authorizeWindowHeight' ] || 500;
125 X_OAUTH2_authWindow = X_Window( {
126 'url' : X_URL_create( pair[ 'authorizeEndpoint' ],
128 'response_type' : 'code',
129 'client_id' : pair[ 'clientID' ],
130 'redirect_uri' : pair[ 'redirectURI' ],
131 'scope' : ( pair[ 'scopes' ] || [] ).join( ' ' )
134 'name' : 'oauthauthorize',
135 'params' : 'width=' + w
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 );
142 X_OAUTH2_authTimerID = X_Timer_add( 333, 0, this, X_OAuth2_detectAuthPopup );
144 pair.oauth2State = 1;
146 this[ 'asyncDispatch' ]( { type : X_EVENT_PROGRESS, message : 'Start to auth.' } );
150 * 認可プロセスのキャンセル。ポップアップを閉じて認可用の通信は中断する。
152 'cancelAuth' : function(){
153 var pair = X_Pair_get( this );
156 pair.net[ 'kill' ]();
160 if( pair.oauth2State !== 1 ){
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;
170 X_OAUTH2_authTimerID && X_Timer_remove( X_OAUTH2_authTimerID );
171 X_OAUTH2_authTimerID = 0;
173 this[ 'asyncDispatch' ]( X_EVENT_CANCELED );
179 'refreshToken' : function(){
180 var pair = X_Pair_get( this ),
181 refreshToken = X_OAuth2_getRefreshToken( this );
184 pair.oauth2State = 0;
185 this[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
189 if( pair.net ) return;
191 if( pair.refreshTimerID ){
192 X_Timer_remove( pair.refreshTimerID );
193 delete pair.refreshTimerID;
196 pair.oauth2State = 3;
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
208 'Accept' : 'application/json',
209 'Content-Type' : 'application/x-www-form-urlencoded'
211 'test' : 'gadget' // canuse
212 } ).listenOnce( [ X_EVENT_SUCCESS, X_EVENT_ERROR ], this, X_OAuth2_responceHandler );
214 this[ 'asyncDispatch' ]( { type : X_EVENT_PROGRESS, message : 'Start to refresh token.' } );
219 function X_OAUTH2_handleEvent( e ){
220 var pair = X_Pair_get( this );
223 case X_EVENT_KILL_INSTANCE :
224 this[ 'cancelAuth' ]();
227 case X_EVENT_NEED_AUTH :
228 pair.refreshTimerID && X_Timer_remove( pair.refreshTimerID );
231 case X_EVENT_SUCCESS :
232 pair.refreshTimerID && X_Timer_remove( pair.refreshTimerID );
233 if( X_OAuth2_getRefreshToken( this ) ){
235 pair.refreshTimerID = X_Timer_once( X_OAuth2_getAccessTokenExpiry( this ) - X_Timer_now() - pair[ 'refreshMargin' ], this, this[ 'refreshToken' ] );
240 function X_OAuth2_detectAuthPopup( e ){
241 var pair = X_Pair_get( this ),
244 if( X_OAUTH2_authWindow[ 'closed' ]() ){
246 this[ 'asyncDispatch' ]( X_EVENT_CANCELED );
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' ];
252 X_OAuth2_authorizationCode( this, pair );
253 this[ 'asyncDispatch' ]( { type : X_EVENT_PROGRESS, message : 'Get code success, then authorization code.' } );
257 pair = pair || X_Pair_get( this );
258 pair.oauth2State = status;
260 X_OAUTH2_authWindow[ 'kill' ]();
261 X_OAUTH2_authWindow = null;
262 X_OAUTH2_authTimerID = X_Timer_remove( X_OAUTH2_authTimerID );
264 return X_CALLBACK_UN_LISTEN;
268 function X_OAuth2_authorizationCode( oauth2, pair ){
270 'xhr' : pair[ 'tokenEndpoint' ],
271 'postdata' : X_URL_objToParam({
272 'client_id' : pair[ 'clientID' ],
273 'client_secret' : pair[ 'clientSecret' ],
274 'grant_type' : 'authorization_code',
276 'redirect_uri' : pair[ 'redirectURI' ]
280 'Accept' : 'application/json',
281 'Content-Type' : 'application/x-www-form-urlencoded'
284 } ).listenOnce( [ X_EVENT_SUCCESS, X_EVENT_ERROR ], oauth2, X_OAuth2_responceHandler );
287 function X_OAuth2_responceHandler( e ){
288 var data = e.response,
289 pair = X_Pair_get( this ),
290 isRefresh = pair.oauth2State === 3;
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 );
304 pair.oauth2State = 0;
305 this[ 'asyncDispatch' ]( { type : X_EVENT_ERROR, message : 'Get new access token error.' } );
306 this[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
310 X_OAuth2_setAccessToken( this, data[ 'access_token' ] || '' );
311 ( !isRefresh || data[ 'refresh_token' ] ) && X_OAuth2_setRefreshToken( this, data[ 'refresh_token' ] || '' );
313 if( data[ 'expires_in' ] ){
314 X_OAuth2_setAccessTokenExpiry( this, X_Timer_now() + data[ 'expires_in' ] * 1000 );
316 if( X_OAuth2_getAccessTokenExpiry( this ) ){
317 X_OAuth2_removeAccessTokenExpiry( this );
320 pair.oauth2State = 4;
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;
327 this[ 'asyncDispatch' ]( { type : X_EVENT_SUCCESS, message : isRefresh ? 'Refresh access token success.' : 'Get new access token success.' } );
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 );
338 if( X_OAuth2_getAuthMechanism( this ) === 'param' ){
339 pair.oauth2State = 0;
340 this[ 'asyncDispatch' ]( { type : X_EVENT_ERROR, message : 'network-error' } );
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. ' } );
346 X_OAuth2_authorizationCode( this, pair );
352 function X_NET_OAUTH2_onXHR401Error( oauth2, e ){
354 headers = e[ 'headers' ],
355 bearerParams, headersExposed = false;
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' ) );
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 );
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' ]();
373 X_OAuth2_removeAccessToken( oauth2 ); // It doesn't work any more.
374 pair.oauth2State = 0;
375 oauth2[ 'asyncDispatch' ]( X_EVENT_NEED_AUTH );
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' ],
385 if( token && mechanism === 'param' ){
386 request[ 'url' ] = X_URL_create( url, { 'bearer_token' : encodeURIComponent( token ) } );
389 if( token && ( !mechanism || mechanism === 'header' ) ){
390 headers = request[ 'headers' ] || ( request[ 'headers' ] = {} );
391 headers[ 'Authorization' ] = 'Bearer ' + token;
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' );
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); }
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' ); }
413 function X_OAuth2_updateLocalStorage( cmd, that, name, value ){
414 var action = cmd === '+' ? 'setItem' : cmd === '-' ? 'removeItem' : 'getItem',
417 if( window.localStorage ){
418 return window.localStorage[ action ]( X_Pair_get( that )[ 'clientID' ] + name, value );
421 pair = X_Pair_get( that );
424 pair[ name ] = value;
427 if( pair[ name ] !== undefined ) delete pair[ name ];