Page MenuHomePhabricator

No OneTemporary

diff --git a/keyserver/src/responders/user-responders.js b/keyserver/src/responders/user-responders.js
index 3cd00a3d3..f7d1c3f82 100644
--- a/keyserver/src/responders/user-responders.js
+++ b/keyserver/src/responders/user-responders.js
@@ -1,568 +1,581 @@
// @flow
import invariant from 'invariant';
import { ErrorTypes, SiweMessage } from 'siwe';
import t from 'tcomb';
import bcrypt from 'twin-bcrypt';
import { baseLegalPolicies, policies } from 'lib/facts/policies.js';
import { hasMinCodeVersion } from 'lib/shared/version-utils.js';
import type {
ResetPasswordRequest,
LogOutResponse,
DeleteAccountRequest,
RegisterResponse,
RegisterRequest,
LogInResponse,
LogInRequest,
UpdatePasswordRequest,
UpdateUserSettingsRequest,
PolicyAcknowledgmentRequest,
} from 'lib/types/account-types.js';
import {
userSettingsTypes,
notificationTypeValues,
logInActionSources,
} from 'lib/types/account-types.js';
-import type { IdentityKeysBlob } from 'lib/types/crypto-types.js';
+import type {
+ IdentityKeysBlob,
+ SignedIdentityKeysBlob,
+} from 'lib/types/crypto-types.js';
import type { CalendarQuery } from 'lib/types/entry-types.js';
import { defaultNumberPerThread } from 'lib/types/message-types.js';
import type {
SIWEAuthRequest,
SIWEMessage,
SIWESocialProof,
} from 'lib/types/siwe-types.js';
import type {
SubscriptionUpdateRequest,
SubscriptionUpdateResponse,
} from 'lib/types/subscription-types.js';
import type { PasswordUpdate } from 'lib/types/user-types.js';
import { ServerError } from 'lib/utils/errors.js';
import { values } from 'lib/utils/objects.js';
import { promiseAll } from 'lib/utils/promises.js';
import {
getPublicKeyFromSIWEStatement,
isValidSIWEMessage,
isValidSIWEStatementWithPublicKey,
primaryIdentityPublicKeyRegex,
} from 'lib/utils/siwe-utils.js';
import {
tShape,
tPlatformDetails,
tPassword,
tEmail,
tOldValidUsername,
tRegex,
} from 'lib/utils/validation-utils.js';
import {
entryQueryInputValidator,
newEntryQueryInputValidator,
normalizeCalendarQuery,
verifyCalendarQueryThreadIDs,
} from './entry-responders.js';
import {
createAccount,
processSIWEAccountCreation,
} from '../creators/account-creator.js';
import { dbQuery, SQL } from '../database/database.js';
import { deleteAccount } from '../deleters/account-deleters.js';
import { deleteCookie } from '../deleters/cookie-deleters.js';
import { checkAndInvalidateSIWENonceEntry } from '../deleters/siwe-nonce-deleters.js';
import { fetchEntryInfos } from '../fetchers/entry-fetchers.js';
import { fetchMessageInfos } from '../fetchers/message-fetchers.js';
import { fetchNotAcknowledgedPolicies } from '../fetchers/policy-acknowledgment-fetchers.js';
import { fetchThreadInfos } from '../fetchers/thread-fetchers.js';
import {
fetchKnownUserInfos,
fetchLoggedInUserInfo,
fetchUserIDForEthereumAddress,
} from '../fetchers/user-fetchers.js';
import {
createNewAnonymousCookie,
createNewUserCookie,
setNewSession,
} from '../session/cookies.js';
import type { Viewer } from '../session/viewer.js';
import {
accountUpdater,
checkAndSendVerificationEmail,
checkAndSendPasswordResetEmail,
updatePassword,
updateUserSettings,
} from '../updaters/account-updaters.js';
import { userSubscriptionUpdater } from '../updaters/user-subscription-updaters.js';
import { viewerAcknowledgmentUpdater } from '../updaters/viewer-acknowledgment-updater.js';
import { getOLMUtility } from '../utils/olm-utils.js';
import type { OLMUtility } from '../utils/olm-utils.js';
import { validateInput } from '../utils/validation-utils.js';
const subscriptionUpdateRequestInputValidator = tShape({
threadID: t.String,
updatedFields: tShape({
pushNotifs: t.maybe(t.Boolean),
home: t.maybe(t.Boolean),
}),
});
async function userSubscriptionUpdateResponder(
viewer: Viewer,
input: any,
): Promise<SubscriptionUpdateResponse> {
const request: SubscriptionUpdateRequest = input;
await validateInput(viewer, subscriptionUpdateRequestInputValidator, request);
const threadSubscription = await userSubscriptionUpdater(viewer, request);
return { threadSubscription };
}
const accountUpdateInputValidator = tShape({
updatedFields: tShape({
email: t.maybe(tEmail),
password: t.maybe(tPassword),
}),
currentPassword: tPassword,
});
async function passwordUpdateResponder(
viewer: Viewer,
input: any,
): Promise<void> {
const request: PasswordUpdate = input;
await validateInput(viewer, accountUpdateInputValidator, request);
await accountUpdater(viewer, request);
}
async function sendVerificationEmailResponder(viewer: Viewer): Promise<void> {
await validateInput(viewer, null, null);
await checkAndSendVerificationEmail(viewer);
}
const resetPasswordRequestInputValidator = tShape({
usernameOrEmail: t.union([tEmail, tOldValidUsername]),
});
async function sendPasswordResetEmailResponder(
viewer: Viewer,
input: any,
): Promise<void> {
const request: ResetPasswordRequest = input;
await validateInput(viewer, resetPasswordRequestInputValidator, request);
await checkAndSendPasswordResetEmail(request);
}
async function logOutResponder(viewer: Viewer): Promise<LogOutResponse> {
await validateInput(viewer, null, null);
if (viewer.loggedIn) {
const [anonymousViewerData] = await Promise.all([
createNewAnonymousCookie({
platformDetails: viewer.platformDetails,
deviceToken: viewer.deviceToken,
}),
deleteCookie(viewer.cookieID),
]);
viewer.setNewCookie(anonymousViewerData);
}
return {
currentUserInfo: {
id: viewer.id,
anonymous: true,
},
};
}
const deleteAccountRequestInputValidator = tShape({
password: t.maybe(tPassword),
});
async function accountDeletionResponder(
viewer: Viewer,
input: any,
): Promise<LogOutResponse> {
const request: DeleteAccountRequest = input;
await validateInput(viewer, deleteAccountRequestInputValidator, request);
const result = await deleteAccount(viewer, request);
invariant(result, 'deleteAccount should return result if handed request');
return result;
}
const deviceTokenUpdateRequestInputValidator = tShape({
deviceType: t.maybe(t.enums.of(['ios', 'android'])),
deviceToken: t.String,
});
const registerRequestInputValidator = tShape({
username: t.String,
email: t.maybe(tEmail),
password: tPassword,
calendarQuery: t.maybe(newEntryQueryInputValidator),
deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator),
platformDetails: tPlatformDetails,
// We include `primaryIdentityPublicKey` to avoid breaking
// old clients, but we no longer do anything with it.
primaryIdentityPublicKey: t.maybe(tRegex(primaryIdentityPublicKeyRegex)),
});
async function accountCreationResponder(
viewer: Viewer,
input: any,
): Promise<RegisterResponse> {
const request: RegisterRequest = input;
await validateInput(viewer, registerRequestInputValidator, request);
return await createAccount(viewer, request);
}
type ProcessSuccessfulLoginParams = {
+viewer: Viewer,
+input: any,
+userID: string,
+calendarQuery: ?CalendarQuery,
+socialProof?: ?SIWESocialProof,
+ +signedIdentityKeysBlob?: ?SignedIdentityKeysBlob,
};
async function processSuccessfulLogin(
params: ProcessSuccessfulLoginParams,
): Promise<LogInResponse> {
- const { viewer, input, userID, calendarQuery, socialProof } = params;
+ const {
+ viewer,
+ input,
+ userID,
+ calendarQuery,
+ socialProof,
+ signedIdentityKeysBlob,
+ } = params;
const request: LogInRequest = input;
const newServerTime = Date.now();
const deviceToken = request.deviceTokenUpdateRequest
? request.deviceTokenUpdateRequest.deviceToken
: viewer.deviceToken;
const [userViewerData, notAcknowledgedPolicies] = await Promise.all([
createNewUserCookie(userID, {
platformDetails: request.platformDetails,
deviceToken,
socialProof,
+ signedIdentityKeysBlob,
}),
fetchNotAcknowledgedPolicies(userID, baseLegalPolicies),
deleteCookie(viewer.cookieID),
]);
viewer.setNewCookie(userViewerData);
if (
notAcknowledgedPolicies.length &&
hasMinCodeVersion(viewer.platformDetails, 181)
) {
const currentUserInfo = await fetchLoggedInUserInfo(viewer);
return {
notAcknowledgedPolicies,
currentUserInfo: currentUserInfo,
rawMessageInfos: [],
truncationStatuses: {},
userInfos: [],
rawEntryInfos: [],
serverTime: 0,
cookieChange: {
threadInfos: {},
userInfos: [],
},
};
}
if (calendarQuery) {
await setNewSession(viewer, calendarQuery, newServerTime);
}
const threadCursors = {};
for (const watchedThreadID of request.watchedIDs) {
threadCursors[watchedThreadID] = null;
}
const messageSelectionCriteria = { threadCursors, joinedThreads: true };
const [
threadsResult,
messagesResult,
entriesResult,
userInfos,
currentUserInfo,
] = await Promise.all([
fetchThreadInfos(viewer),
fetchMessageInfos(viewer, messageSelectionCriteria, defaultNumberPerThread),
calendarQuery ? fetchEntryInfos(viewer, [calendarQuery]) : undefined,
fetchKnownUserInfos(viewer),
fetchLoggedInUserInfo(viewer),
]);
const rawEntryInfos = entriesResult ? entriesResult.rawEntryInfos : null;
const response: LogInResponse = {
currentUserInfo,
rawMessageInfos: messagesResult.rawMessageInfos,
truncationStatuses: messagesResult.truncationStatuses,
serverTime: newServerTime,
userInfos: values(userInfos),
cookieChange: {
threadInfos: threadsResult.threadInfos,
userInfos: [],
},
};
if (rawEntryInfos) {
return {
...response,
rawEntryInfos,
};
}
return response;
}
const signedIdentityKeysBlobValidator = tShape({
payload: t.String,
signature: t.String,
});
const logInRequestInputValidator = tShape({
username: t.maybe(t.String),
usernameOrEmail: t.maybe(t.union([tEmail, tOldValidUsername])),
password: tPassword,
watchedIDs: t.list(t.String),
calendarQuery: t.maybe(entryQueryInputValidator),
deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator),
platformDetails: tPlatformDetails,
source: t.maybe(t.enums.of(values(logInActionSources))),
// We include `primaryIdentityPublicKey` to avoid breaking
// old clients, but we no longer do anything with it.
primaryIdentityPublicKey: t.maybe(tRegex(primaryIdentityPublicKeyRegex)),
signedIdentityKeysBlob: t.maybe(signedIdentityKeysBlobValidator),
});
async function logInResponder(
viewer: Viewer,
input: any,
): Promise<LogInResponse> {
await validateInput(viewer, logInRequestInputValidator, input);
const request: LogInRequest = input;
const { signedIdentityKeysBlob } = request;
if (signedIdentityKeysBlob) {
const identityKeys: IdentityKeysBlob = JSON.parse(
signedIdentityKeysBlob.payload,
);
const olmUtil: OLMUtility = getOLMUtility();
try {
olmUtil.ed25519_verify(
identityKeys.primaryIdentityPublicKeys.ed25519,
signedIdentityKeysBlob.payload,
signedIdentityKeysBlob.signature,
);
} catch (e) {
throw new ServerError('invalid_signature');
}
}
const calendarQuery = request.calendarQuery
? normalizeCalendarQuery(request.calendarQuery)
: null;
const promises = {};
if (calendarQuery) {
promises.verifyCalendarQueryThreadIDs =
verifyCalendarQueryThreadIDs(calendarQuery);
}
const username = request.username ?? request.usernameOrEmail;
if (!username) {
if (hasMinCodeVersion(viewer.platformDetails, 150)) {
throw new ServerError('invalid_credentials');
} else {
throw new ServerError('invalid_parameters');
}
}
const userQuery = SQL`
SELECT id, hash, username
FROM users
WHERE LCASE(username) = LCASE(${username})
`;
promises.userQuery = dbQuery(userQuery);
const {
userQuery: [userResult],
} = await promiseAll(promises);
if (userResult.length === 0) {
if (hasMinCodeVersion(viewer.platformDetails, 150)) {
throw new ServerError('invalid_credentials');
} else {
throw new ServerError('invalid_parameters');
}
}
const userRow = userResult[0];
if (!userRow.hash || !bcrypt.compareSync(request.password, userRow.hash)) {
throw new ServerError('invalid_credentials');
}
const id = userRow.id.toString();
return await processSuccessfulLogin({
viewer,
input,
userID: id,
calendarQuery,
+ signedIdentityKeysBlob,
});
}
const siweAuthRequestInputValidator = tShape({
signature: t.String,
message: t.String,
calendarQuery: entryQueryInputValidator,
deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator),
platformDetails: tPlatformDetails,
watchedIDs: t.list(t.String),
signedIdentityKeysBlob: t.maybe(signedIdentityKeysBlobValidator),
});
async function siweAuthResponder(
viewer: Viewer,
input: any,
): Promise<LogInResponse> {
await validateInput(viewer, siweAuthRequestInputValidator, input);
const request: SIWEAuthRequest = input;
const { message, signature, deviceTokenUpdateRequest, platformDetails } =
request;
const calendarQuery = normalizeCalendarQuery(request.calendarQuery);
// 1. Ensure that `message` is a well formed Comm SIWE Auth message.
const siweMessage: SIWEMessage = new SiweMessage(message);
if (!isValidSIWEMessage(siweMessage)) {
throw new ServerError('invalid_parameters');
}
// 2. Ensure that the `nonce` exists in the `siwe_nonces` table
// AND hasn't expired. If those conditions are met, delete the entry to
// ensure that the same `nonce` can't be re-used in a future request.
const wasNonceCheckedAndInvalidated = await checkAndInvalidateSIWENonceEntry(
siweMessage.nonce,
);
if (!wasNonceCheckedAndInvalidated) {
throw new ServerError('invalid_parameters');
}
// 3. Validate SIWEMessage signature and handle possible errors.
try {
await siweMessage.validate(signature);
} catch (error) {
if (error === ErrorTypes.EXPIRED_MESSAGE) {
// Thrown when the `expirationTime` is present and in the past.
throw new ServerError('expired_message');
} else if (error === ErrorTypes.INVALID_SIGNATURE) {
// Thrown when the `validate()` function can't verify the message.
throw new ServerError('invalid_signature');
} else if (error === ErrorTypes.MALFORMED_SESSION) {
// Thrown when some required field is missing.
throw new ServerError('malformed_session');
} else {
throw new ServerError('unknown_error');
}
}
// 4. Pull `primaryIdentityPublicKey` out from SIWEMessage `statement`
// if it was included. We expect it to be included for native clients,
// and we expect it to be EXCLUDED for web clients.
const { statement } = siweMessage;
// eslint-disable-next-line no-unused-vars
const primaryIdentityPublicKey =
statement && isValidSIWEStatementWithPublicKey(statement)
? getPublicKeyFromSIWEStatement(statement)
: null;
// 5. Construct `SIWESocialProof` object with the stringified
// SIWEMessage and the corresponding signature.
const socialProof: SIWESocialProof = {
siweMessage: siweMessage.toMessage(),
siweMessageSignature: signature,
};
// 6. Create account with call to `processSIWEAccountCreation(...)`
// if address does not correspond to an existing user.
let userID = await fetchUserIDForEthereumAddress(siweMessage.address);
if (!userID) {
const siweAccountCreationRequest = {
address: siweMessage.address,
calendarQuery,
deviceTokenUpdateRequest,
platformDetails,
socialProof,
};
userID = await processSIWEAccountCreation(
viewer,
siweAccountCreationRequest,
);
}
// 7. Complete login with call to `processSuccessfulLogin(...)`.
return await processSuccessfulLogin({
viewer,
input,
userID,
calendarQuery,
socialProof,
});
}
const updatePasswordRequestInputValidator = tShape({
code: t.String,
password: tPassword,
watchedIDs: t.list(t.String),
calendarQuery: t.maybe(entryQueryInputValidator),
deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator),
platformDetails: tPlatformDetails,
});
async function oldPasswordUpdateResponder(
viewer: Viewer,
input: any,
): Promise<LogInResponse> {
await validateInput(viewer, updatePasswordRequestInputValidator, input);
const request: UpdatePasswordRequest = input;
if (request.calendarQuery) {
request.calendarQuery = normalizeCalendarQuery(request.calendarQuery);
}
return await updatePassword(viewer, request);
}
const updateUserSettingsInputValidator = tShape({
name: t.irreducible(
userSettingsTypes.DEFAULT_NOTIFICATIONS,
x => x === userSettingsTypes.DEFAULT_NOTIFICATIONS,
),
data: t.enums.of(notificationTypeValues),
});
async function updateUserSettingsResponder(
viewer: Viewer,
input: any,
): Promise<void> {
const request: UpdateUserSettingsRequest = input;
await validateInput(viewer, updateUserSettingsInputValidator, request);
return await updateUserSettings(viewer, request);
}
const policyAcknowledgmentRequestInputValidator = tShape({
policy: t.maybe(t.enums.of(policies)),
});
async function policyAcknowledgmentResponder(
viewer: Viewer,
input: any,
): Promise<void> {
const request: PolicyAcknowledgmentRequest = input;
await validateInput(
viewer,
policyAcknowledgmentRequestInputValidator,
request,
);
await viewerAcknowledgmentUpdater(viewer, request.policy);
}
export {
userSubscriptionUpdateResponder,
passwordUpdateResponder,
sendVerificationEmailResponder,
sendPasswordResetEmailResponder,
logOutResponder,
accountDeletionResponder,
accountCreationResponder,
logInResponder,
siweAuthResponder,
oldPasswordUpdateResponder,
updateUserSettingsResponder,
policyAcknowledgmentResponder,
};
diff --git a/keyserver/src/session/cookies.js b/keyserver/src/session/cookies.js
index f64d58eea..2be9a0b64 100644
--- a/keyserver/src/session/cookies.js
+++ b/keyserver/src/session/cookies.js
@@ -1,841 +1,845 @@
// @flow
import crypto from 'crypto';
import type { $Response, $Request } from 'express';
import invariant from 'invariant';
import bcrypt from 'twin-bcrypt';
import url from 'url';
import { hasMinCodeVersion } from 'lib/shared/version-utils.js';
import type { Shape } from 'lib/types/core.js';
+import type { SignedIdentityKeysBlob } from 'lib/types/crypto-types.js';
import type { Platform, PlatformDetails } from 'lib/types/device-types.js';
import type { CalendarQuery } from 'lib/types/entry-types.js';
import {
type ServerSessionChange,
cookieLifetime,
cookieSources,
type CookieSource,
cookieTypes,
sessionIdentifierTypes,
type SessionIdentifierType,
} from 'lib/types/session-types.js';
import type { SIWESocialProof } from 'lib/types/siwe-types.js';
import type { InitialClientSocketMessage } from 'lib/types/socket-types.js';
import type { UserInfo } from 'lib/types/user-types.js';
import { values } from 'lib/utils/objects.js';
import { promiseAll } from 'lib/utils/promises.js';
import { Viewer } from './viewer.js';
import type { AnonymousViewerData, UserViewerData } from './viewer.js';
import createIDs from '../creators/id-creator.js';
import { createSession } from '../creators/session-creator.js';
import { dbQuery, SQL } from '../database/database.js';
import { deleteCookie } from '../deleters/cookie-deleters.js';
import { handleAsyncPromise } from '../responders/handlers.js';
import { clearDeviceToken } from '../updaters/device-token-updaters.js';
import { updateThreadMembers } from '../updaters/thread-updaters.js';
import { assertSecureRequest } from '../utils/security-utils.js';
import {
type AppURLFacts,
getAppURLFactsFromRequestURL,
} from '../utils/urls.js';
function cookieIsExpired(lastUsed: number) {
return lastUsed + cookieLifetime <= Date.now();
}
type SessionParameterInfo = {
isSocket: boolean,
sessionID: ?string,
sessionIdentifierType: SessionIdentifierType,
ipAddress: string,
userAgent: ?string,
};
type FetchViewerResult =
| { type: 'valid', viewer: Viewer }
| InvalidFetchViewerResult;
type InvalidFetchViewerResult =
| {
type: 'nonexistant',
cookieName: ?string,
cookieSource: ?CookieSource,
sessionParameterInfo: SessionParameterInfo,
}
| {
type: 'invalidated',
cookieName: string,
cookieID: string,
cookieSource: CookieSource,
sessionParameterInfo: SessionParameterInfo,
platformDetails: ?PlatformDetails,
deviceToken: ?string,
};
async function fetchUserViewer(
cookie: string,
cookieSource: CookieSource,
sessionParameterInfo: SessionParameterInfo,
): Promise<FetchViewerResult> {
const [cookieID, cookiePassword] = cookie.split(':');
if (!cookieID || !cookiePassword) {
return {
type: 'nonexistant',
cookieName: cookieTypes.USER,
cookieSource,
sessionParameterInfo,
};
}
const query = SQL`
SELECT hash, user, last_used, platform, device_token, versions
FROM cookies
WHERE id = ${cookieID} AND user IS NOT NULL
`;
const [[result], allSessionInfo] = await Promise.all([
dbQuery(query),
fetchSessionInfo(sessionParameterInfo, cookieID),
]);
if (result.length === 0) {
return {
type: 'nonexistant',
cookieName: cookieTypes.USER,
cookieSource,
sessionParameterInfo,
};
}
let sessionID = null,
sessionInfo = null;
if (allSessionInfo) {
({ sessionID, ...sessionInfo } = allSessionInfo);
}
const cookieRow = result[0];
let platformDetails = null;
if (cookieRow.versions) {
const versions = JSON.parse(cookieRow.versions);
platformDetails = {
platform: cookieRow.platform,
codeVersion: versions.codeVersion,
stateVersion: versions.stateVersion,
};
} else {
platformDetails = { platform: cookieRow.platform };
}
const deviceToken = cookieRow.device_token;
if (
!bcrypt.compareSync(cookiePassword, cookieRow.hash) ||
cookieIsExpired(cookieRow.last_used)
) {
return {
type: 'invalidated',
cookieName: cookieTypes.USER,
cookieID,
cookieSource,
sessionParameterInfo,
platformDetails,
deviceToken,
};
}
const userID = cookieRow.user.toString();
const viewer = new Viewer({
isSocket: sessionParameterInfo.isSocket,
loggedIn: true,
id: userID,
platformDetails,
deviceToken,
userID,
cookieSource,
cookieID,
cookiePassword,
sessionIdentifierType: sessionParameterInfo.sessionIdentifierType,
sessionID,
sessionInfo,
isScriptViewer: false,
ipAddress: sessionParameterInfo.ipAddress,
userAgent: sessionParameterInfo.userAgent,
});
return { type: 'valid', viewer };
}
async function fetchAnonymousViewer(
cookie: string,
cookieSource: CookieSource,
sessionParameterInfo: SessionParameterInfo,
): Promise<FetchViewerResult> {
const [cookieID, cookiePassword] = cookie.split(':');
if (!cookieID || !cookiePassword) {
return {
type: 'nonexistant',
cookieName: cookieTypes.ANONYMOUS,
cookieSource,
sessionParameterInfo,
};
}
const query = SQL`
SELECT last_used, hash, platform, device_token, versions
FROM cookies
WHERE id = ${cookieID} AND user IS NULL
`;
const [[result], allSessionInfo] = await Promise.all([
dbQuery(query),
fetchSessionInfo(sessionParameterInfo, cookieID),
]);
if (result.length === 0) {
return {
type: 'nonexistant',
cookieName: cookieTypes.ANONYMOUS,
cookieSource,
sessionParameterInfo,
};
}
let sessionID = null,
sessionInfo = null;
if (allSessionInfo) {
({ sessionID, ...sessionInfo } = allSessionInfo);
}
const cookieRow = result[0];
let platformDetails = null;
if (cookieRow.platform && cookieRow.versions) {
const versions = JSON.parse(cookieRow.versions);
platformDetails = {
platform: cookieRow.platform,
codeVersion: versions.codeVersion,
stateVersion: versions.stateVersion,
};
} else if (cookieRow.platform) {
platformDetails = { platform: cookieRow.platform };
}
const deviceToken = cookieRow.device_token;
if (
!bcrypt.compareSync(cookiePassword, cookieRow.hash) ||
cookieIsExpired(cookieRow.last_used)
) {
return {
type: 'invalidated',
cookieName: cookieTypes.ANONYMOUS,
cookieID,
cookieSource,
sessionParameterInfo,
platformDetails,
deviceToken,
};
}
const viewer = new Viewer({
isSocket: sessionParameterInfo.isSocket,
loggedIn: false,
id: cookieID,
platformDetails,
deviceToken,
cookieSource,
cookieID,
cookiePassword,
sessionIdentifierType: sessionParameterInfo.sessionIdentifierType,
sessionID,
sessionInfo,
isScriptViewer: false,
ipAddress: sessionParameterInfo.ipAddress,
userAgent: sessionParameterInfo.userAgent,
});
return { type: 'valid', viewer };
}
type SessionInfo = {
+sessionID: ?string,
+lastValidated: number,
+lastUpdate: number,
+calendarQuery: CalendarQuery,
};
async function fetchSessionInfo(
sessionParameterInfo: SessionParameterInfo,
cookieID: string,
): Promise<?SessionInfo> {
const { sessionID } = sessionParameterInfo;
const session = sessionID !== undefined ? sessionID : cookieID;
if (!session) {
return null;
}
const query = SQL`
SELECT query, last_validated, last_update
FROM sessions
WHERE id = ${session} AND cookie = ${cookieID}
`;
const [result] = await dbQuery(query);
if (result.length === 0) {
return null;
}
return {
sessionID,
lastValidated: result[0].last_validated,
lastUpdate: result[0].last_update,
calendarQuery: JSON.parse(result[0].query),
};
}
// This function is meant to consume a cookie that has already been processed.
// That means it doesn't have any logic to handle an invalid cookie, and it
// doesn't update the cookie's last_used timestamp.
async function fetchViewerFromCookieData(
req: $Request,
sessionParameterInfo: SessionParameterInfo,
): Promise<FetchViewerResult> {
let viewerResult;
const { user, anonymous } = req.cookies;
if (user) {
viewerResult = await fetchUserViewer(
user,
cookieSources.HEADER,
sessionParameterInfo,
);
} else if (anonymous) {
viewerResult = await fetchAnonymousViewer(
anonymous,
cookieSources.HEADER,
sessionParameterInfo,
);
} else {
return {
type: 'nonexistant',
cookieName: null,
cookieSource: null,
sessionParameterInfo,
};
}
// We protect against CSRF attacks by making sure that on web,
// non-GET requests cannot use a bare cookie for session identification
if (viewerResult.type === 'valid') {
const { viewer } = viewerResult;
invariant(
req.method === 'GET' ||
viewer.sessionIdentifierType !== sessionIdentifierTypes.COOKIE_ID ||
viewer.platform !== 'web',
'non-GET request from web using sessionIdentifierTypes.COOKIE_ID',
);
}
return viewerResult;
}
async function fetchViewerFromRequestBody(
body: mixed,
sessionParameterInfo: SessionParameterInfo,
): Promise<FetchViewerResult> {
if (!body || typeof body !== 'object') {
return {
type: 'nonexistant',
cookieName: null,
cookieSource: null,
sessionParameterInfo,
};
}
const cookiePair = body.cookie;
if (cookiePair === null || cookiePair === '') {
return {
type: 'nonexistant',
cookieName: null,
cookieSource: cookieSources.BODY,
sessionParameterInfo,
};
}
if (!cookiePair || typeof cookiePair !== 'string') {
return {
type: 'nonexistant',
cookieName: null,
cookieSource: null,
sessionParameterInfo,
};
}
const [type, cookie] = cookiePair.split('=');
if (type === cookieTypes.USER && cookie) {
return await fetchUserViewer(
cookie,
cookieSources.BODY,
sessionParameterInfo,
);
} else if (type === cookieTypes.ANONYMOUS && cookie) {
return await fetchAnonymousViewer(
cookie,
cookieSources.BODY,
sessionParameterInfo,
);
}
return {
type: 'nonexistant',
cookieName: null,
cookieSource: null,
sessionParameterInfo,
};
}
function getRequestIPAddress(req: $Request) {
const { proxy } = getAppURLFactsFromRequestURL(req.originalUrl);
let ipAddress;
if (proxy === 'none') {
ipAddress = req.socket.remoteAddress;
} else if (proxy === 'apache') {
ipAddress = req.get('X-Forwarded-For');
}
invariant(ipAddress, 'could not determine requesting IP address');
return ipAddress;
}
function getSessionParameterInfoFromRequestBody(
req: $Request,
): SessionParameterInfo {
const body = (req.body: any);
let sessionID =
body.sessionID !== undefined || req.method !== 'GET'
? body.sessionID
: null;
if (sessionID === '') {
sessionID = null;
}
const sessionIdentifierType =
req.method === 'GET' || sessionID !== undefined
? sessionIdentifierTypes.BODY_SESSION_ID
: sessionIdentifierTypes.COOKIE_ID;
return {
isSocket: false,
sessionID,
sessionIdentifierType,
ipAddress: getRequestIPAddress(req),
userAgent: req.get('User-Agent'),
};
}
async function fetchViewerForJSONRequest(req: $Request): Promise<Viewer> {
assertSecureRequest(req);
const sessionParameterInfo = getSessionParameterInfoFromRequestBody(req);
let result = await fetchViewerFromRequestBody(req.body, sessionParameterInfo);
if (
result.type === 'nonexistant' &&
(result.cookieSource === null || result.cookieSource === undefined)
) {
result = await fetchViewerFromCookieData(req, sessionParameterInfo);
}
return await handleFetchViewerResult(result);
}
const webPlatformDetails = { platform: 'web' };
async function fetchViewerForHomeRequest(req: $Request): Promise<Viewer> {
assertSecureRequest(req);
const sessionParameterInfo = getSessionParameterInfoFromRequestBody(req);
const result = await fetchViewerFromCookieData(req, sessionParameterInfo);
return await handleFetchViewerResult(result, webPlatformDetails);
}
async function fetchViewerForSocket(
req: $Request,
clientMessage: InitialClientSocketMessage,
): Promise<?Viewer> {
assertSecureRequest(req);
const { sessionIdentification } = clientMessage.payload;
const { sessionID } = sessionIdentification;
const sessionParameterInfo = {
isSocket: true,
sessionID,
sessionIdentifierType:
sessionID !== undefined
? sessionIdentifierTypes.BODY_SESSION_ID
: sessionIdentifierTypes.COOKIE_ID,
ipAddress: getRequestIPAddress(req),
userAgent: req.get('User-Agent'),
};
let result = await fetchViewerFromRequestBody(
clientMessage.payload.sessionIdentification,
sessionParameterInfo,
);
if (
result.type === 'nonexistant' &&
(result.cookieSource === null || result.cookieSource === undefined)
) {
result = await fetchViewerFromCookieData(req, sessionParameterInfo);
}
if (result.type === 'valid') {
return result.viewer;
}
const promises = {};
if (result.cookieSource === cookieSources.BODY) {
// We initialize a socket's Viewer after the WebSocket handshake, since to
// properly initialize the Viewer we need a bunch of data, but that data
// can't be sent until after the handshake. Consequently, by the time we
// know that a cookie may be invalid, we are no longer communicating via
// HTTP, and have no way to set a new cookie for HEADER (web) clients.
const platformDetails =
result.type === 'invalidated' ? result.platformDetails : null;
const deviceToken =
result.type === 'invalidated' ? result.deviceToken : null;
promises.anonymousViewerData = createNewAnonymousCookie({
platformDetails,
deviceToken,
});
}
if (result.type === 'invalidated') {
promises.deleteCookie = deleteCookie(result.cookieID);
}
const { anonymousViewerData } = await promiseAll(promises);
if (!anonymousViewerData) {
return null;
}
return createViewerForInvalidFetchViewerResult(result, anonymousViewerData);
}
async function handleFetchViewerResult(
result: FetchViewerResult,
inputPlatformDetails?: PlatformDetails,
) {
if (result.type === 'valid') {
return result.viewer;
}
let platformDetails = inputPlatformDetails;
if (!platformDetails && result.type === 'invalidated') {
platformDetails = result.platformDetails;
}
const deviceToken = result.type === 'invalidated' ? result.deviceToken : null;
const [anonymousViewerData] = await Promise.all([
createNewAnonymousCookie({ platformDetails, deviceToken }),
result.type === 'invalidated' ? deleteCookie(result.cookieID) : null,
]);
return createViewerForInvalidFetchViewerResult(result, anonymousViewerData);
}
function createViewerForInvalidFetchViewerResult(
result: InvalidFetchViewerResult,
anonymousViewerData: AnonymousViewerData,
): Viewer {
// If a null cookie was specified in the request body, result.cookieSource
// will still be BODY here. The only way it would be null or undefined here
// is if there was no cookie specified in either the body or the header, in
// which case we default to returning the new cookie in the response header.
const cookieSource =
result.cookieSource !== null && result.cookieSource !== undefined
? result.cookieSource
: cookieSources.HEADER;
const viewer = new Viewer({
...anonymousViewerData,
cookieSource,
sessionIdentifierType: result.sessionParameterInfo.sessionIdentifierType,
isSocket: result.sessionParameterInfo.isSocket,
ipAddress: result.sessionParameterInfo.ipAddress,
userAgent: result.sessionParameterInfo.userAgent,
});
viewer.sessionChanged = true;
// If cookieName is falsey, that tells us that there was no cookie specified
// in the request, which means we can't be invalidating anything.
if (result.cookieName) {
viewer.cookieInvalidated = true;
viewer.initialCookieName = result.cookieName;
}
return viewer;
}
function addSessionChangeInfoToResult(
viewer: Viewer,
res: $Response,
result: Object,
appURLFacts: AppURLFacts,
) {
let threadInfos = {},
userInfos = {};
if (result.cookieChange) {
({ threadInfos, userInfos } = result.cookieChange);
}
let sessionChange;
if (viewer.cookieInvalidated) {
sessionChange = ({
cookieInvalidated: true,
threadInfos,
userInfos: (values(userInfos).map(a => a): UserInfo[]),
currentUserInfo: {
id: viewer.cookieID,
anonymous: true,
},
}: ServerSessionChange);
} else {
sessionChange = ({
cookieInvalidated: false,
threadInfos,
userInfos: (values(userInfos).map(a => a): UserInfo[]),
}: ServerSessionChange);
}
if (viewer.cookieSource === cookieSources.BODY) {
sessionChange.cookie = viewer.cookiePairString;
} else {
addActualHTTPCookie(viewer, res, appURLFacts);
}
if (viewer.sessionIdentifierType === sessionIdentifierTypes.BODY_SESSION_ID) {
sessionChange.sessionID = viewer.sessionID ? viewer.sessionID : null;
}
result.cookieChange = sessionChange;
}
type AnonymousCookieCreationParams = Shape<{
+platformDetails: ?PlatformDetails,
+deviceToken: ?string,
}>;
const defaultPlatformDetails = {};
// The result of this function should not be passed directly to the Viewer
// constructor. Instead, it should be passed to viewer.setNewCookie. There are
// several fields on AnonymousViewerData that are not set by this function:
// sessionIdentifierType, cookieSource, ipAddress, and userAgent. These
// parameters all depend on the initial request. If the result of this function
// is passed to the Viewer constructor directly, the resultant Viewer object
// will throw whenever anybody attempts to access the relevant properties.
async function createNewAnonymousCookie(
params: AnonymousCookieCreationParams,
): Promise<AnonymousViewerData> {
const { platformDetails, deviceToken } = params;
const { platform, ...versions } = platformDetails || defaultPlatformDetails;
const versionsString =
Object.keys(versions).length > 0 ? JSON.stringify(versions) : null;
const time = Date.now();
const cookiePassword = crypto.randomBytes(32).toString('hex');
const cookieHash = bcrypt.hashSync(cookiePassword);
const [[id]] = await Promise.all([
createIDs('cookies', 1),
deviceToken ? clearDeviceToken(deviceToken) : undefined,
]);
const cookieRow = [
id,
cookieHash,
null,
platform,
time,
time,
deviceToken,
versionsString,
];
const query = SQL`
INSERT INTO cookies(id, hash, user, platform, creation_time, last_used,
device_token, versions)
VALUES ${[cookieRow]}
`;
await dbQuery(query);
return {
loggedIn: false,
id,
platformDetails,
deviceToken,
cookieID: id,
cookiePassword,
sessionID: undefined,
sessionInfo: null,
cookieInsertedThisRequest: true,
isScriptViewer: false,
};
}
type UserCookieCreationParams = {
platformDetails: PlatformDetails,
deviceToken?: ?string,
socialProof?: ?SIWESocialProof,
+ signedIdentityKeysBlob?: ?SignedIdentityKeysBlob,
};
// The result of this function should never be passed directly to the Viewer
// constructor. Instead, it should be passed to viewer.setNewCookie. There are
// several fields on UserViewerData that are not set by this function:
// sessionID, sessionIdentifierType, cookieSource, and ipAddress. These
// parameters all depend on the initial request. If the result of this function
// is passed to the Viewer constructor directly, the resultant Viewer object
// will throw whenever anybody attempts to access the relevant properties.
async function createNewUserCookie(
userID: string,
params: UserCookieCreationParams,
): Promise<UserViewerData> {
- const { platformDetails, deviceToken, socialProof } = params;
+ const { platformDetails, deviceToken, socialProof, signedIdentityKeysBlob } =
+ params;
const { platform, ...versions } = platformDetails || defaultPlatformDetails;
const versionsString =
Object.keys(versions).length > 0 ? JSON.stringify(versions) : null;
const time = Date.now();
const cookiePassword = crypto.randomBytes(32).toString('hex');
const cookieHash = bcrypt.hashSync(cookiePassword);
const [[cookieID]] = await Promise.all([
createIDs('cookies', 1),
deviceToken ? clearDeviceToken(deviceToken) : undefined,
]);
const cookieRow = [
cookieID,
cookieHash,
userID,
platform,
time,
time,
deviceToken,
versionsString,
JSON.stringify(socialProof),
+ signedIdentityKeysBlob ? JSON.stringify(signedIdentityKeysBlob) : null,
];
const query = SQL`
INSERT INTO cookies(id, hash, user, platform, creation_time, last_used,
- device_token, versions, social_proof)
+ device_token, versions, social_proof, signed_identity_keys)
VALUES ${[cookieRow]}
`;
await dbQuery(query);
return {
loggedIn: true,
id: userID,
platformDetails,
deviceToken,
userID,
cookieID,
sessionID: undefined,
sessionInfo: null,
cookiePassword,
cookieInsertedThisRequest: true,
isScriptViewer: false,
};
}
// This gets called after createNewUserCookie and from websiteResponder. If the
// Viewer's sessionIdentifierType is COOKIE_ID then the cookieID is used as the
// session identifier; otherwise, a new ID is created for the session.
async function setNewSession(
viewer: Viewer,
calendarQuery: CalendarQuery,
initialLastUpdate: number,
): Promise<void> {
if (viewer.sessionIdentifierType !== sessionIdentifierTypes.COOKIE_ID) {
const [sessionID] = await createIDs('sessions', 1);
viewer.setSessionID(sessionID);
}
await createSession(viewer, calendarQuery, initialLastUpdate);
}
async function extendCookieLifespan(cookieID: string) {
const time = Date.now();
const query = SQL`
UPDATE cookies SET last_used = ${time} WHERE id = ${cookieID}
`;
await dbQuery(query);
}
function addCookieToJSONResponse(
viewer: Viewer,
res: $Response,
result: Object,
expectCookieInvalidation: boolean,
appURLFacts: AppURLFacts,
) {
if (expectCookieInvalidation) {
viewer.cookieInvalidated = false;
}
if (!viewer.getData().cookieInsertedThisRequest) {
handleAsyncPromise(extendCookieLifespan(viewer.cookieID));
}
if (viewer.sessionChanged) {
addSessionChangeInfoToResult(viewer, res, result, appURLFacts);
} else if (viewer.cookieSource !== cookieSources.BODY) {
addActualHTTPCookie(viewer, res, appURLFacts);
}
}
function addCookieToHomeResponse(
viewer: Viewer,
res: $Response,
appURLFacts: AppURLFacts,
) {
if (!viewer.getData().cookieInsertedThisRequest) {
handleAsyncPromise(extendCookieLifespan(viewer.cookieID));
}
addActualHTTPCookie(viewer, res, appURLFacts);
}
function getCookieOptions(appURLFacts: AppURLFacts) {
const { baseDomain, basePath, https } = appURLFacts;
const domainAsURL = new url.URL(baseDomain);
return {
domain: domainAsURL.hostname,
path: basePath,
httpOnly: true,
secure: https,
maxAge: cookieLifetime,
sameSite: 'Strict',
};
}
function addActualHTTPCookie(
viewer: Viewer,
res: $Response,
appURLFacts: AppURLFacts,
) {
res.cookie(
viewer.cookieName,
viewer.cookieString,
getCookieOptions(appURLFacts),
);
if (viewer.cookieName !== viewer.initialCookieName) {
res.clearCookie(viewer.initialCookieName, getCookieOptions(appURLFacts));
}
}
async function setCookiePlatform(
viewer: Viewer,
platform: Platform,
): Promise<void> {
const newPlatformDetails = { ...viewer.platformDetails, platform };
viewer.setPlatformDetails(newPlatformDetails);
const query = SQL`
UPDATE cookies
SET platform = ${platform}
WHERE id = ${viewer.cookieID}
`;
await dbQuery(query);
}
async function setCookiePlatformDetails(
viewer: Viewer,
platformDetails: PlatformDetails,
): Promise<void> {
if (
hasMinCodeVersion(platformDetails, 70) &&
!hasMinCodeVersion(viewer.platformDetails, 70)
) {
await updateThreadMembers(viewer);
}
viewer.setPlatformDetails(platformDetails);
const { platform, ...versions } = platformDetails;
const versionsString =
Object.keys(versions).length > 0 ? JSON.stringify(versions) : null;
const query = SQL`
UPDATE cookies
SET platform = ${platform}, versions = ${versionsString}
WHERE id = ${viewer.cookieID}
`;
await dbQuery(query);
}
export {
fetchViewerForJSONRequest,
fetchViewerForHomeRequest,
fetchViewerForSocket,
createNewAnonymousCookie,
createNewUserCookie,
setNewSession,
extendCookieLifespan,
addCookieToJSONResponse,
addCookieToHomeResponse,
setCookiePlatform,
setCookiePlatformDetails,
};

File Metadata

Mime Type
text/x-diff
Expires
Mon, Dec 23, 3:44 AM (16 h, 8 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2690305
Default Alt Text
(43 KB)

Event Timeline