Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change token refresh mechanism to depend on the token expiration time #8082

Merged
merged 11 commits into from
Sep 17, 2020
86 changes: 70 additions & 16 deletions packages/ckeditor-cloud-services-core/src/token/token.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
* @module cloud-services-core/token
*/

/* globals XMLHttpRequest, setInterval, clearInterval */
/* globals XMLHttpRequest, setTimeout, clearTimeout, atob */

import mix from '@ckeditor/ckeditor5-utils/src/mix';
import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';

const DEFAULT_OPTIONS = { refreshInterval: 3600000, autoRefresh: true };
const DEFAULT_OPTIONS = { autoRefresh: true };
const DEFAULT_TOKEN_REFRESH_TIMEOUT_TIME = 3600000;

/**
* Class representing the token used for communication with CKEditor Cloud Services.
Expand All @@ -30,7 +31,6 @@ class Token {
* value is a function it has to match the {@link module:cloud-services-core/token~refreshToken} interface.
* @param {Object} options
* @param {String} [options.initValue] Initial value of the token.
* @param {Number} [options.refreshInterval=3600000] Delay between refreshes. Default 1 hour.
* @param {Boolean} [options.autoRefresh=true] Specifies whether to start the refresh automatically.
*/
constructor( tokenUrlOrRefreshToken, options = DEFAULT_OPTIONS ) {
Expand All @@ -46,6 +46,10 @@ class Token {
);
}

if ( options.initValue ) {
this._validateTokenValue( options.initValue );
}

/**
* Value of the token.
* The value of the token is null if `initValue` is not provided or `init` method was not called.
Expand Down Expand Up @@ -84,10 +88,6 @@ class Token {
*/
init() {
return new Promise( ( resolve, reject ) => {
if ( this._options.autoRefresh ) {
this._startRefreshing();
}

if ( !this.value ) {
this.refreshToken()
.then( resolve )
Expand All @@ -96,6 +96,10 @@ class Token {
return;
}

if ( this._options.autoRefresh ) {
this._registerRefreshTokenTimeout();
}

resolve( this );
} );
}
Expand All @@ -106,33 +110,84 @@ class Token {
*/
refreshToken() {
return this._refresh()
.then( value => this.set( 'value', value ) )
.then( value => {
this._validateTokenValue( value );
this.set( 'value', value );
jczapiewski-cksource marked this conversation as resolved.
Show resolved Hide resolved

if ( this._options.autoRefresh ) {
this._registerRefreshTokenTimeout();
}
} )
.then( () => this );
}

/**
* Destroys token instance. Stops refreshing.
*/
destroy() {
this._stopRefreshing();
clearTimeout( this._tokenRefreshTimeout );
}

/**
* Starts value refreshing every `refreshInterval` time.
* Checks whether the provided token follows the JSON Web Tokens (JWT) format.
*
* @protected
* @param {String} tokenValue The token to validate.
*/
_startRefreshing() {
this._refreshInterval = setInterval( () => this.refreshToken(), this._options.refreshInterval );
_validateTokenValue( tokenValue ) {
jczapiewski-cksource marked this conversation as resolved.
Show resolved Hide resolved
// The token must be a string.
const isString = typeof tokenValue === 'string';

// The token must be a plain string without quotes ("").
const isPlainString = !/^".*"$/.test( tokenValue );

// JWT token contains 3 parts: header, payload, and signature.
// Each part is separated by a dot.
const isJWTFormat = isString && tokenValue.split( '.' ).length === 3;

if ( !( isPlainString && isJWTFormat ) ) {
/**
* The provided token must follow the [JSON Web Tokens](https://jwt.io/introduction/) format.
*
* @error token-not-in-jwt-format
*/
throw new CKEditorError( 'token-not-in-jwt-format', this );
}
}

/**
* Stops value refreshing.
* Registers a refresh token timeout for the time taken from token.
*
* @protected
*/
_stopRefreshing() {
clearInterval( this._refreshInterval );
_registerRefreshTokenTimeout() {
const tokenRefreshTimeoutTime = this._getTokenRefreshTimeoutTime();

jczapiewski-cksource marked this conversation as resolved.
Show resolved Hide resolved
clearTimeout( this._tokenRefreshTimeout );

this._tokenRefreshTimeout = setTimeout( () => {
this.refreshToken();
}, tokenRefreshTimeoutTime );
}

/**
* Returns token refresh timeout time calculated from expire time in the token payload.
*
* If the token parse fails, the default DEFAULT_TOKEN_REFRESH_TIMEOUT_TIME is returned.
*
* @protected
* @returns {Number}
*/
_getTokenRefreshTimeoutTime() {
try {
const [ , binaryTokenPayload ] = this.value.split( '.' );
const { exp: tokenExpireTime } = JSON.parse( atob( binaryTokenPayload ) );
const tokenRefreshTimeoutTime = Math.floor( ( tokenExpireTime - Date.now() ) / 2 );

return tokenRefreshTimeoutTime;
} catch ( err ) {
return DEFAULT_TOKEN_REFRESH_TIMEOUT_TIME;
}
}

/**
Expand All @@ -142,7 +197,6 @@ class Token {
* value is a function it has to match the {@link module:cloud-services-core/token~refreshToken} interface.
* @param {Object} options
* @param {String} [options.initValue] Initial value of the token.
* @param {Number} [options.refreshInterval=3600000] Delay between refreshes. Default 1 hour.
* @param {Boolean} [options.autoRefresh=true] Specifies whether to start the refresh automatically.
* @returns {Promise.<module:cloud-services-core/token~Token>}
*/
Expand Down
Loading