diff --git a/keyserver/src/creators/account-creator.js b/keyserver/src/creators/account-creator.js --- a/keyserver/src/creators/account-creator.js +++ b/keyserver/src/creators/account-creator.js @@ -94,9 +94,9 @@ ? request.deviceTokenUpdateRequest.deviceToken : viewer.deviceToken; const [id] = await createIDs('users', 1); - const newUserRow = [id, request.username, hash, time]; + const newUserRow = [id, request.username, hash, time, request.address]; const newUserQuery = SQL` - INSERT INTO users(id, username, hash, creation_time) + INSERT INTO users(id, username, hash, creation_time, ethereum_address) VALUES ${[newUserRow]} `; const [userViewerData] = await Promise.all([ diff --git a/keyserver/src/endpoints.js b/keyserver/src/endpoints.js --- a/keyserver/src/endpoints.js +++ b/keyserver/src/endpoints.js @@ -51,6 +51,7 @@ logInResponder, oldPasswordUpdateResponder, updateUserSettingsResponder, + siweResponder, } from './responders/user-responders'; import { codeVerificationResponder } from './responders/verification-responders'; import { uploadDeletionResponder } from './uploads/uploads'; @@ -84,6 +85,7 @@ send_password_reset_email: sendPasswordResetEmailResponder, send_verification_email: sendVerificationEmailResponder, set_thread_unread_status: threadSetUnreadStatusResponder, + siwe: siweResponder, update_account: passwordUpdateResponder, update_activity: updateActivityResponder, update_calendar_query: calendarQueryUpdateResponder, diff --git a/keyserver/src/responders/user-responders.js b/keyserver/src/responders/user-responders.js --- a/keyserver/src/responders/user-responders.js +++ b/keyserver/src/responders/user-responders.js @@ -1,6 +1,8 @@ // @flow import invariant from 'invariant'; +import { SiweMessage, ErrorTypes } from 'siwe'; +// import { generateNonce } from 'siwe'; import t from 'tcomb'; import bcrypt from 'twin-bcrypt'; @@ -13,6 +15,8 @@ RegisterRequest, LogInResponse, LogInRequest, + SIWERequest, + SIWEResponse, UpdatePasswordRequest, UpdateUserSettingsRequest, } from 'lib/types/account-types'; @@ -290,6 +294,130 @@ return response; } +// todo, see https://linear.app/comm/issue/ENG-2226/add-nonce-to-cookie-session +// async function siweNonceResponder(viewer: Viewer) { +// const nonce = generateNonce(); +// return nonce; +// } + +const siweRequestInputValidator = tShape({ + address: t.String, + signature: t.String, + message: t.String, + watchedIDs: t.list(t.String), + calendarQuery: t.maybe(entryQueryInputValidator), + deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), + platformDetails: tPlatformDetails, + source: t.maybe(t.enums.of(values(logInActionSources))), +}); + +async function siweResponder( + viewer: Viewer, + input: any, +): Promise { + await validateInput(viewer, siweRequestInputValidator, input); + const request: SIWERequest = input; + + const { address, message, signature } = request; + const calendarQuery = request.calendarQuery + ? normalizeCalendarQuery(request.calendarQuery) + : null; + if (!address) { + throw new ServerError('invalid_parameters'); + } + + try { + const siweMessage = new SiweMessage(message); + await siweMessage.validate(signature); + } catch (error) { + switch (error) { + case ErrorTypes.EXPIRED_MESSAGE: + throw new ServerError('expired_signature', { status: 440 }); + case ErrorTypes.INVALID_SIGNATURE: + throw new ServerError('invalid_signature', { status: 422 }); + default: + throw new ServerError('oops', { status: 500 }); + } + } + + const userQuery = SQL` + SELECT id, hash, username + FROM users + WHERE ethereum_address = ${address} + `; + const [userResult] = await dbQuery(userQuery); + if (userResult.length === 0) { + // breaking out variables for flow's sake + const { + message: noop, + signature: noop2, + watchedIDs: noop3, + ...rest + } = request; + return await createAccount(viewer, { + username: address, + password: signature, + ...rest, + }); + } + const userRow = userResult[0]; + const id = userRow.id.toString(); + + const newServerTime = Date.now(); + const deviceToken = request.deviceTokenUpdateRequest + ? request.deviceTokenUpdateRequest.deviceToken + : viewer.deviceToken; + const [userViewerData] = await Promise.all([ + createNewUserCookie(id, { + platformDetails: request.platformDetails, + deviceToken, + }), + deleteCookie(viewer.cookieID), + ]); + viewer.setNewCookie(userViewerData); + if (calendarQuery) { + await setNewSession(viewer, calendarQuery, newServerTime); + } + + const threadCursors = {}; + // $FlowFixMe undefined [1] is incompatible with `$Iterable` [2]. + 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) { + response.rawEntryInfos = rawEntryInfos; + } + return response; +} + const updatePasswordRequestInputValidator = tShape({ code: t.String, password: tPassword, @@ -339,4 +467,5 @@ logInResponder, oldPasswordUpdateResponder, updateUserSettingsResponder, + siweResponder, }; diff --git a/lib/actions/user-actions.js b/lib/actions/user-actions.js --- a/lib/actions/user-actions.js +++ b/lib/actions/user-actions.js @@ -7,6 +7,8 @@ LogInResult, RegisterResult, RegisterInfo, + SIWEServerCall, + SIWEResult, UpdateUserSettingsRequest, } from '../types/account-types'; import type { GetSessionPublicKeysArgs } from '../types/request-types'; @@ -91,6 +93,33 @@ }; }; +const siweActionTypes = Object.freeze({ + started: 'SIWE_STARTED', + success: 'SIWE_SUCCESS', + failed: 'SIWE_FAILED', +}); +const siwe = ( + callServerEndpoint: CallServerEndpoint, +): ((siweInfo: SIWEServerCall) => Promise) => async siweInfo => { + const watchedIDs = threadWatcher.getWatchedIDs(); + const response = await callServerEndpoint( + 'siwe', + { + ...siweInfo, + watchedIDs, + platformDetails: getConfig().platformDetails, + }, + registerCallServerEndpointOptions, + ); + return { + currentUserInfo: response.currentUserInfo, + rawMessageInfos: response.rawMessageInfos, + threadInfos: response.cookieChange.threadInfos, + userInfos: response.cookieChange.userInfos, + calendarQuery: siweInfo.calendarQuery, + }; +}; + function mergeUserInfos(...userInfoArrays: UserInfo[][]): UserInfo[] { const merged = {}; for (const userInfoArray of userInfoArrays) { @@ -238,6 +267,8 @@ searchUsersActionTypes, setUserSettings, setUserSettingsActionTypes, + siwe, + siweActionTypes, updateSubscription, updateSubscriptionActionTypes, }; diff --git a/lib/types/account-types.js b/lib/types/account-types.js --- a/lib/types/account-types.js +++ b/lib/types/account-types.js @@ -50,6 +50,7 @@ +calendarQuery?: ?CalendarQuery, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, +platformDetails: PlatformDetails, + +address?: ?string, }; export type RegisterResponse = { @@ -140,6 +141,28 @@ +logInActionSource: LogInActionSource, }; +export type SIWERequest = { + +address: string, + +message: string, + +signature: string, + +calendarQuery?: ?CalendarQuery, + +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, + +platformDetails: PlatformDetails, + +watchedIDs?: ?$ReadOnlyArray, +}; + +export type SIWEServerCall = { + +address: string, + +message: string, + +signature: string, + ...LogInExtraInfo, +}; + +export type SIWEResponse = RegisterResponse | LogInResponse; + +export type SIWEResult = { + ...RegisterResult, +}; export type UpdatePasswordRequest = { code: string, password: string, diff --git a/lib/types/endpoints.js b/lib/types/endpoints.js --- a/lib/types/endpoints.js +++ b/lib/types/endpoints.js @@ -24,6 +24,7 @@ CREATE_ACCOUNT: 'create_account', LOG_IN: 'log_in', UPDATE_PASSWORD: 'update_password', + SIWE: 'siwe', }); type SessionChangingEndpoint = $Values; diff --git a/lib/types/redux-types.js b/lib/types/redux-types.js --- a/lib/types/redux-types.js +++ b/lib/types/redux-types.js @@ -7,6 +7,7 @@ LogInResult, RegisterResult, DefaultNotificationPayload, + SIWEResult, } from './account-types'; import type { ActivityUpdateSuccessPayload, @@ -818,6 +819,22 @@ +error: true, +payload: Error, +loadingInfo: LoadingInfo, + } + | { + +type: 'SIWE_STARTED', + +payload?: void, + +loadingInfo: LoadingInfo, + } + | { + +type: 'SIWE_SUCCESS', + +payload: SIWEResult, + +loadingInfo: LoadingInfo, + } + | { + +type: 'SIWE_FAILED', + +error: true, + +payload: Error, + +loadingInfo: LoadingInfo, }; export type ActionPayload = ?(Object | Array<*> | $ReadOnlyArray<*> | string); diff --git a/native/account/logged-out-modal.react.js b/native/account/logged-out-modal.react.js --- a/native/account/logged-out-modal.react.js +++ b/native/account/logged-out-modal.react.js @@ -320,12 +320,14 @@ const promptButtonsSize = Platform.OS === 'ios' ? 40 : 61; const logInContainerSize = 140; const registerPanelSize = Platform.OS === 'ios' ? 181 : 180; + const siwePanelSize = 250; const containerSize = add( headerHeight, cond(not(isPastPrompt(this.modeValue)), promptButtonsSize, 0), cond(eq(this.modeValue, modeNumbers['log-in']), logInContainerSize, 0), cond(eq(this.modeValue, modeNumbers['register']), registerPanelSize, 0), + cond(eq(this.modeValue, modeNumbers['siwe']), siwePanelSize, 0), ); const potentialPanelPaddingTop = divide( max(sub(this.contentHeight, this.keyboardHeightValue, containerSize), 0), diff --git a/native/account/siwe-panel.react.js b/native/account/siwe-panel.react.js --- a/native/account/siwe-panel.react.js +++ b/native/account/siwe-panel.react.js @@ -3,11 +3,11 @@ import Animated from 'react-native-reanimated'; import WebView from 'react-native-webview'; -import { registerActionTypes, register } from 'lib/actions/user-actions'; +import { siweActionTypes, siwe } from 'lib/actions/user-actions'; import type { - RegisterInfo, + SIWEServerCall, + SIWEResult, LogInExtraInfo, - RegisterResult, LogInStartingPayload, } from 'lib/types/account-types'; import { @@ -20,7 +20,6 @@ import { useSelector } from '../redux/redux-utils'; import { nativeLogInExtraInfoSelector } from '../selectors/account-selectors'; import { defaultLandingURLPrefix } from '../utils/url-utils'; -import { setNativeCredentials } from './native-credentials'; const commSIWE = `${defaultLandingURLPrefix}/siwe`; @@ -35,40 +34,40 @@ // Redux dispatch functions +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs - +registerAction: (registerInfo: RegisterInfo) => Promise, + +siweAction: (siweInfo: SIWEServerCall) => Promise, }; function SIWEPanel({ logInExtraInfo, dispatchActionPromise, - registerAction, + siweAction, }: Props) { const handleSIWE = React.useCallback( - ({ address, signature }) => { + ({ address, message, signature }) => { // this is all mocked from register-panel const extraInfo = logInExtraInfo(); dispatchActionPromise( - registerActionTypes, - registerAction({ - username: address, - password: signature, + siweActionTypes, + siweAction({ + address, + message, + signature, ...extraInfo, }), undefined, ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), ); - setNativeCredentials({ username: address, password: signature }); }, - [logInExtraInfo, dispatchActionPromise, registerAction], + [logInExtraInfo, dispatchActionPromise, siweAction], ); const handleMessage = React.useCallback( event => { const { nativeEvent: { data }, } = event; - const { address, signature } = JSON.parse(data); + const { address, message, signature } = JSON.parse(data); if (address && signature) { - handleSIWE({ address, signature }); + handleSIWE({ address, message, signature }); } }, [handleSIWE], @@ -87,14 +86,14 @@ ); const dispatchActionPromise = useDispatchActionPromise(); - const callRegister = useServerCall(register); + const callSiwe = useServerCall(siwe); return ( ); },