src/auth/index.js

import Client from '../networking';
import {apply} from '../utils/whitelist';
import {toCamelCase} from '../utils/camel';
import AuthError from './authError';
import Auth0Error from './auth0Error';

function responseHandler(response, exceptions = {}) {
  if (response.ok && response.json) {
    return toCamelCase(response.json, exceptions);
  }
  throw new AuthError(response);
}

/**
 * Auth0 Auth API
 *
 * @export Auth
 * @see https://auth0.com/docs/api/authentication
 * @class Auth
 */
export default class Auth {
  constructor(options = {}) {
    this.client = new Client(options);
    const {clientId} = options;
    if (!clientId) {
      throw new Error('Missing clientId in parameters');
    }
    this.domain = this.client.domain;
    this.clientId = clientId;
  }

  /**
   * Builds the full authorize endpoint url in the Authorization Server (AS) with given parameters.
   *
   * @param {Object} parameters parameters to send to `/authorize`
   * @param {String} parameters.responseType type of the response to get from `/authorize`.
   * @param {String} parameters.redirectUri where the AS will redirect back after success or failure.
   * @param {String} parameters.state random string to prevent CSRF attacks.
   * @returns {String} authorize url with specified parameters to redirect to for AuthZ/AuthN.
   * @see https://auth0.com/docs/api/authentication#authorize-client
   *
   * @memberof Auth
   */
  authorizeUrl(parameters = {}) {
    const query = apply(
      {
        parameters: {
          redirectUri: {required: true, toName: 'redirect_uri'},
          responseType: {required: true, toName: 'response_type'},
          state: {required: true},
        },
        whitelist: false,
      },
      parameters,
    );
    return this.client.url(
      '/authorize',
      {...query, client_id: this.clientId},
      true,
    );
  }

  /**
   * Builds the full logout endpoint url in the Authorization Server (AS) with given parameters.
   *
   * @param {Object} parameters parameters to send to `/v2/logout`
   * @param {Boolean} [parameters.federated] if the logout should include removing session for federated IdP.
   * @param {String} [parameters.clientId] client identifier of the one requesting the logout
   * @param {String} [parameters.returnTo] url where the user is redirected to after logout. It must be declared in you Auth0 Dashboard
   * @returns {String} logout url with specified parameters
   * @see https://auth0.com/docs/api/authentication#logout
   *
   * @memberof Auth
   */
  logoutUrl(parameters = {}) {
    const query = apply(
      {
        parameters: {
          federated: {required: false},
          clientId: {required: false, toName: 'client_id'},
          returnTo: {required: false},
        },
      },
      parameters,
    );
    return this.client.url('/v2/logout', {...query}, true);
  }

  /**
   * Exchanges a code obtained via `/authorize` (w/PKCE) for the user's tokens
   *
   * @param {Object} parameters parameters used to obtain tokens from a code
   * @param {String} parameters.code code returned by `/authorize`.
   * @param {String} parameters.redirectUri original redirectUri used when calling `/authorize`.
   * @param {String} parameters.verifier value used to generate the code challenge sent to `/authorize`.
   * @returns {Promise}
   * @see https://auth0.com/docs/api-auth/grant/authorization-code-pkce
   *
   * @memberof Auth
   */
  exchange(parameters = {}) {
    const payload = apply(
      {
        parameters: {
          code: {required: true},
          verifier: {required: true, toName: 'code_verifier'},
          redirectUri: {required: true, toName: 'redirect_uri'},
        },
      },
      parameters,
    );
    return this.client
      .post('/oauth/token', {
        ...payload,
        client_id: this.clientId,
        grant_type: 'authorization_code',
      })
      .then(responseHandler);
  }

  /**
   * Exchanges an external token obtained via a native social authentication solution for the user's tokens
   *
   * @param {Object} parameters parameters used to obtain user tokens from an external provider's token
   * @param {String} parameters.subjectToken token returned by the native social authentication solution
   * @param {String} parameters.subjectTokenType identifier that indicates the native social authentication solution
   * @param {Object} [parameters.userProfile] additional profile attributes to set or override, only on select native social authentication solutions
   * @param {String} [parameters.audience] API audience to request
   * @param {String} [parameters.scope] scopes requested for the issued tokens. e.g. `openid profile`
   * @returns {Promise}
   *
   * @see https://auth0.com/docs/api/authentication#token-exchange-for-native-social
   *
   * @memberof Auth
   */
  exchangeNativeSocial(parameters = {}) {
    const payload = apply(
      {
        parameters: {
          subjectToken: {required: true, toName: 'subject_token'},
          subjectTokenType: {required: true, toName: 'subject_token_type'},
          userProfile: {required: false, toName: 'user_profile'},
          audience: {required: false},
          scope: {required: false},
        },
      },
      parameters,
    );
    return this.client
      .post('/oauth/token', {
        ...payload,
        client_id: this.clientId,
        grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
      })
      .then(responseHandler);
  }

  /**
   * Performs Auth with user credentials using the Password Realm Grant
   *
   * @param {Object} parameters password realm parameters
   * @param {String} parameters.username user's username or email
   * @param {String} parameters.password user's password
   * @param {String} parameters.realm name of the Realm where to Auth (or connection name)
   * @param {String} [parameters.audience] identifier of Resource Server (RS) to be included as audience (aud claim) of the issued access token
   * @param {String} [parameters.scope] scopes requested for the issued tokens. e.g. `openid profile`
   * @returns {Promise}
   * @see https://auth0.com/docs/api-auth/grant/password#realm-support
   *
   * @memberof Auth
   */
  passwordRealm(parameters = {}) {
    const payload = apply(
      {
        parameters: {
          username: {required: true},
          password: {required: true},
          realm: {required: true},
          audience: {required: false},
          scope: {required: false},
        },
      },
      parameters,
    );
    return this.client
      .post('/oauth/token', {
        ...payload,
        client_id: this.clientId,
        grant_type: 'http://auth0.com/oauth/grant-type/password-realm',
      })
      .then(responseHandler);
  }

  /**
   * Obtain new tokens using the Refresh Token obtained during Auth (requesting `offline_access` scope)
   *
   * @param {Object} parameters refresh token parameters
   * @param {String} parameters.refreshToken user's issued refresh token
   * @param {String} [parameters.scope] scopes requested for the issued tokens. e.g. `openid profile`
   * @returns {Promise}
   * @see https://auth0.com/docs/tokens/refresh-token/current#use-a-refresh-token
   *
   * @memberof Auth
   */
  refreshToken(parameters = {}) {
    const payload = apply(
      {
        parameters: {
          refreshToken: {required: true, toName: 'refresh_token'},
          scope: {required: false},
        },
      },
      parameters,
    );
    return this.client
      .post('/oauth/token', {
        ...payload,
        client_id: this.clientId,
        grant_type: 'refresh_token',
      })
      .then(responseHandler);
  }

  /**
   * Starts the Passworldess flow with an email connection
   *
   * @param {Object} parameters passwordless parameters
   * @param {String} parameters.email the email to send the link/code to
   * @param {String} parameters.send the passwordless strategy, either 'link' or 'code'
   * @param {String} parameters.authParams optional parameters, used when strategy is 'linkˁ'
   * @returns {Promise}
   *
   * @memberof Auth
   */
  passwordlessWithEmail(parameters = {}) {
    const payload = apply(
      {
        parameters: {
          email: {required: true},
          send: {required: false},
          authParams: {required: false},
        },
      },
      parameters,
    );
    return this.client
      .post('/passwordless/start', {
        ...payload,
        connection: 'email',
        client_id: this.clientId,
      })
      .then(responseHandler);
  }

  /**
   * Starts the Passworldess flow with an SMS connection
   *
   * @param {Object} parameters passwordless parameters
   * @param {String} parameters.phoneNumber the phone number to send the link/code to
   * @returns {Promise}
   *
   * @memberof Auth
   */
  passwordlessWithSMS(parameters = {}) {
    const payload = apply(
      {
        parameters: {
          phoneNumber: {required: true, toName: 'phone_number'},
          send: {required: false},
          authParams: {required: false},
        },
      },
      parameters,
    );
    return this.client
      .post('/passwordless/start', {
        ...payload,
        connection: 'sms',
        client_id: this.clientId,
      })
      .then(responseHandler);
  }

  /**
   * Finishes the Passworldess authentication with an email connection
   *
   * @param {Object} parameters passwordless parameters
   * @param {String} parameters.email the email where the link/code was received
   * @param {String} parameters.code the code numeric value (OTP)
   * @param {String} parameters.audience optional API audience to request
   * @param {String} parameters.scope optional scopes to request
   * @returns {Promise}
   *
   * @memberof Auth
   */
  loginWithEmail(parameters = {}) {
    const payload = apply(
      {
        parameters: {
          email: {required: true, toName: 'username'},
          code: {required: true, toName: 'otp'},
          audience: {required: false},
          scope: {required: false},
        },
      },
      parameters,
    );
    return this.client
      .post('/oauth/token', {
        ...payload,
        client_id: this.clientId,
        realm: 'email',
        grant_type: 'http://auth0.com/oauth/grant-type/passwordless/otp',
      })
      .then(responseHandler);
  }

  /**
   * Finishes the Passworldess authentication with an SMS connection
   *
   * @param {Object} parameters passwordless parameters
   * @param {String} parameters.phoneNumber the phone number where the code was received
   * @param {String} parameters.code the code numeric value (OTP)
   * @param {String} parameters.audience optional API audience to request
   * @param {String} parameters.scope optional scopes to request
   * @returns {Promise}
   *
   * @memberof Auth
   */
  loginWithSMS(parameters = {}) {
    const payload = apply(
      {
        parameters: {
          phoneNumber: {required: true, toName: 'username'},
          code: {required: true, toName: 'otp'},
          audience: {required: false},
          scope: {required: false},
        },
      },
      parameters,
    );
    return this.client
      .post('/oauth/token', {
        ...payload,
        client_id: this.clientId,
        realm: 'sms',
        grant_type: 'http://auth0.com/oauth/grant-type/passwordless/otp',
      })
      .then(responseHandler);
  }

  /**
   * Revoke an issued refresh token
   *
   * @param {Object} parameters revoke token parameters
   * @param {String} parameters.refreshToken user's issued refresh token
   * @returns {Promise}
   *
   * @memberof Auth
   */
  revoke(parameters = {}) {
    const payload = apply(
      {
        parameters: {
          refreshToken: {required: true, toName: 'token'},
        },
      },
      parameters,
    );
    return this.client
      .post('/oauth/revoke', {
        ...payload,
        client_id: this.clientId,
      })
      .then(response => {
        if (response.ok) {
          return {};
        }
        throw new AuthError(response);
      });
  }

  /**
   * Return user information using an access token
   *
   * @param {Object} parameters user info parameters
   * @param {String} parameters.token user's access token
   * @returns {Promise}
   *
   * @memberof Auth
   */
  userInfo(parameters = {}) {
    const payload = apply(
      {
        parameters: {
          token: {required: true},
        },
      },
      parameters,
    );
    const {baseUrl, telemetry} = this.client;
    const client = new Client({baseUrl, telemetry, token: payload.token});
    const claims = [
      'sub',
      'name',
      'given_name',
      'family_name',
      'middle_name',
      'nickname',
      'preferred_username',
      'profile',
      'picture',
      'website',
      'email',
      'email_verified',
      'gender',
      'birthdate',
      'zoneinfo',
      'locale',
      'phone_number',
      'phone_number_verified',
      'address',
      'updated_at',
    ];
    return client
      .get('/userinfo')
      .then(response =>
        responseHandler(response, {attributes: claims, whitelist: true}),
      );
  }

  /**
   * Request an email with instructions to change password of a user
   *
   * @param {Object} parameters reset password parameters
   * @param {String} parameters.email user's email
   * @param {String} parameters.connection name of the connection of the user
   * @returns {Promise}
   *
   * @memberof Auth
   */
  resetPassword(parameters = {}) {
    const payload = apply(
      {
        parameters: {
          email: {required: true},
          connection: {required: true},
        },
      },
      parameters,
    );
    return this.client
      .post('/dbconnections/change_password', {
        ...payload,
        client_id: this.clientId,
      })
      .then(response => {
        if (response.ok) {
          return {};
        }
        throw new AuthError(response);
      });
  }

  /**
   *
   *
   * @param {Object} parameters create user parameters
   * @param {String} parameters.email user's email
   * @param {String} [parameters.username] user's username
   * @param {String} parameters.password user's password
   * @param {String} parameters.connection name of the database connection where to create the user
   * @param {String} [parameters.metadata] additional user information that will be stored in `user_metadata`
   * @returns {Promise}
   *
   * @memberof Auth
   */
  createUser(parameters = {}) {
    const payload = apply(
      {
        parameters: {
          email: {required: true},
          password: {required: true},
          connection: {required: true},
          username: {required: false},
          metadata: {required: false, toName: 'user_metadata'},
        },
      },
      parameters,
    );

    return this.client
      .post('/dbconnections/signup', {
        ...payload,
        client_id: this.clientId,
      })
      .then(response => {
        if (response.ok && response.json) {
          return toCamelCase(response.json);
        }
        throw new Auth0Error(response);
      });
  }
}