src/support/oauth/providers/generic.js

Summary

Maintainability
B
5 hrs
Test Coverage
"use strict";

const qs = require('query-string'),
  OAuth2 = require('oauth').OAuth2;


const waigo = global.waigo,
  _ = waigo._,
  logger = waigo.load('support/logger'),
  Q = waigo.load('support/promise'),
  OauthError = waigo.load('support/oauth/error');






/**
 * Base class for OAuth providers.
 */
class GenericOauth {
  /**
   * @constructor
   * @param  {Object} context  Current request context.
   * @param  {String} provider Id of provider.
   * @param  {Object} [tokens]   Access tokens previously obtained.
   */
  constructor (context, provider, tokens) {
    this.context = context;
    this.provider = provider;
    this.App = this.context.App;
    this.logger = logger.create(`Oauth-${provider}`);

    this.config = _.get(this.App.config.oauth, this.provider, {});
    this.callbackURL = this.App.routes.url('oauth_callback', {
      provider: provider,
    }, null, {
      absolute: true
    });

    this.oauth2 = new OAuth2(
      this.config.clientId,
      this.config.clientSecret, 
      this.config.baseURL,
      this.config.authorizePath,
      this.config.accessTokenPath,
      this.config.customHeaders
    );

    if (tokens) {
      this._setTokens(tokens);
    }
  }


  /**
   * Get OAuth authorization URL.
   * @return {String}
   */
  getAuthorizeUrl () {
    let params = this._buildAuthorizeParams();

    let url = this.oauth2.getAuthorizeUrl(params);

    this.logger.debug('authorize url', url);

    return url;
  }


  * handleAuthorizationCallback() {
    let user = this._user();

    this.App.emit('record', 'oauth_callback', user || 'anon', {
      provider: this.provider,
      query: this.context.request.query,
    });

    try {
      let errorMsg = this.context.request.query.error,
        errorDesc = this.context.request.query.error_description;

      if (errorMsg) {
        throw new OauthError(errorMsg, 400, errorDesc);
      }
      
      // we got the code!
      let code = this.context.request.query.code;

      if (!code) {
        throw new OauthError('Failed to obtain OAuth code', 400);
      }

      let response = yield this.getAccessToken(code);

      yield this._handlePostAuthorizationSuccess(response);
    } catch (err) {
      yield this._handleError(err);
    }
  }


  /**
   * Get OAuth access token.
   * @param {String} code Code returned by OAuth provider.
   */
  * getAccessToken (code) {
    let user = this._user();

    this.logger.info(`Get access token: user=${user ? user.id : 'anon'} code=${code}`);

    try {
      return yield new Q((resolve, reject) => {
        let params = this._buildAccessTokenParams();

        this.oauth2.getOAuthAccessToken(code, params, (err, access_token, refresh_token, result) => {
          if (err) {
            return reject(new OauthError('Get access token error', err.statusCode || 400, err));
          }

          if (_.get(result, 'error')) {
            return reject(
              new OauthError(`Get access token error: ${result.error}`, 400, {
                error: result.error
              })
            );
          }

          this.logger.debug(`Get access token result: ${access_token}, ${refresh_token}, ${JSON.stringify(result)}`);

          this._setTokens({
            access_token: access_token,
            refresh_token: refresh_token,             
          });

          resolve({
            access_token: access_token,
            refresh_token: refresh_token, 
            result: result,
          });
        });
      });

    } catch (err) {
      yield this._handleError(err, {
        method: 'getOAuthAccessToken',
      });
    }
  }



  _setTokens (tokens) {
    this.tokens = tokens;

    if (this.tokens) {
      this.authHeader = {
        'Authorization': `Bearer ${this.tokens.access_token}`,
      };
    }
  }


  * _get (url, queryParams) {
    return yield this._request('GET', url, queryParams);
  }


  * _post (url, queryParams, body) {
    return yield this._request('POST', url, queryParams, body);
  }


  _buildRequestUrl (url, queryParams) {
    let apiBaseUrl = this.config.apiBaseUrl;

    if (!_.get(apiBaseUrl, 'length')) {
      throw new OauthError(`${this.provider}: apiBaseUrl must be provided`);
    }

    url = apiBaseUrl + url;

    queryParams = qs.stringify(queryParams || {});

    if (queryParams.length) {
      url += '?' + queryParams;
    }

    return url;
  }



  * _request (method, url, queryParams, body) {
    url = this._buildRequestUrl(url, queryParams);

    this.logger.debug(method, url);

    if (body) {
      body = JSON.stringify(body);
    }

    try {
      return yield new Q((resolve, reject) => {
        this.oauth2._request(method, url, this.authHeader, body, this.accessToken, (err, result) => {
          if (err) {
            return reject(new OauthError(method + ' error', err.statusCode || 400, err));
          }

          try {
            result = JSON.parse(result);
          } catch (e) {
            // nothing to do here
          }

          resolve(result);
        });
      });

    } catch (err) {
      yield this._handleError(err, {
        method: method,
        url: url,
        body: body,
      });
    }
  }


  _buildAuthorizeParams () {
    let params = _.extend({}, _.get(this.config, 'authorizeParams'));

    let callbackParamName = _.get(this.config, 'callbackParam', 'redirect_uri');

    params[callbackParamName] = this.callbackURL;

    return params;
  }


  _buildAccessTokenParams () {
    let params = _.extend({}, _.get(this.config, 'accessTokenParams'));

    let callbackParamName = _.get(this.config, 'callbackParam', 'redirect_uri');

    params[callbackParamName] = this.callbackURL;

    return params;
  }


  _user () {
    return this.context.currentUser;
  }


  * _handlePostAuthorizationSuccess (accessTokenResponse) {
    yield this.context.redirect("/");
  }


  * _handleError (err, attrs) {
    this.logger.error(err);

    this.App.emit('record', 'oauth_request', this._user() || 'anon', _.extend({}, attrs, {
      type: 'error',
      provider: this.provider,
      message: err.message,
      details: err.details,
    }));

    throw err;
  }

}

module.exports = GenericOauth;