Repository: jupyterhub/jupyterhub

Issue:
trim username to avoid creating of several notebook_dir volumes
### Proposed change

I think it would be better if you do a `user.name.rstrip()` (maybe in the login form) in order to avoid the dummy authenticator to spawn several `notebook_dir` for the "same" username but with spaces in the end. I've recently accessed my jupyterhub via tablet and the keyboard automatically added a space after completing my username. To my surprise all my notebooks (ipynb files) were gone until I realize that the problem was that I was using a different username (containing a space in the end) . If using docker, several volumes would be created simply by adding a space or two in the username end.  This can be avoided by trimming the username. Also you cannot delete a user (via webgui - hub control panel) created like this (without trimming). TIA.



Discussion:
Thank you for opening your first issue in this project! Engagement like this is essential for open source projects! :hugs:
<br>If you haven't done so already, check out [Jupyter's Code of Conduct](https://github.com/jupyter/governance/blob/master/conduct/code_of_conduct.md). Also, please try to follow the issue template as it helps other other community members to contribute more effectively.
![welcome](https://raw.githubusercontent.com/jupyterhub/.github/master/images/welcome.jpg)
You can meet the other [Jovyans](https://jupyter.readthedocs.io/en/latest/community/content-community.html?highlight=jovyan#what-is-a-jovyan) by joining our [Discourse forum](http://discourse.jupyter.org/). There is also an intro thread there where you can stop by and say Hi! :wave:
<br>Welcome to the Jupyter community! :tada:


Resolving patch:
diff --git a/jupyterhub/auth.py b/jupyterhub/auth.py
--- a/jupyterhub/auth.py
+++ b/jupyterhub/auth.py
@@ -256,6 +256,9 @@ def validate_username(self, username):
         if not username:
             # empty usernames are not allowed
             return False
+        if username != username.strip():
+            # starting/ending with space is not allowed
+            return False
         if not self.username_regex:
             return True
         return bool(self.username_regex.match(username))
diff --git a/jupyterhub/handlers/login.py b/jupyterhub/handlers/login.py
--- a/jupyterhub/handlers/login.py
+++ b/jupyterhub/handlers/login.py
@@ -145,7 +145,9 @@ async def post(self):
         # parse the arguments dict
         data = {}
         for arg in self.request.arguments:
-            data[arg] = self.get_argument(arg, strip=False)
+            # strip username, but not other fields like passwords,
+            # which should be allowed to start or end with space
+            data[arg] = self.get_argument(arg, strip=arg == "username")
 
         auth_timer = self.statsd.timer('login.authenticate').start()
         user = await self.login_user(data)


Test patch:
diff --git a/jupyterhub/tests/test_pages.py b/jupyterhub/tests/test_pages.py
--- a/jupyterhub/tests/test_pages.py
+++ b/jupyterhub/tests/test_pages.py
@@ -740,9 +740,17 @@ async def test_login_fail(app):
     assert not r.cookies
 
 
-async def test_login_strip(app):
-    """Test that login form doesn't strip whitespace from passwords"""
-    form_data = {'username': 'spiff', 'password': ' space man '}
+@pytest.mark.parametrize(
+    "form_user, auth_user, form_password",
+    [
+        ("spiff", "spiff", " space man "),
+        (" spiff ", "spiff", " space man "),
+    ],
+)
+async def test_login_strip(app, form_user, auth_user, form_password):
+    """Test that login form strips space form usernames, but not passwords"""
+    form_data = {"username": form_user, "password": form_password}
+    expected_auth = {"username": auth_user, "password": form_password}
     base_url = public_url(app)
     called_with = []
 
@@ -754,7 +762,7 @@ async def mock_authenticate(handler, data):
             base_url + 'hub/login', data=form_data, allow_redirects=False
         )
 
-    assert called_with == [form_data]
+    assert called_with == [expected_auth]
 
 
 @pytest.mark.parametrize(