| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261 |
- /**
- * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
- /**
- * @module cloud-services-core/token
- */
- /* 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 = { autoRefresh: true };
- const DEFAULT_TOKEN_REFRESH_TIMEOUT_TIME = 3600000;
- /**
- * Class representing the token used for communication with CKEditor Cloud Services.
- * Value of the token is retrieving from the specified URL and is refreshed every 1 hour by default.
- *
- * @mixes ObservableMixin
- */
- class Token {
- /**
- * Creates `Token` instance.
- * Method `init` should be called after using the constructor or use `create` method instead.
- *
- * @param {String|Function} tokenUrlOrRefreshToken Endpoint address to download the token or a callback that provides the token. If the
- * 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 {Boolean} [options.autoRefresh=true] Specifies whether to start the refresh automatically.
- */
- constructor( tokenUrlOrRefreshToken, options = DEFAULT_OPTIONS ) {
- if ( !tokenUrlOrRefreshToken ) {
- /**
- * A `tokenUrl` must be provided as the first constructor argument.
- *
- * @error token-missing-token-url
- */
- throw new CKEditorError(
- 'token-missing-token-url',
- this
- );
- }
- 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.
- * `create` method creates token with initialized value from url.
- *
- * @name value
- * @member {String} #value
- * @observable
- * @readonly
- */
- this.set( 'value', options.initValue );
- /**
- * Base refreshing function.
- *
- * @private
- * @member {String|Function} #_refresh
- */
- if ( typeof tokenUrlOrRefreshToken === 'function' ) {
- this._refresh = tokenUrlOrRefreshToken;
- } else {
- this._refresh = () => defaultRefreshToken( tokenUrlOrRefreshToken );
- }
- /**
- * @type {Object}
- * @private
- */
- this._options = Object.assign( {}, DEFAULT_OPTIONS, options );
- }
- /**
- * Initializes the token.
- *
- * @returns {Promise.<module:cloud-services-core/token~Token>}
- */
- init() {
- return new Promise( ( resolve, reject ) => {
- if ( !this.value ) {
- this.refreshToken()
- .then( resolve )
- .catch( reject );
- return;
- }
- if ( this._options.autoRefresh ) {
- this._registerRefreshTokenTimeout();
- }
- resolve( this );
- } );
- }
- /**
- * Refresh token method. Useful in a method form as it can be override in tests.
- * @returns {Promise.<String>}
- */
- refreshToken() {
- return this._refresh()
- .then( value => {
- this._validateTokenValue( value );
- this.set( 'value', value );
- if ( this._options.autoRefresh ) {
- this._registerRefreshTokenTimeout();
- }
- } )
- .then( () => this );
- }
- /**
- * Destroys token instance. Stops refreshing.
- */
- destroy() {
- clearTimeout( this._tokenRefreshTimeout );
- }
- /**
- * Checks whether the provided token follows the JSON Web Tokens (JWT) format.
- *
- * @protected
- * @param {String} tokenValue The token to validate.
- */
- _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 );
- }
- }
- /**
- * Registers a refresh token timeout for the time taken from token.
- *
- * @protected
- */
- _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 or the token payload doesn't contain, 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 ) );
- if ( !tokenExpireTime ) {
- return DEFAULT_TOKEN_REFRESH_TIMEOUT_TIME;
- }
- const tokenRefreshTimeoutTime = Math.floor( ( ( tokenExpireTime * 1000 ) - Date.now() ) / 2 );
- return tokenRefreshTimeoutTime;
- } catch ( err ) {
- return DEFAULT_TOKEN_REFRESH_TIMEOUT_TIME;
- }
- }
- /**
- * Creates a initialized {@link module:cloud-services-core/token~Token} instance.
- *
- * @param {String|Function} tokenUrlOrRefreshToken Endpoint address to download the token or a callback that provides the token. If the
- * 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 {Boolean} [options.autoRefresh=true] Specifies whether to start the refresh automatically.
- * @returns {Promise.<module:cloud-services-core/token~Token>}
- */
- static create( tokenUrlOrRefreshToken, options = DEFAULT_OPTIONS ) {
- const token = new Token( tokenUrlOrRefreshToken, options );
- return token.init();
- }
- }
- mix( Token, ObservableMixin );
- /**
- * This function is called in a defined interval by the {@link ~Token} class. It also can be invoked manually.
- * It should return a promise, which resolves with the new token value.
- * If any error occurs it should return a rejected promise with an error message.
- *
- * @function refreshToken
- * @returns {Promise.<String>}
- */
- /**
- * @private
- * @param {String} tokenUrl
- */
- function defaultRefreshToken( tokenUrl ) {
- return new Promise( ( resolve, reject ) => {
- const xhr = new XMLHttpRequest();
- xhr.open( 'GET', tokenUrl );
- xhr.addEventListener( 'load', () => {
- const statusCode = xhr.status;
- const xhrResponse = xhr.response;
- if ( statusCode < 200 || statusCode > 299 ) {
- /**
- * Cannot download new token from the provided url.
- *
- * @error token-cannot-download-new-token
- */
- return reject(
- new CKEditorError( 'token-cannot-download-new-token', null )
- );
- }
- return resolve( xhrResponse );
- } );
- xhr.addEventListener( 'error', () => reject( new Error( 'Network Error' ) ) );
- xhr.addEventListener( 'abort', () => reject( new Error( 'Abort' ) ) );
- xhr.send();
- } );
- }
- export default Token;
|