Repository: privacyidea/privacyidea

Issue:
RADIUS token and passthru policy fail with non-ASCII usernames
If we assign a RADIUS token to a user whose username contains non-ASCII characters, the user cannot authenticate. The debug log shows the following error:
```
[2019-02-08 16:41:22,926][7670][139677530805632][ERROR][privacyidea.lib.tokens.radiustoken:281] Error contacting radius Server: UnicodeEncodeError('ascii', u'k\xf6lbel', 1, 2, 'ordinal not in range(128)')
```
The problem is that we encode the username to ASCII before passing it to pyrad here:
https://github.com/privacyidea/privacyidea/blob/42cf5d841465a3aaafc9750b25fd3b71f551b488/privacyidea/lib/tokens/radiustoken.py#L249

I guess it would be sensible to just encode the username to UTF-8.


Discussion:
Just noticed the passthru policy also doesn't support non-ASCII usernames:
```shell
$ http POST http://localhost:5000/validate/check user=kölbel@int pass=125
HTTP/1.0 500 INTERNAL SERVER ERROR
Cache-Control: no-cache
Content-Length: 892
Content-Type: application/json
Date: Fri, 08 Feb 2019 17:08:36 GMT
Server: Werkzeug/0.14.1 Python/2.7.15

{
    "detail": null,
    "id": 1,
    "jsonrpc": "2.0",
    "result": {
        "error": {
            "code": -500,
            "message": "'ascii' codec can't encode character u'\\xf6' in position 1: ordinal not in range(128)"
        },
        "status": false
    },
    "signature": "...",
    "time": 1549645716.914687,
    "version": "privacyIDEA 2.23.2"
}
```

Resolving patch:
diff --git a/privacyidea/lib/radiusserver.py b/privacyidea/lib/radiusserver.py
--- a/privacyidea/lib/radiusserver.py
+++ b/privacyidea/lib/radiusserver.py
@@ -78,7 +78,9 @@ def request(config, user, password):
         :param config: The RADIUS configuration
         :type config: RADIUSServer Database Model
         :param user: the radius username
+        :type user: str
         :param password: the radius password
+        :type password: str
         :return: True or False. If any error occurs, an exception is raised.
         """
         success = False
@@ -106,9 +108,10 @@ def request(config, user, password):
             srv.retries = config.retries
 
         req = srv.CreateAuthPacket(code=pyrad.packet.AccessRequest,
-                                   User_Name=user.encode('ascii'),
+                                   User_Name=user.encode('utf-8'),
                                    NAS_Identifier=nas_identifier.encode('ascii'))
 
+        # PwCrypt encodes unicode strings to UTF-8
         req["User-Password"] = req.PwCrypt(password)
         try:
             response = srv.SendPacket(req)
diff --git a/privacyidea/lib/tokens/radiustoken.py b/privacyidea/lib/tokens/radiustoken.py
--- a/privacyidea/lib/tokens/radiustoken.py
+++ b/privacyidea/lib/tokens/radiustoken.py
@@ -246,7 +246,7 @@ def check_otp(self, otpval, counter=None, window=None, options=None):
                          dict=Dictionary(radius_dictionary))
 
             req = srv.CreateAuthPacket(code=pyrad.packet.AccessRequest,
-                                       User_Name=radius_user.encode('ascii'),
+                                       User_Name=radius_user.encode('utf-8'),
                                        NAS_Identifier=nas_identifier.encode('ascii'))
 
             req["User-Password"] = req.PwCrypt(otpval)


Test patch:
diff --git a/tests/test_lib_radiusserver.py b/tests/test_lib_radiusserver.py
--- a/tests/test_lib_radiusserver.py
+++ b/tests/test_lib_radiusserver.py
@@ -1,3 +1,4 @@
+# coding: utf-8
 """
 This test file tests the lib/radiusserver.py
 """
@@ -99,3 +100,13 @@ def test_06_test_radius(self):
                           identifier="myserver", server="1.2.3.4",
                           user="user", password="password",
                           secret="x" * 96, dictionary=DICT_FILE)
+
+    @radiusmock.activate
+    def test_07_non_ascii(self):
+        radiusmock.setdata(success=True)
+        r = add_radius(identifier="myserver", server="1.2.3.4",
+                       secret="testing123", dictionary=DICT_FILE)
+        self.assertTrue(r > 0)
+        radius = get_radius("myserver")
+        r = RADIUSServer.request(radius.config, u"nönäscii", u"passwörd")
+        self.assertEqual(r, True)
diff --git a/tests/test_lib_tokens_radius.py b/tests/test_lib_tokens_radius.py
--- a/tests/test_lib_tokens_radius.py
+++ b/tests/test_lib_tokens_radius.py
@@ -1,3 +1,4 @@
+# coding: utf-8
 """
 This test file tests the lib.tokens.radiustoken
 This depends on lib.tokenclass
@@ -173,3 +174,18 @@ def test_11_RADIUS_request(self):
         self.assertEqual(r[0], True)
         self.assertEqual(r[1], 0)
         self.assertEqual(r[2].get("message"), "matching 1 tokens")
+
+    @radiusmock.activate
+    def test_12_non_ascii(self):
+        set_privacyidea_config("radius.dictfile", DICT_FILE)
+        radiusmock.setdata(success=True)
+        r = add_radius(identifier="myserver", server="1.2.3.4",
+                       secret="testing123", dictionary=DICT_FILE)
+        self.assertTrue(r > 0)
+        token = init_token({"type": "radius",
+                            "radius.identifier": "myserver",
+                            "radius.user": u"nönäscii"})
+        r = token.authenticate(u"passwörd")
+        self.assertEqual(r[0], True)
+        self.assertEqual(r[1], 0)
+        self.assertEqual(r[2].get("message"), "matching 1 tokens")