Skip to content

Commit a7e0098

Browse files
dschomvpomerleau
authored andcommitted
task(auth): Create a new auth strategy for verifiedSessionTokens
Because: - We want to create an auth strategy so it is easy to ensure a route requires a verified session state This Commit: - Creates a new auth strategy, verified-session-token - Registers strategy with server - Makes checks configurable - Emits metrics for failing checks
1 parent 9383bed commit a7e0098

5 files changed

Lines changed: 424 additions & 0 deletions

File tree

packages/fxa-auth-server/config/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,28 @@ const convictConf = convict({
3131
default: 1,
3232
env: 'AUTH_API_VERSION',
3333
},
34+
authStrategies: {
35+
verifiedSessionToken: {
36+
skipEmailVerifiedCheckForRoutes: {
37+
default: '',
38+
doc: 'A regex that defines the routes that should skip the email verification check.',
39+
env: 'AUTH_STRATEGIES__VERIFIED_SESSION_TOKEN__SKIP_EMAIL_CHECK_FOR_ROUTES',
40+
format: String,
41+
},
42+
skipTokenVerifiedCheckForRoutes: {
43+
default: '',
44+
doc: 'A regex that defines the routes that should skip the token verified check.',
45+
env: 'AUTH_STRATEGIES__VERIFIED_SESSION_TOKEN__SKIP_TOKEN_VERIFIED_CHECK_FOR_ROUTES',
46+
format: String,
47+
},
48+
skipAalCheckForRoutes: {
49+
default: '',
50+
doc: 'A regex that defines the routes that should skip the account assurance level check.',
51+
env: 'AUTH_STRATEGIES__VERIFIED_SESSION_TOKEN__SKIP_AAL_CHECK_FOR_ROUTES',
52+
format: String,
53+
},
54+
},
55+
},
3456
// TODO: Remove this after we have synchronized login records to Firestore
3557
firestore: {
3658
credentials: {

packages/fxa-auth-server/lib/routes/auth-schemes/hawk-fxa-token.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,4 +160,5 @@ function strategy(
160160

161161
module.exports = {
162162
strategy,
163+
parseAuthorizationHeader,
163164
};
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
'use strict';
6+
7+
const AppError = require('../../error');
8+
const authMethods = require('../../authMethods');
9+
const { parseAuthorizationHeader } = require('./hawk-fxa-token');
10+
11+
/**
12+
* Authentication strategy that validates a Hawk session token and ensures:
13+
* 1) account email is verified
14+
* 2) session token is verified (no tokenVerificationId)
15+
* 3) account AAL and session AAL match
16+
*
17+
* @param {Function} getCredentialsFunc - function to fetch a session token by id
18+
* @param {Object} db - database interface to fetch account and factors
19+
* @returns {Function}
20+
*/
21+
function strategy(getCredentialsFunc, db, config, statsd) {
22+
const tokenNotFoundError = () => {
23+
const error = AppError.unauthorized('Token not found');
24+
error.isMissing = true;
25+
return error;
26+
};
27+
28+
// Extract regular expressions to allow for optional skipping of certain routes for certain checks.
29+
const verifiedSessionTokenConfig =
30+
config?.authStrategies?.verifiedSessionToken;
31+
32+
const skipEmailVerifiedCheckForRoutes =
33+
verifiedSessionTokenConfig?.skipEmailVerifiedCheckForRoutes
34+
? new RegExp(verifiedSessionTokenConfig.skipEmailVerifiedCheckForRoutes)
35+
: null;
36+
37+
const skipTokenVerifiedCheckForRoutes =
38+
verifiedSessionTokenConfig?.skipTokenVerifiedCheckForRoutes
39+
? new RegExp(verifiedSessionTokenConfig.skipTokenVerifiedCheckForRoutes)
40+
: null;
41+
42+
const skipAalCheckForRoutes =
43+
verifiedSessionTokenConfig?.skipAalCheckForRoutes
44+
? new RegExp(verifiedSessionTokenConfig.skipAalCheckForRoutes)
45+
: null;
46+
47+
return function (server, options) {
48+
return {
49+
authenticate: async function (req, h) {
50+
const auth = req.headers.authorization;
51+
52+
if (!auth) {
53+
// if this strategy is selected, auth *cannot* be optional
54+
// "optional" mode is not supported
55+
throw tokenNotFoundError();
56+
}
57+
58+
const parsedHeader = parseAuthorizationHeader(auth);
59+
let token;
60+
try {
61+
token = await getCredentialsFunc(parsedHeader.id);
62+
} catch (_) {}
63+
64+
if (!token) {
65+
throw tokenNotFoundError();
66+
}
67+
68+
// Fetch the account for further checks
69+
const account = await db.account(token.uid);
70+
71+
// 1) account email is verified
72+
if (!account?.primaryEmail?.isVerified) {
73+
if (skipEmailVerifiedCheckForRoutes?.test(req.route.path)) {
74+
statsd?.increment(
75+
'verified_session_token.primary_email_not_verified.skipped',
76+
[
77+
// Important! Using req.route.path which has much lower cardinality than req.path
78+
req.route.path,
79+
]
80+
);
81+
} else {
82+
statsd?.increment(
83+
'verified_session_token.primary_email_not_verified.error',
84+
[req.route.path]
85+
);
86+
throw AppError.unverifiedAccount();
87+
}
88+
}
89+
90+
// 2) session token is verified
91+
if (token.tokenVerificationId || token.tokenVerified === false) {
92+
if (skipTokenVerifiedCheckForRoutes?.test(req.route.path)) {
93+
console.log('!!! verified_session_token.token_verified.skipped');
94+
statsd?.increment('verified_session_token.token_verified.skipped', [
95+
req.route.path,
96+
]);
97+
} else {
98+
statsd?.increment('verified_session_token.token_verified.error', [
99+
req.route.path,
100+
]);
101+
throw AppError.unverifiedSession();
102+
}
103+
}
104+
105+
// 3) account AAL and session AAL match
106+
const accountAmr = await authMethods.availableAuthenticationMethods(
107+
db,
108+
account
109+
);
110+
const accountAal = authMethods.maximumAssuranceLevel(accountAmr);
111+
const sessionAal = token.authenticatorAssuranceLevel;
112+
113+
if (accountAal !== sessionAal) {
114+
if (skipAalCheckForRoutes?.test(req.route.path)) {
115+
console.log('!!! verified_session_token.aal.skipped');
116+
statsd?.increment('verified_session_token.aal.skipped', [
117+
req.route.path,
118+
]);
119+
} else {
120+
statsd?.increment('verified_session_token.aal.error', [
121+
req.route.path,
122+
]);
123+
throw AppError.unauthorized('AAL mismatch');
124+
}
125+
}
126+
127+
return h.authenticated({
128+
credentials: token,
129+
});
130+
},
131+
};
132+
};
133+
}
134+
135+
module.exports = {
136+
strategy,
137+
};

packages/fxa-auth-server/lib/server.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const {
2525
} = require('fxa-shared/sentry/report-validation-error');
2626
const { logErrorWithGlean } = require('./metrics/glean');
2727
const mfa = require('./routes/auth-schemes/mfa');
28+
const verifiedSessionToken = require('./routes/auth-schemes/verified-session-token');
2829

2930
function trimLocale(header) {
3031
if (!header) {
@@ -497,6 +498,17 @@ async function create(log, error, config, routes, db, statsd, glean, customs) {
497498
);
498499
server.auth.strategy('mfa', 'mfa');
499500

501+
server.auth.scheme(
502+
'verified-session-token',
503+
verifiedSessionToken.strategy(
504+
makeCredentialFn(db.sessionToken.bind(db)),
505+
db,
506+
config,
507+
statsd
508+
)
509+
);
510+
server.auth.strategy('verifiedSessionToken', 'verified-session-token');
511+
500512
// register all plugins and Swagger configuration
501513
await server.register([
502514
{

0 commit comments

Comments
 (0)