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,7 @@
 // @flow
 
 import invariant from 'invariant';
+import { SiweMessage } from 'siwe';
 import t from 'tcomb';
 import bcrypt from 'twin-bcrypt';
 
@@ -24,6 +25,7 @@
   logInActionSources,
 } from 'lib/types/account-types';
 import { defaultNumberPerThread } from 'lib/types/message-types';
+import type { SIWEAuthRequest, SIWEMessage } from 'lib/types/siwe-types.js';
 import type {
   SubscriptionUpdateRequest,
   SubscriptionUpdateResponse,
@@ -32,6 +34,7 @@
 import { ServerError } from 'lib/utils/errors';
 import { values } from 'lib/utils/objects';
 import { promiseAll } from 'lib/utils/promises';
+import { isValidSIWEMessage } from 'lib/utils/siwe-utils.js';
 import {
   tShape,
   tPlatformDetails,
@@ -304,6 +307,14 @@
 
 async function siweAuthResponder(viewer: Viewer, input: any): Promise<boolean> {
   await validateInput(viewer, siweAuthRequestInputValidator, input);
+  const request: SIWEAuthRequest = input;
+  const { message } = request;
+
+  // 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');
+  }
 
   return false;
 }
diff --git a/landing/siwe.react.js b/landing/siwe.react.js
--- a/landing/siwe.react.js
+++ b/landing/siwe.react.js
@@ -23,6 +23,7 @@
 import { publicProvider } from 'wagmi/providers/public';
 
 import type { SIWEWebViewMessage } from 'lib/types/siwe-types';
+import { siweStatement } from 'lib/utils/siwe-utils.js';
 
 import { SIWENonceContext } from './siwe-nonce-context.js';
 import css from './siwe.css';
@@ -66,11 +67,7 @@
 
 async function signInWithEthereum(address: string, signer, nonce: string) {
   invariant(nonce, 'nonce must be present in signInWithEthereum');
-  const message = createSiweMessage(
-    address,
-    'By continuing, I accept the Comm Terms of Service: https://comm.app/terms',
-    nonce,
-  );
+  const message = createSiweMessage(address, siweStatement, nonce);
   const signature = await signer.signMessage(message);
   postMessageToNativeWebView({
     type: 'siwe_success',
diff --git a/lib/utils/siwe-utils.js b/lib/utils/siwe-utils.js
--- a/lib/utils/siwe-utils.js
+++ b/lib/utils/siwe-utils.js
@@ -1,5 +1,8 @@
 // @flow
 
+import type { SIWEMessage } from '../types/siwe-types.js';
+import { isDev } from './dev-utils.js';
+
 const siweNonceRegex: RegExp = /^[a-zA-Z0-9]{17}$/;
 function isValidSIWENonce(candidate: string): boolean {
   return siweNonceRegex.test(candidate);
@@ -10,4 +13,28 @@
   return ethereumAddressRegex.test(candidate);
 }
 
-export { isValidSIWENonce, isValidEthereumAddress };
+const siweStatement: string =
+  'By continuing, I accept the Comm Terms of Service: https://comm.app/terms';
+
+const expectedDomain = isDev ? 'localhost:3000' : 'comm.app';
+const expectedURI = isDev ? 'http://localhost:3000' : 'https://comm.app';
+
+// Verify that the SIWEMessage is a well formed Comm SIWE Auth message.
+function isValidSIWEMessage(candidate: SIWEMessage): boolean {
+  return (
+    candidate.statement === siweStatement &&
+    candidate.version === '1' &&
+    candidate.chainId === 1 &&
+    candidate.domain === expectedDomain &&
+    candidate.uri === expectedURI &&
+    isValidSIWENonce(candidate.nonce) &&
+    isValidEthereumAddress(candidate.address)
+  );
+}
+
+export {
+  siweStatement,
+  isValidSIWENonce,
+  isValidEthereumAddress,
+  isValidSIWEMessage,
+};