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,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 = {
@@ -83,3 +95,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,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,18 +1,31 @@
 // @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,
   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 {
@@ -124,6 +137,101 @@
 
     return assertWithValidator(keyserverKeys, keyserverKeysValidator);
   };
+
+  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(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 };