diff --git a/lib/types/identity-service-types.js b/lib/types/identity-service-types.js
--- a/lib/types/identity-service-types.js
+++ b/lib/types/identity-service-types.js
@@ -54,6 +54,10 @@
     username: string,
     password: string,
   ) => Promise<IdentityAuthResult>;
+  +logInPasswordUser: (
+    username: string,
+    password: string,
+  ) => Promise<IdentityAuthResult>;
 }
 
 export type IdentityServiceAuthLayer = {
@@ -94,3 +98,12 @@
 };
 
 export const ONE_TIME_KEYS_NUMBER = 10;
+
+export const identityDeviceTypes = Object.freeze({
+  KEYSERVER: 0,
+  WEB: 1,
+  IOS: 2,
+  ANDROID: 3,
+  WINDOWS: 4,
+  MAC_OS: 5,
+});
diff --git a/native/identity-service/identity-service-context-provider.react.js b/native/identity-service/identity-service-context-provider.react.js
--- a/native/identity-service/identity-service-context-provider.react.js
+++ b/native/identity-service/identity-service-context-provider.react.js
@@ -134,6 +134,34 @@
         const { userID, accessToken } = JSON.parse(registrationResult);
         return { accessToken, userID, username };
       },
+      logInPasswordUser: async (username: string, password: string) => {
+        await commCoreModule.initializeCryptoAccount();
+        const [
+          { blobPayload, signature },
+          notificationsOneTimeKeys,
+          primaryOneTimeKeys,
+          prekeys,
+        ] = await Promise.all([
+          commCoreModule.getUserPublicKey(),
+          commCoreModule.getNotificationsOneTimeKeys(ONE_TIME_KEYS_NUMBER),
+          commCoreModule.getPrimaryOneTimeKeys(ONE_TIME_KEYS_NUMBER),
+          commCoreModule.generateAndGetPrekeys(),
+        ]);
+        const loginResult = await commRustModule.logInPasswordUser(
+          username,
+          password,
+          blobPayload,
+          signature,
+          prekeys.contentPrekey,
+          prekeys.contentPrekeySignature,
+          prekeys.notifPrekey,
+          prekeys.notifPrekeySignature,
+          getOneTimeKeyArray(primaryOneTimeKeys),
+          getOneTimeKeyArray(notificationsOneTimeKeys),
+        );
+        const { userID, accessToken } = JSON.parse(loginResult);
+        return { accessToken, userID, username };
+      },
     }),
     [getAuthMetadata],
   );
diff --git a/web/grpc/identity-service-client-wrapper.js b/web/grpc/identity-service-client-wrapper.js
--- a/web/grpc/identity-service-client-wrapper.js
+++ b/web/grpc/identity-service-client-wrapper.js
@@ -1,30 +1,49 @@
 // @flow
 
+import { Login } from '@commapp/opaque-ke-wasm';
+
 import identityServiceConfig from 'lib/facts/identity-service.js';
 import {
   type IdentityServiceAuthLayer,
   type IdentityServiceClient,
   type KeyserverKeys,
+  type IdentityAuthResult,
+  type IdentityDeviceKeyUpload,
   keyserverKeysValidator,
+  identityDeviceTypes,
 } from 'lib/types/identity-service-types.js';
+import { getMessageForException } from 'lib/utils/errors.js';
 import { assertWithValidator } from 'lib/utils/validation-utils.js';
 
 import { VersionInterceptor, AuthInterceptor } from './interceptor.js';
+import { initOpaque } from '../crypto/opaque-utils.js';
 import * as IdentityAuthClient from '../protobufs/identity-auth-client.cjs';
 import * as IdentityAuthStructs from '../protobufs/identity-auth-structs.cjs';
-import { Empty } from '../protobufs/identity-unauth-structs.cjs';
+import {
+  DeviceKeyUpload,
+  Empty,
+  IdentityKeyInfo,
+  OpaqueLoginFinishRequest,
+  OpaqueLoginStartRequest,
+  Prekey,
+} from '../protobufs/identity-unauth-structs.cjs';
 import * as IdentityUnauthClient from '../protobufs/identity-unauth.cjs';
 
 class IdentityServiceClientWrapper implements IdentityServiceClient {
   authClient: ?IdentityAuthClient.IdentityClientServicePromiseClient;
   unauthClient: IdentityUnauthClient.IdentityClientServicePromiseClient;
+  getDeviceKeyUpload: () => Promise<IdentityDeviceKeyUpload>;
 
-  constructor(authLayer: ?IdentityServiceAuthLayer) {
+  constructor(
+    authLayer: ?IdentityServiceAuthLayer,
+    getDeviceKeyUpload: () => Promise<IdentityDeviceKeyUpload>,
+  ) {
     if (authLayer) {
       this.authClient =
         IdentityServiceClientWrapper.createAuthClient(authLayer);
     }
     this.unauthClient = IdentityServiceClientWrapper.createUnauthClient();
+    this.getDeviceKeyUpload = getDeviceKeyUpload;
   }
 
   static determineSocketAddr(): string {
@@ -124,6 +143,104 @@
 
     return assertWithValidator(keyserverKeys, keyserverKeysValidator);
   };
+
+  logInPasswordUser: (
+    username: string,
+    password: string,
+  ) => Promise<IdentityAuthResult> = async (
+    username: string,
+    password: string,
+  ) => {
+    const client = this.unauthClient;
+    if (!client) {
+      throw new Error('Identity service client is not initialized');
+    }
+
+    const [identityDeviceKeyUpload] = await Promise.all([
+      this.getDeviceKeyUpload(),
+      initOpaque(),
+    ]);
+
+    const {
+      keyPayload,
+      keyPayloadSignature,
+      contentPrekey,
+      contentPrekeySignature,
+      notifPrekey,
+      notifPrekeySignature,
+      contentOneTimeKeys,
+      notifOneTimeKeys,
+    } = identityDeviceKeyUpload;
+
+    const contentOneTimeKeysArray = Array.from(contentOneTimeKeys);
+    const notifOneTimeKeysArray = Array.from(notifOneTimeKeys);
+
+    const opaqueLogin = new Login();
+    const startRequestBytes = opaqueLogin.start(password);
+
+    const identityKeyInfo = new IdentityKeyInfo();
+    identityKeyInfo.setPayload(keyPayload);
+    identityKeyInfo.setPayloadSignature(keyPayloadSignature);
+
+    const contentPrekeyUpload = new Prekey();
+    contentPrekeyUpload.setPrekey(contentPrekey);
+    contentPrekeyUpload.setPrekeySignature(contentPrekeySignature);
+
+    const notifPrekeyUpload = new Prekey();
+    notifPrekeyUpload.setPrekey(notifPrekey);
+    notifPrekeyUpload.setPrekeySignature(notifPrekeySignature);
+
+    const deviceKeyUpload = new DeviceKeyUpload();
+    deviceKeyUpload.setDeviceKeyInfo(identityKeyInfo);
+    deviceKeyUpload.setContentUpload(contentPrekeyUpload);
+    deviceKeyUpload.setNotifUpload(notifPrekeyUpload);
+    deviceKeyUpload.setOneTimeContentPrekeysList(contentOneTimeKeysArray);
+    deviceKeyUpload.setOneTimeNotifPrekeysList(notifOneTimeKeysArray);
+    deviceKeyUpload.setDeviceType(identityDeviceTypes.WEB);
+
+    const loginStartRequest = new OpaqueLoginStartRequest();
+    loginStartRequest.setUsername(username);
+    loginStartRequest.setOpaqueLoginRequest(startRequestBytes);
+    loginStartRequest.setDeviceKeyUpload(deviceKeyUpload);
+
+    let loginStartResponse;
+    try {
+      loginStartResponse =
+        await client.logInPasswordUserStart(loginStartRequest);
+    } catch (e) {
+      console.log('Error calling logInPasswordUserStart:', e);
+      throw new Error(
+        `logInPasswordUserStart RPC failed: ${
+          getMessageForException(e) ?? 'unknown'
+        }`,
+      );
+    }
+    const finishRequestBytes = opaqueLogin.finish(
+      loginStartResponse.getOpaqueLoginResponse_asU8(),
+    );
+
+    const loginFinishRequest = new OpaqueLoginFinishRequest();
+    loginFinishRequest.setSessionId(loginStartResponse.getSessionId());
+    loginFinishRequest.setOpaqueLoginUpload(finishRequestBytes);
+
+    let loginFinishResponse;
+    try {
+      loginFinishResponse =
+        await client.logInPasswordUserFinish(loginFinishRequest);
+    } catch (e) {
+      console.log('Error calling logInPasswordUserFinish:', e);
+      throw new Error(
+        `logInPasswordUserFinish RPC failed: ${
+          getMessageForException(e) ?? 'unknown'
+        }`,
+      );
+    }
+
+    const userID = loginFinishResponse.getUserId();
+    const accessToken = loginFinishResponse.getAccessToken();
+
+    return { userID, accessToken, username };
+  };
 }
 
 export { IdentityServiceClientWrapper };
diff --git a/web/grpc/identity-service-context-provider.react.js b/web/grpc/identity-service-context-provider.react.js
--- a/web/grpc/identity-service-context-provider.react.js
+++ b/web/grpc/identity-service-context-provider.react.js
@@ -5,6 +5,7 @@
 import { IdentityClientContext } from 'lib/shared/identity-client-context.js';
 
 import { IdentityServiceClientWrapper } from './identity-service-client-wrapper.js';
+import { useGetDeviceKeyUpload } from '../account/account-hooks.js';
 import { useSelector } from '../redux/redux-utils.js';
 
 type Props = {
@@ -18,6 +19,7 @@
   const deviceID = useSelector(
     state => state.cryptoStore?.primaryIdentityKeys.ed25519,
   );
+  const getDeviceKeyUpload = useGetDeviceKeyUpload();
 
   const client = React.useMemo(() => {
     let authLayer = null;
@@ -28,8 +30,8 @@
         commServicesAccessToken: accessToken,
       };
     }
-    return new IdentityServiceClientWrapper(authLayer);
-  }, [accessToken, deviceID, userID]);
+    return new IdentityServiceClientWrapper(authLayer, getDeviceKeyUpload);
+  }, [accessToken, deviceID, getDeviceKeyUpload, userID]);
 
   const value = React.useMemo(
     () => ({