diff --git a/keyserver/addons/opaque-ke-node/src/lib.rs b/keyserver/addons/opaque-ke-node/src/lib.rs
--- a/keyserver/addons/opaque-ke-node/src/lib.rs
+++ b/keyserver/addons/opaque-ke-node/src/lib.rs
@@ -8,9 +8,10 @@
 use opaque_ke::hash::Hash;
 use opaque_ke::slow_hash::SlowHash;
 use opaque_ke::{
-  ClientLogin, ClientLoginStartParameters, ClientRegistration,
-  ClientRegistrationFinishParameters, CredentialRequest, RegistrationRequest,
-  RegistrationResponse, RegistrationUpload,
+  ClientLogin, ClientLoginFinishParameters, ClientLoginStartParameters,
+  ClientRegistration, ClientRegistrationFinishParameters,
+  CredentialFinalization, CredentialRequest, CredentialResponse,
+  RegistrationRequest, RegistrationResponse, RegistrationUpload,
 };
 use rand::rngs::OsRng;
 
@@ -58,6 +59,13 @@
 
 impl Finalize for ClientLoginStartResult {}
 
+struct ClientLoginFinishResult {
+  message: CredentialFinalization<Cipher>,
+  session_key: Vec<u8>,
+}
+
+impl Finalize for ClientLoginFinishResult {}
+
 fn client_register_start(
   mut cx: FunctionContext,
 ) -> JsResult<JsBox<ClientRegistrationStartResult>> {
@@ -176,6 +184,47 @@
   Ok(JsArrayBuffer::external(&mut cx, login_start_message_vec))
 }
 
+fn client_login_finish(
+  mut cx: FunctionContext,
+) -> JsResult<JsBox<ClientLoginFinishResult>> {
+  let client_login_state = cx.argument::<JsTypedArray<u8>>(0)?;
+  let server_message = cx.argument::<JsTypedArray<u8>>(1)?;
+  let client_login =
+    ClientLogin::<Cipher>::deserialize(client_login_state.as_slice(&cx))
+      .or_else(|err| cx.throw_error(err.to_string()))?;
+  let credential_response =
+    CredentialResponse::<Cipher>::deserialize(server_message.as_slice(&cx))
+      .or_else(|err| cx.throw_error(err.to_string()))?;
+  let client_login_finish = client_login
+    .finish(credential_response, ClientLoginFinishParameters::default())
+    .or_else(|err| cx.throw_error(err.to_string()))?;
+  Ok(cx.boxed(ClientLoginFinishResult {
+    message: client_login_finish.message,
+    session_key: client_login_finish.session_key,
+  }))
+}
+
+fn get_login_finish_message_array(
+  mut cx: FunctionContext,
+) -> JsResult<JsArrayBuffer> {
+  let client_login_finish_result =
+    cx.argument::<JsBox<ClientLoginFinishResult>>(0)?;
+  let login_finish_message_vec = client_login_finish_result
+    .message
+    .serialize()
+    .or_else(|err| cx.throw_error(err.to_string()))?;
+  Ok(JsArrayBuffer::external(&mut cx, login_finish_message_vec))
+}
+
+fn get_login_finish_session_array(
+  mut cx: FunctionContext,
+) -> JsResult<JsArrayBuffer> {
+  let client_login_finish_result =
+    cx.argument::<JsBox<ClientLoginFinishResult>>(0)?;
+  let login_finish_session_vec = client_login_finish_result.session_key.clone();
+  Ok(JsArrayBuffer::external(&mut cx, login_finish_session_vec))
+}
+
 #[neon::main]
 fn main(mut cx: ModuleContext) -> NeonResult<()> {
   cx.export_function("clientRegisterStart", client_register_start)?;
@@ -198,5 +247,14 @@
     get_login_start_message_array,
   )?;
   cx.export_function("getLoginStartStateArray", get_login_start_state_array)?;
+  cx.export_function("clientLoginFinish", client_login_finish)?;
+  cx.export_function(
+    "getLoginFinishMessageArray",
+    get_login_finish_message_array,
+  )?;
+  cx.export_function(
+    "getLoginFinishSessionArray",
+    get_login_finish_session_array,
+  )?;
   Ok(())
 }