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,13 +1,25 @@
 // @flow
 
+import { Login } from '@commapp/opaque-ke-wasm';
+
 import identityServiceConfig from 'lib/facts/identity-service.js';
 import type { IdentityServiceAuthLayer } from 'lib/types/identity-service-types.js';
 
 import { VersionInterceptor, AuthInterceptor } from './interceptor.js';
+import { initOpaque } from '../crypto/opaque-utils.js';
 import * as IdentityAuthClient from '../protobufs/identity-auth-client.cjs';
-import { Empty } from '../protobufs/identity-unauth-structs.cjs';
+import {
+  DeviceKeyUpload,
+  Empty,
+  IdentityKeyInfo,
+  OpaqueLoginFinishRequest,
+  OpaqueLoginStartRequest,
+  Prekey,
+} from '../protobufs/identity-unauth-structs.cjs';
 import * as IdentityClient from '../protobufs/identity-unauth.cjs';
 
+const webDeviceType = Object.freeze(1);
+
 class IdentityServiceClientWrapper {
   authClient: ?IdentityAuthClient.IdentityClientServicePromiseClient;
   unauthorizedClient: ?IdentityClient.IdentityClientServicePromiseClient;
@@ -21,7 +33,9 @@
     return process.env.IDENTITY_SOCKET_ADDR ?? identityServiceConfig.defaultURL;
   }
 
-  async initAuthClient(authLayer: IdentityServiceAuthLayer): Promise<void> {
+  initAuthClient(
+    authLayer: IdentityServiceAuthLayer,
+  ): IdentityAuthClient.IdentityClientServicePromiseClient {
     const { userID, deviceID, commServicesAccessToken } = authLayer;
 
     const identitySocketAddr = this.determineSocketAddr();
@@ -42,9 +56,11 @@
       null,
       authClientOpts,
     );
+
+    return this.authClient;
   }
 
-  async initUnauthorizedClient(): Promise<void> {
+  initUnauthorizedClient(): IdentityClient.IdentityClientServicePromiseClient {
     const identitySocketAddr = this.determineSocketAddr();
 
     const versionInterceptor = new VersionInterceptor<Request, Response>();
@@ -59,6 +75,8 @@
         null,
         unauthorizedClientOpts,
       );
+
+    return this.unauthorizedClient;
   }
 
   deleteUser: (
@@ -70,20 +88,100 @@
     deviceID: string,
     accessToken: string,
   ): Promise<void> => {
-    if (!this.authClient) {
-      const authLayer: IdentityServiceAuthLayer = {
+    const authClient =
+      this.authClient ||
+      this.initAuthClient({
         userID,
         deviceID,
         commServicesAccessToken: accessToken,
-      };
-      await this.initAuthClient(authLayer);
+      });
+
+    await 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<string> = async (
+    username: string,
+    password: string,
+    keyPayload: string,
+    keyPayloadSignature: string,
+    contentPrekey: string,
+    contentPrekeySignature: string,
+    notifPrekey: string,
+    notifPrekeySignature: string,
+    contentOneTimeKeys: Array<string>,
+    notifOneTimeKeys: Array<string>,
+  ): Promise<string> => {
+    const client = this.unauthorizedClient || this.initUnauthorizedClient();
+
+    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(webDeviceType);
+
+    const loginStartRequest = new OpaqueLoginStartRequest();
+    loginStartRequest.setUsername(username);
+    loginStartRequest.setOpaqueLoginRequest(startRequestBytes);
+    loginStartRequest.setDeviceKeyUpload(deviceKeyUpload);
+
+    let loginStartResponse;
+    try {
+      loginStartResponse =
+        await client.logInPasswordUserStart(loginStartRequest);
+    } catch (startError) {
+      startError.method = 'loginPasswordUserStart';
+      throw startError;
     }
+    const finishRequestBytes = opaqueLogin.finish(
+      loginStartResponse.getOpaqueLoginResponse_asU8(),
+    );
 
-    if (this.authClient) {
-      await this.authClient.deleteUser(new Empty());
-    } else {
-      throw new Error('Identity service client is not initialized');
+    const loginFinishRequest = new OpaqueLoginFinishRequest();
+    loginFinishRequest.setSessionId(loginStartResponse.getSessionId());
+    loginFinishRequest.setOpaqueLoginUpload(finishRequestBytes);
+
+    let loginFinishResponse;
+    try {
+      loginFinishResponse =
+        await client.logInPasswordUserFinish(loginFinishRequest);
+    } catch (finishError) {
+      finishError.method = 'loginPasswordUserFinish';
+      throw finishError;
     }
+
+    const userID = loginFinishResponse.getUserId();
+    const accessToken = loginFinishResponse.getAccessToken();
+
+    return JSON.stringify({ userID, accessToken });
   };
 }