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
@@ -26,6 +26,18 @@
     username: string,
     password: string,
   ) => Promise<IdentityAuthResult>;
+  +logInPasswordUser: (
+    username: string,
+    password: string,
+    keyPayload: string,
+    keyPayloadSignature: string,
+    contentPrekey: string,
+    contentPrekeySignature: string,
+    notifPrekey: string,
+    notifPrekeySignature: string,
+    contentOneTimeKeys: Array<string>,
+    notifOneTimeKeys: Array<string>,
+  ) => Promise<IdentityAuthResult>;
 }
 
 export type IdentityServiceAuthLayer = {
@@ -55,3 +67,12 @@
 };
 
 export const ONE_TIME_KEYS_NUMBER = 10;
+
+export const DeviceType = 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
@@ -117,6 +117,33 @@
         const { userID, accessToken } = JSON.parse(registrationResult);
         return { accessToken, userID, username };
       },
+      logInPasswordUser: async (
+        username: string,
+        password: string,
+        keyPayload: string,
+        keyPayloadSignature: string,
+        contentPrekey: string,
+        contentPrekeySignature: string,
+        notifPrekey: string,
+        notifPrekeySignature: string,
+        contentOneTimeKeys: Array<string>,
+        notifOneTimeKeys: Array<string>,
+      ) => {
+        const loginResult = await commRustModule.logInPasswordUser(
+          username,
+          password,
+          keyPayload,
+          keyPayloadSignature,
+          contentPrekey,
+          contentPrekeySignature,
+          notifPrekey,
+          notifPrekeySignature,
+          contentOneTimeKeys,
+          notifOneTimeKeys,
+        );
+        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,16 +1,29 @@
 // @flow
 
+import { Login } from '@commapp/opaque-ke-wasm';
+
 import identityServiceConfig from 'lib/facts/identity-service.js';
 import type {
   IdentityServiceAuthLayer,
   IdentityServiceClient,
   OutboundKeyInfoResponse,
+  IdentityAuthResult,
 } from 'lib/types/identity-service-types.js';
+import { DeviceType } from 'lib/types/identity-service-types.js';
+import { getMessageForException } from 'lib/utils/errors.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 {
@@ -79,6 +92,101 @@
     await this.authClient.deleteUser(new Empty());
   };
 
+  logInPasswordUser: (
+    username: string,
+    password: string,
+    keyPayload: string,
+    keyPayloadSignature: string,
+    contentPrekey: string,
+    contentPrekeySignature: string,
+    notifPrekey: string,
+    notifPrekeySignature: string,
+    contentOneTimeKeys: Array<string>,
+    notifOneTimeKeys: Array<string>,
+  ) => Promise<IdentityAuthResult> = async (
+    username: string,
+    password: string,
+    keyPayload: string,
+    keyPayloadSignature: string,
+    contentPrekey: string,
+    contentPrekeySignature: string,
+    notifPrekey: string,
+    notifPrekeySignature: string,
+    contentOneTimeKeys: Array<string>,
+    notifOneTimeKeys: Array<string>,
+  ) => {
+    const client = this.unauthClient;
+    if (!client) {
+      throw new Error('Identity service client is not initialized');
+    }
+    await initOpaque();
+    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(contentOneTimeKeys);
+    deviceKeyUpload.setOneTimeNotifPrekeysList(notifOneTimeKeys);
+    deviceKeyUpload.setDeviceType(DeviceType.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 };
+  };
+
   getKeyserverKeys: (keyserverID: string) => Promise<?OutboundKeyInfoResponse> =
     async (keyserverID: string) => {
       const client = this.authClient;