8
0
Просмотр исходного кода

Merge pull request #8082 from jczapiewski-cksource/i/316

Other (cloud-services-core): Change tokens refreshing mechanism to depend on the token expiration time.
Kamil Piechaczek 5 лет назад
Родитель
Сommit
501490a572

+ 70 - 16
packages/ckeditor-cloud-services-core/src/token/token.js

@@ -7,13 +7,14 @@
  * @module cloud-services-core/token
  * @module cloud-services-core/token
  */
  */
 
 
-/* globals XMLHttpRequest, setInterval, clearInterval */
+/* globals XMLHttpRequest, setTimeout, clearTimeout, atob */
 
 
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
 import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 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.
  * Class representing the token used for communication with CKEditor Cloud Services.
@@ -30,7 +31,6 @@ class Token {
 	 * value is a function it has to match the {@link module:cloud-services-core/token~refreshToken} interface.
 	 * value is a function it has to match the {@link module:cloud-services-core/token~refreshToken} interface.
 	 * @param {Object} options
 	 * @param {Object} options
 	 * @param {String} [options.initValue] Initial value of the token.
 	 * @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.
 	 * @param {Boolean} [options.autoRefresh=true] Specifies whether to start the refresh automatically.
 	 */
 	 */
 	constructor( tokenUrlOrRefreshToken, options = DEFAULT_OPTIONS ) {
 	constructor( tokenUrlOrRefreshToken, options = DEFAULT_OPTIONS ) {
@@ -46,6 +46,10 @@ class Token {
 			);
 			);
 		}
 		}
 
 
+		if ( options.initValue ) {
+			this._validateTokenValue( options.initValue );
+		}
+
 		/**
 		/**
 		 * Value of the token.
 		 * Value of the token.
 		 * The value of the token is null if `initValue` is not provided or `init` method was not called.
 		 * The value of the token is null if `initValue` is not provided or `init` method was not called.
@@ -84,10 +88,6 @@ class Token {
 	 */
 	 */
 	init() {
 	init() {
 		return new Promise( ( resolve, reject ) => {
 		return new Promise( ( resolve, reject ) => {
-			if ( this._options.autoRefresh ) {
-				this._startRefreshing();
-			}
-
 			if ( !this.value ) {
 			if ( !this.value ) {
 				this.refreshToken()
 				this.refreshToken()
 					.then( resolve )
 					.then( resolve )
@@ -96,6 +96,10 @@ class Token {
 				return;
 				return;
 			}
 			}
 
 
+			if ( this._options.autoRefresh ) {
+				this._registerRefreshTokenTimeout();
+			}
+
 			resolve( this );
 			resolve( this );
 		} );
 		} );
 	}
 	}
@@ -106,7 +110,14 @@ class Token {
 	 */
 	 */
 	refreshToken() {
 	refreshToken() {
 		return this._refresh()
 		return this._refresh()
-			.then( value => this.set( 'value', value ) )
+			.then( value => {
+				this._validateTokenValue( value );
+				this.set( 'value', value );
+
+				if ( this._options.autoRefresh ) {
+					this._registerRefreshTokenTimeout();
+				}
+			} )
 			.then( () => this );
 			.then( () => this );
 	}
 	}
 
 
@@ -114,25 +125,69 @@ class Token {
 	 * Destroys token instance. Stops refreshing.
 	 * Destroys token instance. Stops refreshing.
 	 */
 	 */
 	destroy() {
 	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
 	 * @protected
+	 * @param {String} tokenValue The token to validate.
 	 */
 	 */
-	_startRefreshing() {
-		this._refreshInterval = setInterval( () => this.refreshToken(), this._options.refreshInterval );
+	_validateTokenValue( tokenValue ) {
+		// 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
 	 * @protected
 	 */
 	 */
-	_stopRefreshing() {
-		clearInterval( this._refreshInterval );
+	_registerRefreshTokenTimeout() {
+		const tokenRefreshTimeoutTime = this._getTokenRefreshTimeoutTime();
+
+		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;
+		}
 	}
 	}
 
 
 	/**
 	/**
@@ -142,7 +197,6 @@ class Token {
 	 * value is a function it has to match the {@link module:cloud-services-core/token~refreshToken} interface.
 	 * value is a function it has to match the {@link module:cloud-services-core/token~refreshToken} interface.
 	 * @param {Object} options
 	 * @param {Object} options
 	 * @param {String} [options.initValue] Initial value of the token.
 	 * @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.
 	 * @param {Boolean} [options.autoRefresh=true] Specifies whether to start the refresh automatically.
 	 * @returns {Promise.<module:cloud-services-core/token~Token>}
 	 * @returns {Promise.<module:cloud-services-core/token~Token>}
 	 */
 	 */

+ 180 - 103
packages/ckeditor-cloud-services-core/tests/token/token.js

@@ -26,31 +26,56 @@ describe( 'Token', () => {
 	} );
 	} );
 
 
 	describe( 'constructor()', () => {
 	describe( 'constructor()', () => {
-		it( 'should throw error when no tokenUrl provided', () => {
+		it( 'should throw an error when no tokenUrl provided', () => {
 			expect( () => new Token() ).to.throw(
 			expect( () => new Token() ).to.throw(
 				CKEditorError,
 				CKEditorError,
 				'token-missing-token-url'
 				'token-missing-token-url'
 			);
 			);
 		} );
 		} );
 
 
-		it( 'should set a init token value', () => {
-			const token = new Token( 'http://token-endpoint', { initValue: 'initValue', autoRefresh: false } );
+		it( 'should throw an error if the token passed in options is not a string', () => {
+			expect( () => new Token( 'http://token-endpoint', { initValue: 123456 } ) ).to.throw(
+				CKEditorError,
+				'token-not-in-jwt-format'
+			);
+		} );
+
+		it( 'should throw an error if the token passed in options is wrapped in additional quotes', () => {
+			const tokenInitValue = getTestTokenValue();
+
+			expect( () => new Token( 'http://token-endpoint', { initValue: `"${ tokenInitValue }"` } ) ).to.throw(
+				CKEditorError,
+				'token-not-in-jwt-format'
+			);
+		} );
+
+		it( 'should throw an error if the token passed in options is not a valid JWT token', () => {
+			expect( () => new Token( 'http://token-endpoint', { initValue: 'token' } ) ).to.throw(
+				CKEditorError,
+				'token-not-in-jwt-format'
+			);
+		} );
 
 
-			expect( token.value ).to.equal( 'initValue' );
+		it( 'should set token value if the token passed in options is valid', () => {
+			const tokenInitValue = getTestTokenValue();
+			const token = new Token( 'http://token-endpoint', { initValue: tokenInitValue } );
+
+			expect( token.value ).to.equal( tokenInitValue );
 		} );
 		} );
 
 
 		it( 'should fire `change:value` event if the value of the token has changed', done => {
 		it( 'should fire `change:value` event if the value of the token has changed', done => {
+			const tokenValue = getTestTokenValue();
 			const token = new Token( 'http://token-endpoint', { autoRefresh: false } );
 			const token = new Token( 'http://token-endpoint', { autoRefresh: false } );
 
 
 			token.on( 'change:value', ( event, name, newValue ) => {
 			token.on( 'change:value', ( event, name, newValue ) => {
-				expect( newValue ).to.equal( 'token-value' );
+				expect( newValue ).to.equal( tokenValue );
 
 
 				done();
 				done();
 			} );
 			} );
 
 
 			token.init();
 			token.init();
 
 
-			requests[ 0 ].respond( 200, '', 'token-value' );
+			requests[ 0 ].respond( 200, '', tokenValue );
 		} );
 		} );
 
 
 		it( 'should accept the callback in the constructor', () => {
 		it( 'should accept the callback in the constructor', () => {
@@ -62,98 +87,183 @@ describe( 'Token', () => {
 	} );
 	} );
 
 
 	describe( 'init()', () => {
 	describe( 'init()', () => {
-		it( 'should get a token value from endpoint', done => {
+		it( 'should get a token value from the endpoint', done => {
+			const tokenValue = getTestTokenValue();
 			const token = new Token( 'http://token-endpoint', { autoRefresh: false } );
 			const token = new Token( 'http://token-endpoint', { autoRefresh: false } );
 
 
 			token.init()
 			token.init()
 				.then( () => {
 				.then( () => {
-					expect( token.value ).to.equal( 'token-value' );
+					expect( token.value ).to.equal( tokenValue );
 
 
 					done();
 					done();
 				} );
 				} );
 
 
-			requests[ 0 ].respond( 200, '', 'token-value' );
+			requests[ 0 ].respond( 200, '', tokenValue );
 		} );
 		} );
 
 
 		it( 'should get a token from the refreshToken function when is provided', () => {
 		it( 'should get a token from the refreshToken function when is provided', () => {
-			const token = new Token( () => Promise.resolve( 'token-value' ), { autoRefresh: false } );
+			const tokenValue = getTestTokenValue();
+			const token = new Token( () => Promise.resolve( tokenValue ), { autoRefresh: false } );
 
 
 			return token.init()
 			return token.init()
 				.then( () => {
 				.then( () => {
-					expect( token.value ).to.equal( 'token-value' );
+					expect( token.value ).to.equal( tokenValue );
 				} );
 				} );
 		} );
 		} );
 
 
-		it( 'should start token refresh every 1 hour', done => {
-			const clock = sinon.useFakeTimers( { toFake: [ 'setInterval' ] } );
+		it( 'should not refresh token if autoRefresh is disabled in options', async () => {
+			const clock = sinon.useFakeTimers( { toFake: [ 'setTimeout' ] } );
+			const tokenInitValue = getTestTokenValue();
 
 
-			const token = new Token( 'http://token-endpoint', { initValue: 'initValue' } );
+			const token = new Token( 'http://token-endpoint', { initValue: tokenInitValue, autoRefresh: false } );
 
 
-			token.init()
-				.then( () => {
-					clock.tick( 3600000 );
-					clock.tick( 3600000 );
-					clock.tick( 3600000 );
-					clock.tick( 3600000 );
-					clock.tick( 3600000 );
+			await token.init();
 
 
-					expect( requests.length ).to.equal( 5 );
+			await clock.tickAsync( 3600000 );
 
 
-					clock.restore();
+			expect( requests ).to.be.empty;
 
 
-					done();
-				} );
+			clock.restore();
+		} );
+
+		it( 'should refresh token with the time specified in token `exp` payload property', async () => {
+			const clock = sinon.useFakeTimers( { toFake: [ 'setTimeout' ] } );
+			const tokenInitValue = getTestTokenValue();
+
+			const token = new Token( 'http://token-endpoint', { initValue: tokenInitValue } );
+
+			await token.init();
+
+			await clock.tickAsync( 1800000 );
+			requests[ 0 ].respond( 200, '', getTestTokenValue( 150000 ) );
+
+			await clock.tickAsync( 75000 );
+			requests[ 1 ].respond( 200, '', getTestTokenValue( 10000 ) );
+
+			await clock.tickAsync( 5000 );
+			requests[ 2 ].respond( 200, '', getTestTokenValue( 2000 ) );
+
+			await clock.tickAsync( 1000 );
+			requests[ 3 ].respond( 200, '', getTestTokenValue( 300 ) );
+
+			await clock.tickAsync( 150 );
+			requests[ 4 ].respond( 200, '', getTestTokenValue( 300 ) );
+
+			expect( requests.length ).to.equal( 5 );
+
+			clock.restore();
+		} );
+
+		it( 'should refresh the token with the default time if getting token expiration time failed', async () => {
+			const clock = sinon.useFakeTimers( { toFake: [ 'setTimeout' ] } );
+			const tokenValue = 'header.test.signature';
+
+			const token = new Token( 'http://token-endpoint', { initValue: tokenValue } );
+
+			await token.init();
+
+			await clock.tickAsync( 3600000 );
+			requests[ 0 ].respond( 200, '', tokenValue );
+
+			await clock.tickAsync( 3600000 );
+			requests[ 1 ].respond( 200, '', tokenValue );
+
+			expect( requests.length ).to.equal( 2 );
+
+			clock.restore();
 		} );
 		} );
 	} );
 	} );
 
 
 	describe( 'destroy', () => {
 	describe( 'destroy', () => {
-		it( 'should stop refreshing the token', () => {
-			const clock = sinon.useFakeTimers( { toFake: [ 'setInterval', 'clearInterval' ] } );
-			const token = new Token( 'http://token-endpoint', { initValue: 'initValue' } );
+		it( 'should stop refreshing the token', async () => {
+			const clock = sinon.useFakeTimers( { toFake: [ 'setTimeout', 'clearTimeout' ] } );
+			const tokenInitValue = getTestTokenValue();
 
 
-			return token.init()
-				.then( () => {
-					clock.tick( 3600000 );
-					clock.tick( 3600000 );
+			const token = new Token( 'http://token-endpoint', { initValue: tokenInitValue } );
 
 
-					expect( requests.length ).to.equal( 2 );
+			await token.init();
 
 
-					token.destroy();
+			await clock.tickAsync( 1800000 );
+			requests[ 0 ].respond( 200, '', getTestTokenValue( 150000 ) );
+			await clock.tickAsync( 100 );
 
 
-					clock.tick( 3600000 );
-					clock.tick( 3600000 );
-					clock.tick( 3600000 );
+			await clock.tickAsync( 75000 );
+			requests[ 1 ].respond( 200, '', getTestTokenValue( 10000 ) );
+			await clock.tickAsync( 100 );
 
 
-					expect( requests.length ).to.equal( 2 );
-				} );
+			token.destroy();
+
+			await clock.tickAsync( 3600000 );
+			await clock.tickAsync( 3600000 );
+			await clock.tickAsync( 3600000 );
+
+			expect( requests.length ).to.equal( 2 );
+
+			clock.restore();
 		} );
 		} );
 	} );
 	} );
 
 
 	describe( 'refreshToken()', () => {
 	describe( 'refreshToken()', () => {
 		it( 'should get a token from the specified address', done => {
 		it( 'should get a token from the specified address', done => {
-			const token = new Token( 'http://token-endpoint', { initValue: 'initValue', autoRefresh: false } );
+			const tokenValue = getTestTokenValue();
+			const token = new Token( 'http://token-endpoint', { autoRefresh: false } );
 
 
 			token.refreshToken()
 			token.refreshToken()
 				.then( newToken => {
 				.then( newToken => {
-					expect( newToken.value ).to.equal( 'token-value' );
+					expect( newToken.value ).to.equal( tokenValue );
 
 
 					done();
 					done();
 				} );
 				} );
 
 
-			requests[ 0 ].respond( 200, '', 'token-value' );
+			requests[ 0 ].respond( 200, '', tokenValue );
+		} );
+
+		it( 'should throw an error if the returned token is wrapped in additional quotes', done => {
+			const tokenValue = getTestTokenValue();
+			const token = new Token( 'http://token-endpoint', { autoRefresh: false } );
+
+			token.refreshToken()
+				.then( () => {
+					done( new Error( 'Promise should be rejected' ) );
+				} )
+				.catch( error => {
+					expect( error.constructor ).to.equal( CKEditorError );
+					expect( error ).to.match( /token-not-in-jwt-format/ );
+					done();
+				} );
+
+			requests[ 0 ].respond( 200, '', `"${ tokenValue }"` );
+		} );
+
+		it( 'should throw an error if the returned token is not a valid JWT token', done => {
+			const token = new Token( 'http://token-endpoint', { autoRefresh: false } );
+
+			token.refreshToken()
+				.then( () => {
+					done( new Error( 'Promise should be rejected' ) );
+				} )
+				.catch( error => {
+					expect( error.constructor ).to.equal( CKEditorError );
+					expect( error ).to.match( /token-not-in-jwt-format/ );
+					done();
+				} );
+
+			requests[ 0 ].respond( 200, '', 'token' );
 		} );
 		} );
 
 
 		it( 'should get a token from the specified callback function', () => {
 		it( 'should get a token from the specified callback function', () => {
-			const token = new Token( () => Promise.resolve( 'token-value' ), { initValue: 'initValue', autoRefresh: false } );
+			const tokenValue = getTestTokenValue();
+			const token = new Token( () => Promise.resolve( tokenValue ), { autoRefresh: false } );
 
 
 			return token.refreshToken()
 			return token.refreshToken()
 				.then( newToken => {
 				.then( newToken => {
-					expect( newToken.value ).to.equal( 'token-value' );
+					expect( newToken.value ).to.equal( tokenValue );
 				} );
 				} );
 		} );
 		} );
 
 
-		it( 'should throw an error when cannot download new token', () => {
-			const token = new Token( 'http://token-endpoint', { initValue: 'initValue', autoRefresh: false } );
+		it( 'should throw an error when cannot download a new token', () => {
+			const tokenInitValue = getTestTokenValue();
+			const token = new Token( 'http://token-endpoint', { initValue: tokenInitValue, autoRefresh: false } );
 			const promise = token._refresh();
 			const promise = token._refresh();
 
 
 			requests[ 0 ].respond( 401 );
 			requests[ 0 ].respond( 401 );
@@ -167,7 +277,8 @@ describe( 'Token', () => {
 		} );
 		} );
 
 
 		it( 'should throw an error when the response is aborted', () => {
 		it( 'should throw an error when the response is aborted', () => {
-			const token = new Token( 'http://token-endpoint', { initValue: 'initValue', autoRefresh: false } );
+			const tokenInitValue = getTestTokenValue();
+			const token = new Token( 'http://token-endpoint', { initValue: tokenInitValue, autoRefresh: false } );
 			const promise = token._refresh();
 			const promise = token._refresh();
 
 
 			requests[ 0 ].abort();
 			requests[ 0 ].abort();
@@ -180,7 +291,8 @@ describe( 'Token', () => {
 		} );
 		} );
 
 
 		it( 'should throw an error when network error occurs', () => {
 		it( 'should throw an error when network error occurs', () => {
-			const token = new Token( 'http://token-endpoint', { initValue: 'initValue', autoRefresh: false } );
+			const tokenInitValue = getTestTokenValue();
+			const token = new Token( 'http://token-endpoint', { initValue: tokenInitValue, autoRefresh: false } );
 			const promise = token._refresh();
 			const promise = token._refresh();
 
 
 			requests[ 0 ].error();
 			requests[ 0 ].error();
@@ -192,8 +304,9 @@ describe( 'Token', () => {
 			} );
 			} );
 		} );
 		} );
 
 
-		it( 'should throw an error when the callback throws error', () => {
-			const token = new Token( () => Promise.reject( 'Custom error occurred' ), { initValue: 'initValue', autoRefresh: false } );
+		it( 'should throw an error when the callback throws an error', () => {
+			const tokenInitValue = getTestTokenValue();
+			const token = new Token( () => Promise.reject( 'Custom error occurred' ), { initValue: tokenInitValue, autoRefresh: false } );
 
 
 			token.refreshToken()
 			token.refreshToken()
 				.catch( error => {
 				.catch( error => {
@@ -202,75 +315,39 @@ describe( 'Token', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( '_startRefreshing()', () => {
-		it( 'should start refreshing', () => {
-			const clock = sinon.useFakeTimers( { toFake: [ 'setInterval' ] } );
-
-			const token = new Token( 'http://token-endpoint', { initValue: 'initValue', autoRefresh: false } );
-
-			token._startRefreshing();
-
-			clock.tick( 3600000 );
-			clock.tick( 3600000 );
-			clock.tick( 3600000 );
-			clock.tick( 3600000 );
-			clock.tick( 3600000 );
-
-			expect( requests.length ).to.equal( 5 );
-
-			clock.restore();
-		} );
-	} );
-
-	describe( '_stopRefreshing()', () => {
-		it( 'should stop refreshing', done => {
-			const clock = sinon.useFakeTimers( { toFake: [ 'setInterval', 'clearInterval' ] } );
-
-			const token = new Token( 'http://token-endpoint', { initValue: 'initValue' } );
-
-			token.init()
-				.then( () => {
-					clock.tick( 3600000 );
-					clock.tick( 3600000 );
-					clock.tick( 3600000 );
-
-					token._stopRefreshing();
-
-					clock.tick( 3600000 );
-					clock.tick( 3600000 );
-
-					expect( requests.length ).to.equal( 3 );
-
-					clock.restore();
-
-					done();
-				} );
-		} );
-	} );
-
 	describe( 'static create()', () => {
 	describe( 'static create()', () => {
-		it( 'should return a initialized token', done => {
+		it( 'should return an initialized token', done => {
+			const tokenValue = getTestTokenValue();
+
 			Token.create( 'http://token-endpoint', { autoRefresh: false } )
 			Token.create( 'http://token-endpoint', { autoRefresh: false } )
 				.then( token => {
 				.then( token => {
-					expect( token.value ).to.equal( 'token-value' );
+					expect( token.value ).to.equal( tokenValue );
 
 
 					done();
 					done();
 				} );
 				} );
 
 
-			requests[ 0 ].respond( 200, '', 'token-value' );
+			requests[ 0 ].respond( 200, '', tokenValue );
 		} );
 		} );
 
 
 		it( 'should use default options when none passed', done => {
 		it( 'should use default options when none passed', done => {
-			const intervalSpy = sinon.spy( window, 'setInterval' );
+			const tokenValue = getTestTokenValue();
 
 
 			Token.create( 'http://token-endpoint' )
 			Token.create( 'http://token-endpoint' )
-				.then( () => {
-					expect( intervalSpy.args[ 0 ][ 1 ] ).to.equal( 3600000 );
+				.then( token => {
+					expect( token._options ).to.eql( { autoRefresh: true } );
 
 
 					done();
 					done();
 				} );
 				} );
 
 
-			requests[ 0 ].respond( 200, '', 'token-value' );
+			requests[ 0 ].respond( 200, '', tokenValue );
 		} );
 		} );
 	} );
 	} );
 } );
 } );
+
+// Returns valid token for tests with given expiration time offset.
+//
+// @param {Number} [timeOffset=3600000]
+// @returns {String}
+function getTestTokenValue( timeOffset = 3600000 ) {
+	return `header.${ btoa( JSON.stringify( { exp: Date.now() + timeOffset } ) ) }.signature`;
+}

+ 6 - 2
packages/ckeditor-cloud-services-core/tests/uploadgateway/fileuploader.js

@@ -14,7 +14,8 @@ const BASE_64_FILE = 'data:image/gif;base64,R0lGODlhCQAJAPIAAGFhYZXK/1FRUf///' +
 	'9ra2gD/AAAAAAAAACH5BAEAAAUALAAAAAAJAAkAAAMYWFqwru2xERcYJLSNNWNBVimC5wjfaTkJADs=';
 	'9ra2gD/AAAAAAAAACH5BAEAAAUALAAAAAAJAAkAAAMYWFqwru2xERcYJLSNNWNBVimC5wjfaTkJADs=';
 
 
 describe( 'FileUploader', () => {
 describe( 'FileUploader', () => {
-	const token = new Token( 'url', { initValue: 'token', autoRefresh: false } );
+	const tokenInitValue = `header.${ btoa( JSON.stringify( { exp: Date.now() + 3600000 } ) ) }.signature`;
+	const token = new Token( 'url', { initValue: tokenInitValue, autoRefresh: false } );
 
 
 	let fileUploader;
 	let fileUploader;
 
 
@@ -116,9 +117,12 @@ describe( 'FileUploader', () => {
 					expect( request.url ).to.equal( API_ADDRESS );
 					expect( request.url ).to.equal( API_ADDRESS );
 					expect( request.method ).to.equal( 'POST' );
 					expect( request.method ).to.equal( 'POST' );
 					expect( request.responseType ).to.equal( 'json' );
 					expect( request.responseType ).to.equal( 'json' );
-					expect( request.requestHeaders ).to.be.deep.equal( { Authorization: 'token' } );
+					expect( request.requestHeaders ).to.be.deep.equal( { Authorization: tokenInitValue } );
 
 
 					done();
 					done();
+				} )
+				.catch( err => {
+					console.log( err );
 				} );
 				} );
 
 
 			request.respond( 200, { 'Content-Type': 'application/json' },
 			request.respond( 200, { 'Content-Type': 'application/json' },

+ 4 - 1
packages/ckeditor-cloud-services-core/tests/uploadgateway/uploadgateway.js

@@ -3,13 +3,16 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
  */
 
 
+/* eslint-env browser */
+
 import FileUploader from '../../src/uploadgateway/fileuploader';
 import FileUploader from '../../src/uploadgateway/fileuploader';
 import UploadGateway from '../../src/uploadgateway/uploadgateway';
 import UploadGateway from '../../src/uploadgateway/uploadgateway';
 import Token from '../../src/token/token';
 import Token from '../../src/token/token';
 import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 
 describe( 'UploadGateway', () => {
 describe( 'UploadGateway', () => {
-	const token = new Token( 'url', { initValue: 'token', autoRefresh: false } );
+	const tokenInitValue = `header.${ btoa( JSON.stringify( { exp: Date.now() + 3600000 } ) ) }.signature`;
+	const token = new Token( 'url', { initValue: tokenInitValue, autoRefresh: false } );
 
 
 	describe( 'constructor()', () => {
 	describe( 'constructor()', () => {
 		it( 'should throw error when no token provided', () => {
 		it( 'should throw error when no token provided', () => {