Repository: privacyidea/privacyidea

Issue:
Allow validate check for rlm_rest
To be able to use FreeRADIUS rlm_rest we need to provide a validate endpoint, that returns the results in HTTP Return Codes.

Add another route to ``api/validate.py`` on top of the ``check`` function.

See the return codes here:
https://doc.freeradius.org/rlm__rest_8c_source.html



Discussion:

Resolving patch:
diff --git a/privacyidea/api/lib/postpolicy.py b/privacyidea/api/lib/postpolicy.py
--- a/privacyidea/api/lib/postpolicy.py
+++ b/privacyidea/api/lib/postpolicy.py
@@ -43,7 +43,7 @@
 import logging
 log = logging.getLogger(__name__)
 from privacyidea.lib.error import PolicyError
-from flask import g, current_app
+from flask import g, current_app, make_response
 from privacyidea.lib.policy import SCOPE, ACTION, AUTOASSIGNVALUE
 from privacyidea.lib.user import get_user_from_param
 from privacyidea.lib.token import get_tokens, assign_token, get_realms_of_token
@@ -54,7 +54,7 @@
 import re
 import netaddr
 from privacyidea.lib.crypto import Sign
-from privacyidea.api.lib.utils import get_all_params
+from privacyidea.api.lib.utils import get_all_params, getParam
 from privacyidea.lib.auth import ROLE
 from privacyidea.lib.user import (split_user, User)
 from privacyidea.lib.realm import get_default_realm
@@ -645,3 +645,24 @@ def autoassign(request, response):
                                 break
 
     return response
+
+def construct_radius_response(request, response):
+    """
+    This decorator implements the /validate/radiuscheck endpoint.
+    In case this URL was requested, a successful authentication
+    results in an empty response with a HTTP 204 status code.
+    An unsuccessful authentication results in an empty response
+    with a HTTP 400 status code.
+    :return:
+    """
+    if request.url_rule.rule == '/validate/radiuscheck':
+        return_code = 400 # generic 400 error by default
+        content = json.loads(response.data)
+        if content['result']['status']:
+            if content['result']['value']:
+                # user was successfully authenticated
+                return_code = 204
+        # send empty body
+        return make_response('', return_code)
+    else:
+        return response
diff --git a/privacyidea/api/validate.py b/privacyidea/api/validate.py
--- a/privacyidea/api/validate.py
+++ b/privacyidea/api/validate.py
@@ -83,7 +83,7 @@
                                             no_detail_on_fail,
                                             no_detail_on_success, autoassign,
                                             offline_info,
-                                            add_user_detail_to_response)
+                                            add_user_detail_to_response, construct_radius_response)
 from privacyidea.lib.policy import PolicyClass
 from privacyidea.lib.config import ConfigClass
 from privacyidea.lib.event import EventConfiguration
@@ -158,6 +158,8 @@ def after_request(response):
 
 
 @validate_blueprint.route('/check', methods=['POST', 'GET'])
+@validate_blueprint.route('/radiuscheck', methods=['POST', 'GET'])
+@postpolicy(construct_radius_response, request=request)
 @postpolicy(no_detail_on_fail, request=request)
 @postpolicy(no_detail_on_success, request=request)
 @postpolicy(add_user_detail_to_response, request=request)
@@ -179,6 +181,12 @@ def check():
     The PIN and OTP value is sent in the parameter ``pass``.
     In case of successful authentication it returns ``result->value: true``.
 
+    In case ``/validate/radiuscheck`` is requested, the responses are
+    modified as follows: A successful authentication returns an empty HTTP
+    204 response. An unsuccessful authentication returns an empty HTTP
+    400 response. Error responses are the same responses as for the
+    ``/validate/check`` endpoint.
+
     :param serial: The serial number of the token, that tries to authenticate.
     :param user: The loginname/username of the user, who tries to authenticate.
     :param realm: The realm of the user, who tries to authenticate. If the


Test patch:
diff --git a/tests/test_api_validate.py b/tests/test_api_validate.py
--- a/tests/test_api_validate.py
+++ b/tests/test_api_validate.py
@@ -1582,3 +1582,36 @@ def test_27_multiple_challenge_response_different_pin(self):
         remove_token("CR2B")
         delete_policy("test49")
 
+    def test_28_validate_radiuscheck(self):
+        # setup a spass token
+        token_obj = init_token({"serial": "pass3", "pin": "123456",
+                                "type": "spass"})
+
+        # test successful authentication
+        with self.app.test_request_context('/validate/radiuscheck',
+                                           method='POST',
+                                           data={"serial": "pass3",
+                                                 "pass": "123456"}):
+            res = self.app.full_dispatch_request()
+            # HTTP 204 status code signals a successful authentication
+            self.assertEqual(res.status_code, 204)
+            self.assertEqual(res.data, '')
+
+        # test authentication fails with wrong PIN
+        with self.app.test_request_context('/validate/radiuscheck',
+                                           method='POST',
+                                           data={"serial": "pass3",
+                                                 "pass": "wrong"}):
+            res = self.app.full_dispatch_request()
+            self.assertEqual(res.status_code, 400)
+            self.assertEqual(res.data, '')
+
+        # test authentication fails with an unknown user
+        # here, we get an ordinary JSON response
+        with self.app.test_request_context('/validate/radiuscheck',
+                                           method='POST',
+                                           data={"user": "unknown",
+                                                 "pass": "123456"}):
+            res = self.app.full_dispatch_request()
+            result = json.loads(res.data).get("result")
+            self.assertFalse(result.get("status"))
\ No newline at end of file