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;
}