(core) Move user profile to new page and begin MFA work

Summary:
The user profile dialog is now a separate page, in preparation
for upcoming work to enable MFA. This commit also contains
some MFA changes, but the UI is currently disabled and the
implementation is limited to software tokens (TOTP) only.

Test Plan:
Updated browser tests for new profile page. Tests for MFAConfig
and CognitoClient will be added in a later diff, once the UI is enabled.

Reviewers: paulfitz

Reviewed By: paulfitz

Subscribers: dsagal

Differential Revision: https://phab.getgrist.com/D3199
This commit is contained in:
George Gevoian
2022-01-07 10:11:52 -08:00
parent 8f531ef622
commit ba6ecc5e9e
21 changed files with 1179 additions and 211 deletions

View File

@@ -13,16 +13,22 @@ export interface SessionUserObj {
// The user profile object.
profile?: UserProfile;
/**
* Unix time in seconds of the last successful login. Includes security
* verification prompts, such as those for configuring MFA preferences.
*/
lastLoginTimestamp?: number;
// [UNUSED] Authentication provider string indicating the login method used.
authProvider?: string;
// [UNUSED] Login ID token used to access AWS services.
idToken?: string;
// [UNUSED] Login access token used to access other AWS services.
// Login access token used to access other AWS services.
accessToken?: string;
// [UNUSED] Login refresh token used to retrieve new ID and access tokens.
// Login refresh token used to retrieve new ID and access tokens.
refreshToken?: string;
// State for SAML-mediated logins.
@@ -166,14 +172,16 @@ export class ScopedSession {
// This is mainly used to know which emails are logged in in this session; fields like name and
// picture URL come from the database instead.
public async updateUserProfile(req: Request, profile: UserProfile|null): Promise<void> {
if (profile) {
await this.operateOnScopedSession(req, async user => {
user.profile = profile;
return user;
});
} else {
await this.clearScopedSession(req);
}
profile ? await this.updateUser(req, {profile}) : await this.clearScopedSession(req);
}
/**
* Updates the properties of the current session user.
*
* @param {Partial<SessionUserObj>} newProps New property values to set.
*/
public async updateUser(req: Request, newProps: Partial<SessionUserObj>): Promise<void> {
await this.operateOnScopedSession(req, async user => ({...user, ...newProps}));
}
/**

View File

@@ -30,7 +30,7 @@ import {DocManager} from 'app/server/lib/DocManager';
import {DocStorageManager} from 'app/server/lib/DocStorageManager';
import {DocWorker} from 'app/server/lib/DocWorker';
import {DocWorkerInfo, IDocWorkerMap} from 'app/server/lib/DocWorkerMap';
import {expressWrap, jsonErrorHandler} from 'app/server/lib/expressWrap';
import {expressWrap, jsonErrorHandler, secureJsonErrorHandler} from 'app/server/lib/expressWrap';
import {Hosts, RequestWithOrg} from 'app/server/lib/extractOrg';
import {addGoogleAuthEndpoint} from "app/server/lib/GoogleAuth";
import {GristLoginMiddleware, GristServer, RequestWithGrist} from 'app/server/lib/GristServer';
@@ -521,6 +521,7 @@ export class FlexServer implements GristServer {
});
// Add a final error handler for /api endpoints that reports errors as JSON.
this.app.use('/api/auth', secureJsonErrorHandler);
this.app.use('/api', jsonErrorHandler);
}
@@ -1003,6 +1004,18 @@ export class FlexServer implements GristServer {
this._disableS3 = true;
}
public addAccountPage() {
const middleware = [
this._redirectToHostMiddleware,
this._userIdMiddleware,
this._redirectToLoginWithoutExceptionsMiddleware
];
this.app.get('/account', ...middleware, expressWrap(async (req, resp) => {
return this._sendAppPage(req, resp, {path: 'account.html', status: 200, config: {}});
}));
}
public addBillingPages() {
const middleware = [
this._redirectToHostMiddleware,

View File

@@ -15,25 +15,52 @@ export function expressWrap(callback: express.RequestHandler): express.RequestHa
};
}
interface JsonErrorHandlerOptions {
shouldLogBody?: boolean;
shouldLogParams?: boolean;
}
/**
* Returns a custom error-handling middleware that responds to errors in json.
*
* Currently allows for toggling of logging request bodies and params.
*/
const buildJsonErrorHandler = (options: JsonErrorHandlerOptions = {}): express.ErrorRequestHandler => {
return (err, req, res, _next) => {
const mreq = req as RequestWithLogin;
log.warn(
"Error during api call to %s: (%s) user %d%s%s",
req.path, err.message, mreq.userId,
options.shouldLogParams !== false ? ` params ${JSON.stringify(req.params)}` : '',
options.shouldLogBody !== false ? ` body ${JSON.stringify(req.body)}` : '',
);
let details = err.details && {...err.details};
const status = details?.status || err.status || 500;
if (details) {
// Remove some details exposed for websocket API only.
delete details.accessMode;
delete details.status; // TODO: reconcile err.status and details.status, no need for both.
if (Object.keys(details).length === 0) { details = undefined; }
}
res.status(status).json({error: err.message || 'internal error', details});
};
};
/**
* Error-handling middleware that responds to errors in json. The status code is taken from
* error.status property (for which ApiError is convenient), and defaults to 500.
*/
export const jsonErrorHandler: express.ErrorRequestHandler = (err, req, res, next) => {
const mreq = req as RequestWithLogin;
log.warn("Error during api call to %s: (%s) user %d params %s body %s", req.path, err.message,
mreq.userId,
JSON.stringify(req.params), JSON.stringify(req.body));
let details = err.details && {...err.details};
const status = details?.status || err.status || 500;
if (details) {
// Remove some details exposed for websocket API only.
delete details.accessMode;
delete details.status; // TODO: reconcile err.status and details.status, no need for both.
if (Object.keys(details).length === 0) { details = undefined; }
}
res.status(status).json({error: err.message || 'internal error', details});
};
export const jsonErrorHandler: express.ErrorRequestHandler = buildJsonErrorHandler();
/**
* Variant of `jsonErrorHandler` that skips logging request bodies and params.
*
* Should be used for sensitive routes, such as those under '/api/auth/'.
*/
export const secureJsonErrorHandler: express.ErrorRequestHandler = buildJsonErrorHandler({
shouldLogBody: false,
shouldLogParams: false,
});
/**
* Middleware that responds with a 404 status and a json error object.