Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3509347
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
43 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment