async function create()

in packages/fxa-auth-server/lib/server.js [84:492]


async function create(log, error, config, routes, db, statsd, glean) {
  const getGeoData = require('./geodb')(log);
  const metricsContext = require('./metrics/context')(log, config);
  const metricsEvents = require('./metrics/events')(log, config, glean);
  const { sharedSecret: SUBSCRIPTIONS_SECRET } = config.subscriptions;

  function makeCredentialFn(dbGetFn) {
    return function (id) {
      log.trace('DB.getToken', { id: id });
      if (!HEX_STRING.test(id)) {
        return null;
      }

      return (async () => {
        const token = await dbGetFn(id);

        if (!token.expired(Date.now())) {
          return token;
        }

        const err = error.invalidToken('The authentication token has expired');
        if (token.constructor.tokenTypeID === 'sessionToken') {
          return (async () => {
            try {
              await db.pruneSessionTokens(token.uid, [token]);
            } catch (ignoreError) {
              // Ignore errors
            }
            throw err;
          })();
        }
        return null;
      })();
    };
  }

  const serverOptions = {
    host: config.listen.host,
    port: config.listen.port,
    routes: {
      cors: {
        additionalExposedHeaders: ['Timestamp', 'Accept-Language'],
        additionalHeaders: ['sentry-trace', 'baggage'],
        // If we're accepting CORS from any origin then use Hapi's "ignore" mode,
        // which is more forgiving of missing Origin header.
        origin: config.corsOrigin[0] === '*' ? 'ignore' : config.corsOrigin,
      },
      security: {
        hsts: {
          maxAge: 31536000,
          includeSubdomains: true,
        },
      },
      state: {
        parse: false,
      },
      payload: {
        maxBytes: 16384,
      },
      files: {
        relativeTo: path.dirname(__dirname),
      },
      validate: {
        options: {
          stripUnknown: true,
        },
        failAction: async (request, h, err) => {
          // Starting with Hapi 17, the framework hides the validation info
          // We want the full validation information and use it in `onPreResponse` below

          // See: https://github.com/hapijs/hapi/issues/3706#issuecomment-349765943
          throw err;
        },
      },
      response: { options: { abortEarly: false } },
    },
    load: {
      sampleInterval: 1000,
      maxEventLoopDelay: config.maxEventLoopDelay,
    },
  };

  if (config.useHttps) {
    serverOptions.tls = {
      key: fs.readFileSync(config.keyPath),
      cert: fs.readFileSync(config.certPath),
    };
  }

  const server = new Hapi.Server(serverOptions);
  server.validator(require('joi'));

  server.ext('onRequest', (request, h) => {
    log.begin('server.onRequest', request);
    return h.continue;
  });

  server.ext('onPreAuth', (request, h) => {
    defineLazyGetter(request.app, 'remoteAddressChain', () => {
      return getRemoteAddressChain(request, config.remoteAddressChainOverride);
    });

    defineLazyGetter(request.app, 'clientAddress', () => {
      const remoteAddressChain = request.app.remoteAddressChain;
      let clientAddressIndex =
        remoteAddressChain.length - (config.clientAddressDepth || 1);

      if (clientAddressIndex < 0) {
        clientAddressIndex = 0;
      }

      return remoteAddressChain[clientAddressIndex];
    });

    defineLazyGetter(request.app, 'acceptLanguage', () =>
      trimLocale(request.headers['accept-language'])
    );
    defineLazyGetter(request.app, 'locale', () =>
      determineLocale(request.app.acceptLanguage)
    );

    defineLazyGetter(request.app, 'ua', () =>
      userAgent(request.headers['user-agent'])
    );
    defineLazyGetter(request.app, 'geo', () =>
      getGeoData(request.app.clientAddress)
    );
    defineLazyGetter(request.app, 'metricsContext', () =>
      metricsContext.get(request)
    );

    defineLazyGetter(request.app, 'devices', () => {
      let uid;

      if (
        request.auth &&
        request.auth.credentials &&
        request.auth.credentials.uid
      ) {
        // sessionToken strategy comes with uid as uid
        uid = request.auth.credentials.uid;
      } else if (
        request.auth &&
        request.auth.credentials &&
        request.auth.credentials.user
      ) {
        // oauthToken strategy comes with uid as user
        uid = request.auth.credentials.user;
      } else if (request.payload && request.payload.uid) {
        uid = request.payload.uid;
      }

      return db.devices(uid);
    });

    defineLazyGetter(request.app, 'isMetricsEnabled', async () => {
      // This catches most but not all cases where the given uid
      // is opted out and saves us from making a db call further down.
      // Note that unverified accounts can not be opted out of metrics.

      let uid;
      if (
        request.auth &&
        request.auth.credentials &&
        request.auth.credentials.uid
      ) {
        // sessionToken strategy sets this property already
        return !request.auth.credentials.metricsOptOutAt;
      } else if (
        request.auth &&
        request.auth.credentials &&
        request.auth.credentials.user
      ) {
        // oauthToken strategy comes with uid as user
        uid = request.auth.credentials.user;
      } else if (request.payload && request.payload.uid) {
        // Some unauthenticated requests might set uid in payload, ex. `/account/status`
        uid = request.payload.uid;
      } else if (request.app.metricsEventUid) {
        // For access tokens, and webhooks, we stash the uid
        uid = request.app.metricsEventUid;
      } else if (request.payload && request.payload.email) {
        // last resort is to check if an email is in the payload
        try {
          const account = await Account.findByPrimaryEmail(
            request.payload.email
          );
          uid = account.uid;
        } catch (err) {
          // Unknown accounts will have the default experience
        }
      }

      if (!uid) {
        return true;
      }

      return Account.metricsEnabled(uid);
    });

    if (request.headers.authorization) {
      // Log some helpful details for debugging authentication problems.
      log.trace('server.onPreAuth', {
        rid: request.id,
        path: request.path,
        auth: request.headers.authorization,
        type: request.headers['content-type'] || '',
      });
    }

    return h.continue;
  });

  server.ext('onPreHandler', (request, h) => {
    const features = request.payload && request.payload.features;
    request.app.features = new Set(Array.isArray(features) ? features : []);

    return h.continue;
  });

  server.ext('onPreResponse', (request, h) => {
    let response = request.response;
    if (response.isBoom) {
      logEndpointErrors(response, log);
      logErrorWithGlean({ glean, request, error: response });

      // Do not log errors that either aren't a validation error or have a status code below 500
      // ValidationError that are 4xx status are request validation errors
      if (
        response?.__proto__.name === 'ValidationError' &&
        response.output &&
        response.output.statusCode >= 500
      ) {
        logValidationError(response, log);
      }

      if (config.env !== 'prod') {
        translateStripeErrors(response);
      }

      response = error.translate(request, response);
      if (config.env !== 'prod') {
        response.backtrace(request.app.traced);
      }
    }
    response.header('Timestamp', `${Math.floor(Date.now() / 1000)}`);
    return response;
  });

  const metricReporter = metricFactory(statsd);
  server.events.on('response', (request) => {
    log.summary(request, request.response);
    metricReporter(request);
  });

  // Configure Sentry... Note, Sentry will already be initialized by this point,
  // but this will add a couple hooks into hapi request life cycle so that transactions
  // can be created.
  await configureSentry(server, config);

  server.decorate('request', 'stashMetricsContext', metricsContext.stash);
  server.decorate('request', 'gatherMetricsContext', metricsContext.gather);
  server.decorate(
    'request',
    'propagateMetricsContext',
    metricsContext.propagate
  );
  server.decorate('request', 'clearMetricsContext', metricsContext.clear);
  server.decorate('request', 'validateMetricsContext', metricsContext.validate);
  server.decorate(
    'request',
    'setMetricsFlowCompleteSignal',
    metricsContext.setFlowCompleteSignal
  );

  server.decorate('request', 'emitMetricsEvent', metricsEvents.emit);
  server.decorate(
    'request',
    'emitRouteFlowEvent',
    metricsEvents.emitRouteFlowEvent
  );

  server.stat = function () {
    return {
      stat: 'mem',
      rss: server.load.rss,
      heapUsed: server.load.heapUsed,
    };
  };

  await server.register(require('hapi-auth-jwt2'));

  const hawkFxAToken = require('./routes/auth-schemes/hawk-fxa-token');
  // Register auth strategies for all token types. These strategies support Hawk (without validation) and FxA token types.
  server.auth.scheme(
    'fxa-hawk-session-token',
    hawkFxAToken.strategy(makeCredentialFn(db.sessionToken.bind(db)))
  );
  server.auth.scheme(
    'fxa-hawk-keyFetch-token',
    hawkFxAToken.strategy(makeCredentialFn(db.keyFetchToken.bind(db)))
  );
  server.auth.scheme(
    'fxa-hawk-keyFetch-with-verification-token',
    hawkFxAToken.strategy(
      makeCredentialFn(db.keyFetchTokenWithVerificationStatus.bind(db))
    )
  );
  server.auth.scheme(
    'fxa-hawk-accountReset-token',
    hawkFxAToken.strategy(makeCredentialFn(db.accountResetToken.bind(db)))
  );
  server.auth.scheme(
    'fxa-hawk-passwordForgot-token',
    hawkFxAToken.strategy(makeCredentialFn(db.passwordForgotToken.bind(db)))
  );
  server.auth.scheme(
    'fxa-hawk-passwordChange-token',
    hawkFxAToken.strategy(makeCredentialFn(db.passwordChangeToken.bind(db)))
  );

  // the recoveryKey/exists route accepts a session token or a password-forgot
  // token.  in order for Hapi to try all the strategies (until auth succeeds),
  // the strategy's authenticate function _must not_ throw on failed
  // authentication. otherwise, Hapi will stop at the first strategy that
  // throws.
  server.auth.scheme(
    'multi-strategy-fxa-hawk-session-token',
    hawkFxAToken.strategy(makeCredentialFn(db.sessionToken.bind(db)), {
      throwOnFailure: false,
    })
  );
  server.auth.scheme(
    'multi-strategy-fxa-hawk-passwordForgot-token',
    hawkFxAToken.strategy(makeCredentialFn(db.passwordForgotToken.bind(db)), {
      throwOnFailure: false,
    })
  );

  server.auth.strategy('sessionToken', 'fxa-hawk-session-token');
  server.auth.strategy('keyFetchToken', 'fxa-hawk-keyFetch-token');
  server.auth.strategy(
    // This strategy fetches the keyFetchToken with its
    // verification state. It doesn't check that state.
    'keyFetchTokenWithVerificationStatus',
    'fxa-hawk-keyFetch-with-verification-token'
  );
  server.auth.strategy('accountResetToken', 'fxa-hawk-accountReset-token');
  server.auth.strategy('passwordForgotToken', 'fxa-hawk-passwordForgot-token');
  server.auth.strategy('passwordChangeToken', 'fxa-hawk-passwordChange-token');

  server.auth.strategy(
    'multiStrategySessionToken',
    'multi-strategy-fxa-hawk-session-token'
  );
  server.auth.strategy(
    'multiStrategyPasswordForgotToken',
    'multi-strategy-fxa-hawk-passwordForgot-token'
  );

  server.auth.scheme(authOauth.AUTH_SCHEME, authOauth.strategy);
  server.auth.strategy('oauthToken', authOauth.AUTH_SCHEME, config.oauth);

  server.auth.scheme('fxa-oauth-refreshToken', schemeRefreshToken(config, db));

  server.auth.strategy('refreshToken', 'fxa-oauth-refreshToken');

  server.auth.scheme(
    'subscriptionsSecret',
    sharedSecretAuth.strategy(SUBSCRIPTIONS_SECRET)
  );
  server.auth.strategy('subscriptionsSecret', 'subscriptionsSecret');

  server.auth.scheme(
    'supportSecret',
    sharedSecretAuth.strategy(`Bearer ${config.support.secretBearerToken}`, {
      throwOnFailure: false,
    })
  );
  server.auth.strategy('supportSecret', 'supportSecret');

  server.auth.strategy('pubsub', 'jwt', pubsubAuth.strategy(config));

  server.auth.scheme(
    'cloudTasksOIDC',
    googleOIDC.strategy(config.cloudTasks.oidc)
  );
  server.auth.strategy('cloudTasksOIDC', 'cloudTasksOIDC');

  server.auth.scheme(
    'cloudSchedulerOIDC',
    googleOIDC.strategy(config.cloudScheduler.oidc)
  );
  server.auth.strategy('cloudSchedulerOIDC', 'cloudSchedulerOIDC');

  // register all plugins and Swagger configuration
  await server.register([
    {
      plugin: HapiSwagger,
      options: swaggerOptions,
    },
  ]);

  // routes should be registered after all auth strategies have initialized:
  // ref: http://hapijs.com/tutorials/auth

  server.route(routes);
  return server;
}