Repository: lldap/lldap (language: rust)

Issue:
[BUG] WebUI Case Sensitivity
**Describe the bug**
Cannot login using 31-len username + 63-len password. But OK if used 5-len username + 11-len password.
I cannot login using these credentials by WebUI (17170 port), but Authelia can successfully login using these creds (at the startup check).

Perhaps, there's some cuts in WebUI? But even if it's so, there's no hint message, like (username too long, or password too long).

**To Reproduce**
Steps to reproduce the behavior:
1. Use username `user1` and password `password123` - OK. Everything is fine. Can login using WebUI, can login using Authelia.
2. Use username `S46EAHa4OTDzPkLjGmOPZ6jIjVpL0Xi` and password `LnAzLNVn16mVASPu1u5HlTe6SOcj4bQ2aHJkE4E1PB15Ntupz7BU24ujARvtvBO` - Fail. **Cannot** login using WebUI, **can** login using Authelia.
3. Use username `S46EAHa4OTDzPkLjGmOPZ6jIjVpL0Xi` and password `password123` - Fail. **Cannot** login using WebUI, **can** login using Authelia.

**Expected behavior**
Any of:
- Message about limits during lldap startup (something like "Fatal, username/password too long").
- Successfully login using WebUI even when long username/password is used

**Logs**
Nothing special. According to my tests, providing correct or incorrect credentials leads to the same log message of `lldap`:
```
2024-01-15T12:40:39.243788611+00:00  DEBUG    HTTP request [ 2.77ms | 5.47% / 100.00% ] method: "POST" | uri: "/auth/opaque/login/start"
2024-01-15T12:40:39.243855252+00:00  DEBUG    ┝━ opaque_login_start [ 2.61ms | 2.39% / 94.53% ]
2024-01-15T12:40:39.243871172+00:00  DEBUG    │  ┕━ login_start [ 2.55ms | 77.98% / 92.14% ]
2024-01-15T12:40:39.243878932+00:00  DEBUG    │     ┕━ get_password_file_for_user [ 392µs | 14.16% ] user_id: UserId(CaseInsensitiveString("s46eaha4otdzpkljgmopz6jijvpl0xi"))
2024-01-15T12:40:39.247770526+00:00  DEBUG    ┕━ 🐛 [debug]:  | status_code: 200
```

**Additional context**
Maybe there's some limitations I don't know about? Are they mentioned somewhere?

---

_Provided 31-len username and 63-len password are just randomly generated strings and obviously used only for local tests. So, I don't care exposing them here._


Discussion / hints:
There are no meaningful restrictions on the user/password. The user ID should be <256 characters to fit in the DB, but that's not the issue here. The password is not actually sent to the server but a zero-knowledge proof is negotiated with the server to prove that you have the same password as when you registered. That proof uses the same code for the web login (where the client doesn't send the password) and for an LDAP login as with Authelia (where the client sends the password - the server acts as both parties to verify the proof).

Some things to test: Is it due to the password length or the username length? What if you try logging in with a long password and short username, or vice-versa?

As for the logs, logging in from the web UI takes 2 messages: You showed me the `opaque_login_start`, there should be an `opaque_login_finish` to match that, unless the password is wrong.

You say that logging in fails: what is the error? And at which step? Can you describe it more accurately?
Thank you for your fast response. 

Let me describe the setup more precisely. 
There's 3 services: PostgreSQL (for LLDAP and Authelia), LLDAP and Authelia. All envs that are used for LLDAP and Authelia are linked to each other. So, generally speaking, Authelia's LLDAP vars derive their values from the LLDAP ones. 

So, when I start the whole stack, both of LLDAP and Authelia runs fine. I mentioned it here, because Authelia has startup checks including check LLDAP conn with given credentials. And no matter what combination of described credentials here will I use, Authelia successfully passes that check. But, if I'd provide an explicitly incorrect password (not the same, that I give to LLDAP), it will fail during startup. This observations leads me to the following thought: 

**From the Authelia POV, any combination of login+pass I'm talking here about are correct and can be used to authenticate using LDAP protocol (3890 port).**

> Some things to test: Is it due to the password length or the username length? What if you try logging in with a long password and short username, or vice-versa?

In the first post I mentioned, that I also tried long username + short password and it leads to "Invalid username or password" error.

**I tried the combination of short username + long password right now and it works correctly. I can login using either WebUI or Authelia**.

So, I think the problem with the username's length.

> As for the logs, logging in from the web UI takes 2 messages: You showed me the opaque_login_start, there should be an opaque_login_finish to match that, unless the password is wrong.

I tested again 2 cases:
- Long username + short password
- Long username + long password

Both of them has following artefacts:
- "Invalid username or password" error in WebUI
- No error in logs
- Only `opaque_login_start` in logs and no presence of `opaque_login_finish`. 

FYI
I can not reproduce this problem, all the given user/password combinations work for me.

LLDAP: [0.5.0](https://github.com/lldap/lldap/tree/v0.5.0)
Browser: Firefox/Librewolf 121.0
Browser Addons: uBlock, LocalCDN
Ah, forgot to mention versions.

Docker image's version: `lldap:2024-01-08-alpine`
LLDAP version output: `lldap 0.5.1-alpha`
Browser: Chrome Version 120.0.6099.199 (Official Build) (arm64)

@Masgalor you cannot reproduce the problem using LLDAP's WebUI, right? Not the LDAP port.
Right, I'm talking about the WebUI.
And there are no JS errors in the dev console?
@nitnelave yes, I have JS error in dev console. Sorry for forgetting to check it and provide it alongside with the report.

```
Invalid username or password: Protocol error: `This error results from an error during password verification`
```

It refers to `lldap_app.js:597`. 
Do you have a reverse-proxy or a waf in front of lldap?
Maybe something is limiting your request size or changing your request in another way.
> Do you have a reverse-proxy or a waf in front of lldap?
Maybe something is limiting your request size or changing your request in another way.

For all these testings - nope. I use SSH tun to the remote machine directly to the LLDAP WebUI port. So, it's only SSH tun + Docker routes. 
SSH tun is opened by `ssh -L 17170:<lldap_ip>:17170 <server_ip> -Nv`
Could we get a screenshot of the network requests from the browser when trying to log in?
The JS error is expected (it means it couldn't verify the password, likely because the password was wrong).
> Could we get a screenshot of the network requests from the browser when trying to log in?
The JS error is expected (it means it couldn't verify the password, likely because the password was wrong).

Sure. I attached the screenshot. Also, looks like the main request is `start`, because there's no other one. So, here's the data of it:

<details><summary>JSON request</summary>
<pre>
{
  "username": "S46EAHa4OTDzPkLjGmOPZ6jIjVpL0Xi",
  "login_start_request": "xlIiZqCbLsbIN2o9++czjj/b2B1PvS0jWyEgj+9IhzjHtabC5+Ii5AxpMlRv8d7k0TQ6r2+troAoZWkbSi/mDcQ/h2qdIO2tMky6OgmdI+nliHSJxwVBxN3AQpmTn9US"
}
</pre>
</details> 

<details><summary>JSON response</summary>
<pre>
{
    "server_data": "/8DATnRwTnY21tUGdO/PcH0DBaeZ0oc0QAplrl9rm5aAQoVP2e87oYwlrDAmNle3k1zrwXU0pnmK4aNR+3yQu3POKCjLrFiZ618EFJcR9I6mHCHta2kz1pEISjLKUAJSTOC0CpesDlBJwAKIpSbcXvf9LnEDutqAL9K7qAkgE8sDMjXntVN6Pyn8VqX+K3i6+qqJaCB6H2F4velE/l4lp8bgwMUSR2gbYuhXVjqQvk0ESriKnLVo9HNjEfJMMCK2f+A/TxMOrej0acxosw32aGOQhqbqUj85F+OHjfTgBv+ux2wQgNHom9fALx6QHULAXMI03f9cVI8SIyaGWuBGCIKMUOjUo+lM1Zu6BHE8zB4pJSYVMDlC",
    "credential_response": "AipMrM/IiBKHqZIFiBUMeyFfRoHszs8VfV0BbN91Y2qwy5dXjSpoMdcoRR17SGp7urIP+SWM6ZIh3ZKeamh9LWEprGiDm4DdmxASAgfAk6s94J1Y8JaSdO+GeJVxS39wXxiVdXmoK9Q42hipZKJ+KgE3mb5e7I43rclN+wzAaJSxBHPyKthEKa8MmiQp/FZ05IUYlxkJDrnie7upfOVRPkbRgrZgBKDIyfUHJnr9mD4qu/RSSlfSDGo8WBKu/cAfgB+uDz15oUkz8uJYEAw8I2xZSDuQ8v2nVTU75dtdMKqK+1UEzIdyJ9Z2dnGA8jN1Zmm7T3DYVr/ZopIWQeK6MJejVEUrHiKKmZz1mi1/rBMg0W4wC8+Kgai9WdhaIVsr9EX0voFF0Tuf+ypwxvt1DiKJ6+cdV7bwqq8X3G0ZMcE="
}
</pre>
</details> 

Moreover, if it may help you to investigate I may provide all ENVs using which LLDAP is started.

<details><summary>ENV secrets</summary>
<pre>
LLDAP_JWT_SECRET="kL6raWXhw2n57PaqnPDvUxxAFhd6Oep4atrmhlFQcRS0P1PF7hzDjG51Xj1S47x"
LLDAP_KEY_SEED="ZXchTCmdmoRQNn4YbMVTkZ4kTSOyd1n6mcOFgZ7VXrTellYmQ1Fy7id7sIrPtY1"
LLDAP_LDAP_BASE_DN="dc=S46EAHa4OTDzPkLjGmOPZ6jIjVpL0Xi,dc=com"
LLDAP_LDAP_USER_DN="S46EAHa4OTDzPkLjGmOPZ6jIjVpL0Xi"
LLDAP_LDAP_USER_EMAIL="inaneverb@example.com"
LLDAP_LDAP_USER_PASS="password123"
</pre>
</details> 

<details><summary>Screenshot</summary>
<img width="1512" alt="Screenshot 2024-01-15 at 4 11 42 PM" src="https://github.com/lldap/lldap/assets/16417743/1976ea59-e618-403f-9cc7-42af594ed9e0">
</details> 
I think this is about case sensitivity. 🤔 Rather than character size.
> I think this is about case sensitivity. 🤔 Rather than character size.

@martadinata666 Seems like you're right. I just tested `User1/password123` and `user1/password123`. First one has been failed and second one successfully logged in.
erf, yeah I see it. That should be an easy fix.

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 661bea7f..218a790b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2572,6 +2572,7 @@ dependencies = [
  "opaque-ke",
  "rand 0.8.5",
  "rust-argon2",
+ "sea-orm",
  "serde",
  "sha2 0.9.9",
  "thiserror",
diff --git a/app/src/components/change_password.rs b/app/src/components/change_password.rs
index dd296232..296fd9dc 100644
--- a/app/src/components/change_password.rs
+++ b/app/src/components/change_password.rs
@@ -97,7 +97,7 @@ impl CommonComponent<ChangePasswordForm> for ChangePasswordForm {
                             .context("Could not initialize login")?;
                     self.opaque_data = OpaqueData::Login(login_start_request.state);
                     let req = login::ClientLoginStartRequest {
-                        username: ctx.props().username.clone(),
+                        username: ctx.props().username.clone().into(),
                         login_start_request: login_start_request.message,
                     };
                     self.common.call_backend(
@@ -134,7 +134,7 @@ impl CommonComponent<ChangePasswordForm> for ChangePasswordForm {
                 )
                 .context("Could not initiate password change")?;
                 let req = registration::ClientRegistrationStartRequest {
-                    username: ctx.props().username.clone(),
+                    username: ctx.props().username.clone().into(),
                     registration_start_request: registration_start_request.message,
                 };
                 self.opaque_data = OpaqueData::Registration(registration_start_request.state);
diff --git a/app/src/components/create_user.rs b/app/src/components/create_user.rs
index ba74b800..c8c89392 100644
--- a/app/src/components/create_user.rs
+++ b/app/src/components/create_user.rs
@@ -123,7 +123,7 @@ impl CommonComponent<CreateUserForm> for CreateUserForm {
                         &mut rng,
                     )?;
                     let req = registration::ClientRegistrationStartRequest {
-                        username: user_id,
+                        username: user_id.into(),
                         registration_start_request: message,
                     };
                     self.common
diff --git a/app/src/components/login.rs b/app/src/components/login.rs
index 91bf41f2..8d35be8c 100644
--- a/app/src/components/login.rs
+++ b/app/src/components/login.rs
@@ -66,7 +66,7 @@ impl CommonComponent<LoginForm> for LoginForm {
                     opaque::client::login::start_login(&password, &mut rng)
                         .context("Could not initialize login")?;
                 let req = login::ClientLoginStartRequest {
-                    username,
+                    username: username.into(),
                     login_start_request: message,
                 };
                 self.common
diff --git a/app/src/components/reset_password_step2.rs b/app/src/components/reset_password_step2.rs
index c46d1f65..7dfece62 100644
--- a/app/src/components/reset_password_step2.rs
+++ b/app/src/components/reset_password_step2.rs
@@ -68,7 +68,7 @@ impl CommonComponent<ResetPasswordStep2Form> for ResetPasswordStep2Form {
                     opaque_registration::start_registration(new_password.as_bytes(), &mut rng)
                         .context("Could not initiate password change")?;
                 let req = registration::ClientRegistrationStartRequest {
-                    username: self.username.clone().unwrap(),
+                    username: self.username.as_ref().unwrap().into(),
                     registration_start_request: registration_start_request.message,
                 };
                 self.opaque_data = Some(registration_start_request.state);
diff --git a/auth/Cargo.toml b/auth/Cargo.toml
index 4d538237..1f8cc105 100644
--- a/auth/Cargo.toml
+++ b/auth/Cargo.toml
@@ -13,6 +13,7 @@ default = ["opaque_server", "opaque_client"]
 opaque_server = []
 opaque_client = []
 js = []
+sea_orm = ["dep:sea-orm"]
 
 [dependencies]
 rust-argon2 = "0.8"
@@ -31,6 +32,12 @@ version = "0.6"
 version = "*"
 features = [ "serde" ]
 
+[dependencies.sea-orm]
+version= "0.12"
+default-features = false
+features = ["macros"]
+optional = true
+
 # For WASM targets, use the JS getrandom.
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies.getrandom]
 version = "0.2"
diff --git a/auth/src/lib.rs b/auth/src/lib.rs
index d51af8a8..411d6bd2 100644
--- a/auth/src/lib.rs
+++ b/auth/src/lib.rs
@@ -9,17 +9,17 @@ pub mod opaque;
 
 /// The messages for the 3-step OPAQUE and simple login process.
 pub mod login {
-    use super::*;
+    use super::{types::UserId, *};
 
     #[derive(Serialize, Deserialize, Clone)]
     pub struct ServerData {
-        pub username: String,
+        pub username: UserId,
         pub server_login: opaque::server::login::ServerLogin,
     }
 
     #[derive(Serialize, Deserialize, Clone)]
     pub struct ClientLoginStartRequest {
-        pub username: String,
+        pub username: UserId,
         pub login_start_request: opaque::server::login::CredentialRequest,
     }
 
@@ -39,14 +39,14 @@ pub mod login {
 
     #[derive(Serialize, Deserialize, Clone)]
     pub struct ClientSimpleLoginRequest {
-        pub username: String,
+        pub username: UserId,
         pub password: String,
     }
 
     impl fmt::Debug for ClientSimpleLoginRequest {
         fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
             f.debug_struct("ClientSimpleLoginRequest")
-                .field("username", &self.username)
+                .field("username", &self.username.as_str())
                 .field("password", &"***********")
                 .finish()
         }
@@ -63,16 +63,16 @@ pub mod login {
 /// The messages for the 3-step OPAQUE registration process.
 /// It is used to reset a user's password.
 pub mod registration {
-    use super::*;
+    use super::{types::UserId, *};
 
     #[derive(Serialize, Deserialize, Clone)]
     pub struct ServerData {
-        pub username: String,
+        pub username: UserId,
     }
 
     #[derive(Serialize, Deserialize, Clone)]
     pub struct ClientRegistrationStartRequest {
-        pub username: String,
+        pub username: UserId,
         pub registration_start_request: opaque::server::registration::RegistrationRequest,
     }
 
@@ -104,6 +104,100 @@ pub mod password_reset {
     }
 }
 
+pub mod types {
+    use serde::{Deserialize, Serialize};
+
+    #[cfg(feature = "sea_orm")]
+    use sea_orm::{DbErr, DeriveValueType, QueryResult, TryFromU64, Value};
+
+    #[derive(
+        PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Default, Hash, Serialize, Deserialize,
+    )]
+    #[cfg_attr(feature = "sea_orm", derive(DeriveValueType))]
+    #[serde(from = "String")]
+    pub struct CaseInsensitiveString(String);
+
+    impl CaseInsensitiveString {
+        pub fn new(s: &str) -> Self {
+            Self(s.to_ascii_lowercase())
+        }
+
+        pub fn as_str(&self) -> &str {
+            self.0.as_str()
+        }
+
+        pub fn into_string(self) -> String {
+            self.0
+        }
+    }
+
+    impl From<String> for CaseInsensitiveString {
+        fn from(mut s: String) -> Self {
+            s.make_ascii_lowercase();
+            Self(s)
+        }
+    }
+
+    impl From<&String> for CaseInsensitiveString {
+        fn from(s: &String) -> Self {
+            Self::new(s.as_str())
+        }
+    }
+
+    impl From<&str> for CaseInsensitiveString {
+        fn from(s: &str) -> Self {
+            Self::new(s)
+        }
+    }
+
+    #[derive(
+        PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Default, Hash, Serialize, Deserialize,
+    )]
+    #[cfg_attr(feature = "sea_orm", derive(DeriveValueType))]
+    #[serde(from = "CaseInsensitiveString")]
+    pub struct UserId(CaseInsensitiveString);
+
+    impl UserId {
+        pub fn new(s: &str) -> Self {
+            s.into()
+        }
+        pub fn as_str(&self) -> &str {
+            self.0.as_str()
+        }
+        pub fn into_string(self) -> String {
+            self.0.into_string()
+        }
+    }
+    impl<T> From<T> for UserId
+    where
+        T: Into<CaseInsensitiveString>,
+    {
+        fn from(s: T) -> Self {
+            Self(s.into())
+        }
+    }
+    impl std::fmt::Display for UserId {
+        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+            write!(f, "{}", self.0.as_str())
+        }
+    }
+
+    #[cfg(feature = "sea_orm")]
+    impl From<&UserId> for Value {
+        fn from(user_id: &UserId) -> Self {
+            user_id.as_str().into()
+        }
+    }
+    #[cfg(feature = "sea_orm")]
+    impl TryFromU64 for UserId {
+        fn try_from_u64(_n: u64) -> Result<Self, DbErr> {
+            Err(DbErr::ConvertFromU64(
+                "UserId cannot be constructed from u64",
+            ))
+        }
+    }
+}
+
 #[derive(Clone, Serialize, Deserialize)]
 pub struct JWTClaims {
     pub exp: DateTime<Utc>,
diff --git a/auth/src/opaque.rs b/auth/src/opaque.rs
index cfdd3966..a2dfa39c 100644
--- a/auth/src/opaque.rs
+++ b/auth/src/opaque.rs
@@ -1,3 +1,4 @@
+use crate::types::UserId;
 use opaque_ke::ciphersuite::CipherSuite;
 use rand::{CryptoRng, RngCore};
 
@@ -145,12 +146,12 @@ pub mod server {
         pub fn start_registration(
             server_setup: &ServerSetup,
             registration_request: RegistrationRequest,
-            username: &str,
+            username: &UserId,
         ) -> AuthenticationResult<ServerRegistrationStartResult> {
             Ok(ServerRegistration::start(
                 server_setup,
                 registration_request,
-                username.as_bytes(),
+                username.as_str().as_bytes(),
             )?)
         }
 
@@ -178,14 +179,14 @@ pub mod server {
             server_setup: &ServerSetup,
             password_file: Option<ServerRegistration>,
             credential_request: CredentialRequest,
-            username: &str,
+            username: &UserId,
         ) -> AuthenticationResult<ServerLoginStartResult> {
             Ok(ServerLogin::start(
                 rng,
                 server_setup,
                 password_file,
                 credential_request,
-                username.as_bytes(),
+                username.as_str().as_bytes(),
                 ServerLoginStartParameters::default(),
             )?)
         }
diff --git a/migration-tool/src/lldap.rs b/migration-tool/src/lldap.rs
index 7f61c575..5e4ce358 100644
--- a/migration-tool/src/lldap.rs
+++ b/migration-tool/src/lldap.rs
@@ -136,7 +136,7 @@ fn try_login(
     let ClientLoginStartResult { state, message } =
         start_login(password, &mut rng).context("Could not initialize login")?;
     let req = ClientLoginStartRequest {
-        username: username.to_owned(),
+        username: username.into(),
         login_start_request: message,
     };
     let response = client
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 28b164e7..bbf013d4 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -79,6 +79,7 @@ version = "0.10.1"
 
 [dependencies.lldap_auth]
 path = "../auth"
+features = ["opaque_server", "opaque_client", "sea_orm"]
 
 [dependencies.opaque-ke]
 version = "0.6"
diff --git a/server/src/domain/sql_backend_handler.rs b/server/src/domain/sql_backend_handler.rs
index e7426969..5963e7de 100644
--- a/server/src/domain/sql_backend_handler.rs
+++ b/server/src/domain/sql_backend_handler.rs
@@ -63,7 +63,7 @@ pub mod tests {
             opaque::client::registration::start_registration(pass.as_bytes(), &mut rng).unwrap();
         let response = handler
             .registration_start(registration::ClientRegistrationStartRequest {
-                username: name.to_string(),
+                username: name.into(),
                 registration_start_request: client_registration_start.message,
             })
             .await
diff --git a/server/src/domain/sql_opaque_handler.rs b/server/src/domain/sql_opaque_handler.rs
index 9bda7e63..b296615a 100644
--- a/server/src/domain/sql_opaque_handler.rs
+++ b/server/src/domain/sql_opaque_handler.rs
@@ -33,7 +33,7 @@ fn passwords_match(
         server_setup,
         Some(password_file),
         client_login_start_result.message,
-        username.as_str(),
+        username,
     )?;
     client::login::finish_login(
         client_login_start_result.state,
@@ -100,15 +100,13 @@ impl OpaqueHandler for SqlOpaqueHandler {
         &self,
         request: login::ClientLoginStartRequest,
     ) -> Result<login::ServerLoginStartResponse> {
+        let user_id = request.username;
         let maybe_password_file = self
-            .get_password_file_for_user(UserId::new(&request.username))
+            .get_password_file_for_user(user_id.clone())
             .await?
             .map(|bytes| {
                 opaque::server::ServerRegistration::deserialize(&bytes).map_err(|_| {
-                    DomainError::InternalError(format!(
-                        "Corrupted password file for {}",
-                        &request.username
-                    ))
+                    DomainError::InternalError(format!("Corrupted password file for {}", &user_id))
                 })
             })
             .transpose()?;
@@ -120,11 +118,11 @@ impl OpaqueHandler for SqlOpaqueHandler {
             self.config.get_server_setup(),
             maybe_password_file,
             request.login_start_request,
-            &request.username,
+            &user_id,
         )?;
         let secret_key = self.get_orion_secret_key()?;
         let server_data = login::ServerData {
-            username: request.username,
+            username: user_id,
             server_login: start_response.state,
         };
         let encrypted_state = orion::aead::seal(&secret_key, &bincode::serialize(&server_data)?)?;
@@ -151,7 +149,7 @@ impl OpaqueHandler for SqlOpaqueHandler {
             opaque::server::login::finish_login(server_login, request.credential_finalization)?
                 .session_key;
 
-        Ok(UserId::new(&username))
+        Ok(username)
     }
 
     #[instrument(skip_all, level = "debug", err)]
@@ -191,7 +189,7 @@ impl OpaqueHandler for SqlOpaqueHandler {
             opaque::server::registration::get_password_file(request.registration_upload);
         // Set the user password to the new password.
         let user_update = model::users::ActiveModel {
-            user_id: ActiveValue::Set(UserId::new(&username)),
+            user_id: ActiveValue::Set(username),
             password_hash: ActiveValue::Set(Some(password_file.serialize())),
             ..Default::default()
         };
@@ -204,7 +202,7 @@ impl OpaqueHandler for SqlOpaqueHandler {
 #[instrument(skip_all, level = "debug", err, fields(username = %username.as_str()))]
 pub(crate) async fn register_password(
     opaque_handler: &SqlOpaqueHandler,
-    username: &UserId,
+    username: UserId,
     password: &SecUtf8,
 ) -> Result<()> {
     let mut rng = rand::rngs::OsRng;
@@ -213,7 +211,7 @@ pub(crate) async fn register_password(
         opaque::client::registration::start_registration(password.unsecure().as_bytes(), &mut rng)?;
     let start_response = opaque_handler
         .registration_start(ClientRegistrationStartRequest {
-            username: username.to_string(),
+            username,
             registration_start_request: registration_start.message,
         })
         .await?;
@@ -245,7 +243,7 @@ mod tests {
         let login_start = opaque::client::login::start_login(password, &mut rng)?;
         let start_response = opaque_handler
             .login_start(ClientLoginStartRequest {
-                username: username.to_string(),
+                username: UserId::new(username),
                 login_start_request: login_start.message,
             })
             .await?;
@@ -276,7 +274,7 @@ mod tests {
             .unwrap_err();
         register_password(
             &opaque_handler,
-            &UserId::new("bob"),
+            UserId::new("bob"),
             &secstr::SecUtf8::from("bob00"),
         )
         .await?;
diff --git a/server/src/domain/types.rs b/server/src/domain/types.rs
index a22e4871..1f498e80 100644
--- a/server/src/domain/types.rs
+++ b/server/src/domain/types.rs
@@ -2,6 +2,7 @@ use std::cmp::Ordering;
 
 use base64::Engine;
 use chrono::{NaiveDateTime, TimeZone};
+use lldap_auth::types::CaseInsensitiveString;
 use sea_orm::{
     entity::IntoActiveValue,
     sea_query::{value::ValueType, ArrayType, BlobSize, ColumnType, Nullable, ValueTypeErr},
@@ -11,6 +12,7 @@ use serde::{Deserialize, Serialize};
 use strum::{EnumString, IntoStaticStr};
 
 pub use super::model::UserColumn;
+pub use lldap_auth::types::UserId;
 
 #[derive(PartialEq, Hash, Eq, Clone, Debug, Default, Serialize, Deserialize, DeriveValueType)]
 #[serde(try_from = "&str")]
@@ -122,112 +124,6 @@ impl Serialized {
     }
 }
 
-#[derive(
-    PartialEq,
-    Eq,
-    PartialOrd,
-    Ord,
-    Clone,
-    Debug,
-    Default,
-    Hash,
-    Serialize,
-    Deserialize,
-    DeriveValueType,
-)]
-#[serde(from = "String")]
-pub struct CaseInsensitiveString(String);
-
-impl CaseInsensitiveString {
-    pub fn new(user_id: &str) -> Self {
-        Self(user_id.to_lowercase())
-    }
-
-    pub fn as_str(&self) -> &str {
-        self.0.as_str()
-    }
-
-    pub fn into_string(self) -> String {
-        self.0
-    }
-}
-
-impl From<String> for CaseInsensitiveString {
-    fn from(s: String) -> Self {
-        Self::new(&s)
-    }
-}
-
-macro_rules! make_case_insensitive_string {
-    ($c:ident) => {
-        #[derive(
-            PartialEq,
-            Eq,
-            PartialOrd,
-            Ord,
-            Clone,
-            Debug,
-            Default,
-            Hash,
-            Serialize,
-            Deserialize,
-            DeriveValueType,
-        )]
-        #[serde(from = "CaseInsensitiveString")]
-        pub struct $c(CaseInsensitiveString);
-
-        impl $c {
-            pub fn new(raw: &str) -> Self {
-                Self(CaseInsensitiveString::new(raw))
-            }
-
-            pub fn as_str(&self) -> &str {
-                self.0.as_str()
-            }
-
-            pub fn into_string(self) -> String {
-                self.0.into_string()
-            }
-        }
-
-        impl From<CaseInsensitiveString> for $c {
-            fn from(s: CaseInsensitiveString) -> Self {
-                Self(s)
-            }
-        }
-
-        impl From<String> for $c {
-            fn from(s: String) -> Self {
-                Self(CaseInsensitiveString::from(s))
-            }
-        }
-
-        impl From<&str> for $c {
-            fn from(s: &str) -> Self {
-                Self::new(s)
-            }
-        }
-
-        impl std::fmt::Display for $c {
-            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
-                write!(f, "{}", self.0.as_str())
-            }
-        }
-
-        impl From<&$c> for Value {
-            fn from(user_id: &$c) -> Self {
-                user_id.as_str().into()
-            }
-        }
-
-        impl TryFromU64 for $c {
-            fn try_from_u64(_n: u64) -> Result<Self, DbErr> {
-                Err(DbErr::ConvertFromU64("$c cannot be constructed from u64"))
-            }
-        }
-    };
-}
-
 fn compare_str_case_insensitive(s1: &str, s2: &str) -> Ordering {
     let mut it_1 = s1.chars().flat_map(|c| c.to_lowercase());
     let mut it_2 = s2.chars().flat_map(|c| c.to_lowercase());
@@ -323,8 +219,58 @@ macro_rules! make_case_insensitive_comparable_string {
     };
 }
 
-make_case_insensitive_string!(UserId);
-make_case_insensitive_string!(AttributeName);
+#[derive(
+    PartialEq,
+    Eq,
+    PartialOrd,
+    Ord,
+    Clone,
+    Debug,
+    Default,
+    Hash,
+    Serialize,
+    Deserialize,
+    DeriveValueType,
+)]
+#[serde(from = "CaseInsensitiveString")]
+pub struct AttributeName(CaseInsensitiveString);
+
+impl AttributeName {
+    pub fn new(s: &str) -> Self {
+        s.into()
+    }
+    pub fn as_str(&self) -> &str {
+        self.0.as_str()
+    }
+    pub fn into_string(self) -> String {
+        self.0.into_string()
+    }
+}
+impl<T> From<T> for AttributeName
+where
+    T: Into<CaseInsensitiveString>,
+{
+    fn from(s: T) -> Self {
+        Self(s.into())
+    }
+}
+impl std::fmt::Display for AttributeName {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        write!(f, "{}", self.0.as_str())
+    }
+}
+impl From<&AttributeName> for Value {
+    fn from(attribute_name: &AttributeName) -> Self {
+        attribute_name.as_str().into()
+    }
+}
+impl TryFromU64 for AttributeName {
+    fn try_from_u64(_n: u64) -> Result<Self, DbErr> {
+        Err(DbErr::ConvertFromU64(
+            "AttributeName cannot be constructed from u64",
+        ))
+    }
+}
 make_case_insensitive_comparable_string!(Email);
 make_case_insensitive_comparable_string!(GroupName);
 
diff --git a/server/src/infra/auth_service.rs b/server/src/infra/auth_service.rs
index b65b9d0f..78ee47b3 100644
--- a/server/src/infra/auth_service.rs
+++ b/server/src/infra/auth_service.rs
@@ -428,13 +428,13 @@ async fn simple_login<Backend>(
 where
     Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + LoginHandler + 'static,
 {
-    let user_id = UserId::new(&request.username);
+    let login::ClientSimpleLoginRequest { username, password } = request.into_inner();
     let bind_request = BindRequest {
-        name: user_id.clone(),
-        password: request.password.clone(),
+        name: username.clone(),
+        password,
     };
     data.get_login_handler().bind(bind_request).await?;
-    get_login_successful_response(&data, &user_id).await
+    get_login_successful_response(&data, &username).await
 }
 
 async fn simple_login_handler<Backend>(
@@ -500,14 +500,14 @@ where
         .await
         .map_err(|e| TcpError::BadRequest(format!("{:#?}", e)))?
         .into_inner();
-    let user_id = UserId::new(&registration_start_request.username);
+    let user_id = &registration_start_request.username;
     let user_is_admin = data
         .get_readonly_handler()
-        .get_user_groups(&user_id)
+        .get_user_groups(user_id)
         .await?
         .iter()
         .any(|g| g.display_name == "lldap_admin".into());
-    if !validation_result.can_change_password(&user_id, user_is_admin) {
+    if !validation_result.can_change_password(user_id, user_is_admin) {
         return Err(TcpError::UnauthorizedError(
             "Not authorized to change the user's password".to_string(),
         ));
diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs
index 02b901c9..2b05e231 100644
--- a/server/src/infra/ldap_handler.rs
+++ b/server/src/infra/ldap_handler.rs
@@ -306,7 +306,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
     async fn change_password<B: OpaqueHandler>(
         &self,
         backend_handler: &B,
-        user: &UserId,
+        user: UserId,
         password: &[u8],
     ) -> Result<()> {
         use lldap_auth::*;
@@ -314,7 +314,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
         let registration_start_request =
             opaque::client::registration::start_registration(password, &mut rng)?;
         let req = registration::ClientRegistrationStartRequest {
-            username: user.to_string(),
+            username: user.clone(),
             registration_start_request: registration_start_request.message,
         };
         let registration_start_response = backend_handler.registration_start(req).await?;
@@ -371,7 +371,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
                                 ),
                             })
                         } else if let Err(e) = self
-                            .change_password(self.get_opaque_handler(), &uid, password.as_bytes())
+                            .change_password(self.get_opaque_handler(), uid, password.as_bytes())
                             .await
                         {
                             Err(LdapError {
@@ -413,7 +413,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
 
     async fn handle_modify_change(
         &mut self,
-        user_id: &UserId,
+        user_id: UserId,
         credentials: &ValidationResults,
         user_is_admin: bool,
         change: &LdapModify,
@@ -429,7 +429,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
                 ),
             });
         }
-        if !credentials.can_change_password(user_id, user_is_admin) {
+        if !credentials.can_change_password(&user_id, user_is_admin) {
             return Err(LdapError {
                 code: LdapResultCode::InsufficentAccessRights,
                 message: format!(
@@ -488,7 +488,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
                     .iter()
                     .any(|g| g.display_name == "lldap_admin".into());
                 for change in &request.changes {
-                    self.handle_modify_change(&uid, &credentials, user_is_admin, change)
+                    self.handle_modify_change(uid.clone(), &credentials, user_is_admin, change)
                         .await?
                 }
                 Ok(vec![make_modify_response(
@@ -2199,7 +2199,7 @@ mod tests {
             opaque::client::registration::start_registration("password".as_bytes(), &mut rng)
                 .unwrap();
         let request = registration::ClientRegistrationStartRequest {
-            username: "bob".to_string(),
+            username: "bob".into(),
             registration_start_request: registration_start_request.message,
         };
         let start_response = opaque::server::registration::start_registration(
@@ -2247,7 +2247,7 @@ mod tests {
             opaque::client::registration::start_registration("password".as_bytes(), &mut rng)
                 .unwrap();
         let request = registration::ClientRegistrationStartRequest {
-            username: "bob".to_string(),
+            username: "bob".into(),
             registration_start_request: registration_start_request.message,
         };
         let start_response = opaque::server::registration::start_registration(
@@ -2297,7 +2297,7 @@ mod tests {
             opaque::client::registration::start_registration("password".as_bytes(), &mut rng)
                 .unwrap();
         let request = registration::ClientRegistrationStartRequest {
-            username: "bob".to_string(),
+            username: "bob".into(),
             registration_start_request: registration_start_request.message,
         };
         let start_response = opaque::server::registration::start_registration(
diff --git a/server/src/main.rs b/server/src/main.rs
index 303d634b..560a77d9 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -46,7 +46,9 @@ async fn create_admin_user(handler: &SqlBackendHandler, config: &Configuration)
             display_name: Some("Administrator".to_string()),
             ..Default::default()
         })
-        .and_then(|_| register_password(handler, &config.ldap_user_dn, &config.ldap_user_pass))
+        .and_then(|_| {
+            register_password(handler, config.ldap_user_dn.clone(), &config.ldap_user_pass)
+        })
         .await
         .context("Error creating admin user")?;
     let groups = handler
@@ -137,7 +139,7 @@ async fn set_up_server(config: Configuration) -> Result<ServerBuilder> {
         warn!("Forcing admin password reset to the config-provided password");
         register_password(
             &backend_handler,
-            &config.ldap_user_dn,
+            config.ldap_user_dn.clone(),
             &config.ldap_user_pass,
         )
         .await
diff --git a/set-password/src/main.rs b/set-password/src/main.rs
index 74d3f85b..9ed9cd73 100644
--- a/set-password/src/main.rs
+++ b/set-password/src/main.rs
@@ -49,7 +49,7 @@ fn get_token(base_url: &Url, username: &str, password: &str) -> Result<String> {
         .header(reqwest::header::CONTENT_TYPE, "application/json")
         .body(
             serde_json::to_string(&lldap_auth::login::ClientSimpleLoginRequest {
-                username: username.to_string(),
+                username: username.into(),
                 password: password.to_string(),
             })
             .expect("Failed to encode the username/password as json to log in"),
@@ -121,7 +121,7 @@ fn main() -> Result<()> {
         opaque::client::registration::start_registration(opts.password.as_bytes(), &mut rng)
             .context("Could not initiate password change")?;
     let start_request = registration::ClientRegistrationStartRequest {
-        username: opts.username.to_string(),
+        username: opts.username.clone().into(),
         registration_start_request: registration_start_request.message,
     };
     let res = register_start(&opts.base_url, &token, start_request)?;


Test patch:
diff --git a/server/tests/common/auth.rs b/server/tests/common/auth.rs
index 3414398d..eff4e027 100644
--- a/server/tests/common/auth.rs
+++ b/server/tests/common/auth.rs
@@ -10,7 +10,7 @@ pub fn get_token(client: &Client) -> String {
         .header(reqwest::header::CONTENT_TYPE, "application/json")
         .body(
             serde_json::to_string(&lldap_auth::login::ClientSimpleLoginRequest {
-                username,
+                username: username.into(),
                 password,
             })
             .expect("Failed to encode the username/password as json to log in"),