We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Use information, which pyhsical token was used with a remote token
**Is your feature request related to a problem? Please describe.**

With a remote token it would be great to know, what actually was the physical token that led to a successful authentication.
This can help with rollout scenarios and assigning those physical tokens.

This is interesting if the remote token object points to a user on the remote system. The user can have several tokens. But it would be interesting to know, which token was actually used for authentication.

**Describe the solution you'd like**

The remote tokentype could store the information, which physical token was used for authentication. Returning it in the response does not work out well.
It can be stored in the tokeninfo.



--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `privacyidea/lib/tokens/remotetoken.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 #
3 #  privacyIDEA is a fork of LinOTP
4 #  May 08, 2014 Cornelius Kölbel
5 #  License:  AGPLv3
6 #  contact:  http://www.privacyidea.org
7 #
8 #  2018-01-21 Cornelius Kölbel <cornelius.koelbel@netknights.it>
9 #             Add tokenkind
10 #  2015-01-28 Rewrite for migration to flask
11 #             Cornelius Kölbel <cornelius@privacyidea.org>
12 #
13 #
14 #  Copyright (C) 2010 - 2014 LSE Leading Security Experts GmbH
15 #  License:  LSE
16 #  contact:  http://www.linotp.org
17 #            http://www.lsexperts.de
18 #            linotp@lsexperts.de
19 #
20 # This code is free software; you can redistribute it and/or
21 # modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
22 # License as published by the Free Software Foundation; either
23 # version 3 of the License, or any later version.
24 #
25 # This code is distributed in the hope that it will be useful,
26 # but WITHOUT ANY WARRANTY; without even the implied warranty of
27 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
28 # GNU AFFERO GENERAL PUBLIC LICENSE for more details.
29 #
30 # You should have received a copy of the GNU Affero General Public
31 # License along with this program.  If not, see <http://www.gnu.org/licenses/>.
32 #
33 __doc__ = """This is the implementation of the remote token. The remote token
34 forwards an authentication request to another privacyidea server.
35 
36 To do this it uses the parameters remote.server, remote.realm,
37 remote.resolver, remote.user or remote.serial.
38 The parameter remote.local_checkpin determines, whether the PIN should be
39 checked locally or remotely.
40 
41 The code is tested in tests/test_lib_tokens_remote
42 """
43 
44 import logging
45 import traceback
46 import requests
47 from privacyidea.lib.utils import is_true
48 from privacyidea.lib.decorators import check_token_locked
49 from privacyidea.lib.config import get_from_config
50 from privacyidea.api.lib.utils import getParam
51 from privacyidea.lib.log import log_with
52 from privacyidea.lib.policydecorators import challenge_response_allowed
53 from privacyidea.lib.tokenclass import TokenClass, TOKENKIND
54 from privacyidea.lib import _
55 from privacyidea.lib.policy import SCOPE, ACTION, GROUP
56 
57 optional = True
58 required = False
59 
60 log = logging.getLogger(__name__)
61 
62 ###############################################
63 
64 
65 class RemoteTokenClass(TokenClass):
66     """
67     The Remote token forwards an authentication request to another privacyIDEA
68     server. The request can be forwarded to a user on the other server or to
69     a serial number on the other server. The PIN can be checked on the local
70     privacyIDEA server or on the remote server.
71 
72     Using the Remote token you can assign one physical token to many
73     different users.
74     """
75 
76     def __init__(self, db_token):
77         """
78         constructor - create a token class object with it's db token binding
79 
80         :param aToken: the db bound token
81         """
82         TokenClass.__init__(self, db_token)
83         self.set_type(u"remote")
84         self.mode = ['authenticate', 'challenge']
85 
86     @staticmethod
87     def get_class_type():
88         """
89         return the class type identifier
90         """
91         return "remote"
92 
93     @staticmethod
94     def get_class_prefix():
95         """
96         return the token type prefix
97         """
98         return "PIRE"
99 
100     @staticmethod
101     @log_with(log)
102     def get_class_info(key=None, ret='all'):
103         """
104         :param key: subsection identifier
105         :type key: string
106         :param ret: default return value, if nothing is found
107         :type ret: user defined
108         :return: subsection if key exists or user defined
109         :rtype: dict or string
110         """
111         res = {'type': 'remote',
112                'title': 'Remote Token',
113                'description': _('Remote Token: Forward authentication request '
114                                 'to another server.'),
115                'user': [],
116                # This tokentype is enrollable in the UI for...
117                'ui_enroll': ["admin"],
118                'policy': {
119                    SCOPE.ENROLL: {
120                        ACTION.MAXTOKENUSER: {
121                            'type': 'int',
122                            'desc': _("The user may only have this maximum number of remote tokens assigned."),
123                            'group': GROUP.TOKEN
124                        },
125                        ACTION.MAXACTIVETOKENUSER: {
126                            'type': 'int',
127                            'desc': _(
128                                "The user may only have this maximum number of active remote tokens assigned."),
129                            'group': GROUP.TOKEN
130                        }
131                    }
132                },
133                }
134 
135         if key:
136             ret = res.get(key, {})
137         else:
138             if ret == 'all':
139                 ret = res
140         return ret
141 
142     def update(self, param):
143         """
144         second phase of the init process - updates parameters
145 
146         :param param: the request parameters
147         :return: - nothing -
148         """
149         # if another OTP length would be specified in /admin/init this would
150         # be overwritten by the parent class, which is ok.
151         self.set_otplen(6)
152         TokenClass.update(self, param)
153 
154         remoteServer = getParam(param, "remote.server", required)
155         self.add_tokeninfo("remote.server", remoteServer)
156 
157         val = getParam(param, "remote.local_checkpin", optional) or 0
158         self.add_tokeninfo("remote.local_checkpin", val)
159 
160         for key in ["remote.serial", "remote.user", "remote.path",
161                     "remote.realm", "remote.resolver"]:
162             val = getParam(param, key, optional)
163             if val is not None:
164                 self.add_tokeninfo(key, val)
165 
166         self.add_tokeninfo("tokenkind", TOKENKIND.VIRTUAL)
167 
168     @property
169     def check_pin_local(self):
170         """
171         lookup if pin should be checked locally or on remote host
172 
173         :return: bool
174         """
175         local_check = is_true(self.get_tokeninfo("remote.local_checkpin"))
176         log.debug(" local checking pin? {0!r}".format(local_check))
177 
178         return local_check
179 
180     @log_with(log)
181     @check_token_locked
182     def authenticate(self, passw, user=None, options=None):
183         """
184         do the authentication on base of password / otp and user and
185         options, the request parameters.
186 
187         Here we contact the other privacyIDEA server to validate the OtpVal.
188 
189         :param passw: the password / otp
190         :param user: the requesting user
191         :param options: the additional request parameters
192 
193         :return: tuple of (success, otp_count - 0 or -1, reply)
194 
195         """
196         res = False
197         otp_counter = -1
198         reply = None
199         otpval = passw
200 
201         # should we check the pin localy?
202         if self.check_pin_local:
203             (_res, pin, otpval) = self.split_pin_pass(passw, user,
204                                                       options=options)
205 
206             if not TokenClass.check_pin(self, pin):
207                 return False, otp_counter, {'message': "Wrong PIN"}
208 
209         otp_count = self.check_otp(otpval, options=options)
210         if otp_count >= 0:
211             res = True
212             reply = {'message': 'matching 1 tokens',
213                      'serial': self.get_serial(),
214                      'type': self.get_tokentype()}
215         else:
216             reply = {'message': 'remote side denied access'}
217 
218         return res, otp_count, reply
219 
220     @check_token_locked
221     def check_otp(self, otpval, counter=None, window=None, options=None):
222         """
223         run the http request against the remote host
224 
225         :param otpval: the OTP value
226         :param counter: The counter for counter based otp values
227         :type counter: int
228         :param window: a counter window
229         :type counter: int
230         :param options: additional token specific options
231         :type options: dict
232         :return: counter of the matching OTP value.
233         :rtype: int
234         """
235         otp_count = -1
236 
237         remoteServer = self.get_tokeninfo("remote.server") or ""
238 
239         # in preparation of the ability to relocate privacyidea urls,
240         # we introduce the remote url path
241         remotePath = self.get_tokeninfo("remote.path") or ""
242         remotePath = remotePath.strip()
243 
244         remoteSerial = self.get_tokeninfo("remote.serial") or ""
245 
246         remoteUser = self.get_tokeninfo("remote.user") or ""
247 
248         remoteRealm = self.get_tokeninfo("remote.realm") or ""
249 
250         remoteResolver = self.get_tokeninfo("remote.resolver") or ""
251 
252         ssl_verify = get_from_config("remote.verify_ssl_certificate",
253                                      False, return_bool=True) or False
254 
255         # here we also need to check for remote.user and so on....
256         log.debug("checking OTP len:%r remotely on server: %r,"
257                   " serial: %r, user: %r" %
258                   (len(otpval), remoteServer, remoteSerial, remoteUser))
259         params = {}
260 
261         remotePath = remotePath or "/validate/check"
262         if remoteSerial:
263             params['serial'] = remoteSerial
264         elif remoteUser:
265             params['user'] = remoteUser
266             params['realm'] = remoteRealm
267             params['resolver'] = remoteResolver
268 
269         else:
270             log.warning("The remote token does neither contain a "
271                         "remote.serial nor a remote.user.")
272             return otp_count
273 
274         params['pass'] = otpval
275         request_url = "{0!s}{1!s}".format(remoteServer, remotePath)
276 
277         try:
278             r = requests.post(request_url, data=params, verify=ssl_verify)
279 
280             if r.status_code == requests.codes.ok:
281                 response = r.json()
282                 result = response.get("result")
283                 if result.get("value"):
284                     otp_count = 1
285 
286         except Exception as exx:  # pragma: no cover
287             log.error("Error getting response from "
288                       "remote Server (%r): %r" % (request_url, exx))
289             log.debug("{0!s}".format(traceback.format_exc()))
290 
291         return otp_count
292 
293     @log_with(log)
294     @challenge_response_allowed
295     def is_challenge_request(self, passw, user=None, options=None):
296         """
297         This method checks, if this is a request, that triggers a challenge.
298         It depends on the way, the pin is checked - either locally or remote
299 
300         :param passw: password, which might be pin or pin+otp
301         :type passw: string
302         :param user: The user from the authentication request
303         :type user: User object
304         :param options: dictionary of additional request parameters
305         :type options: dict
306 
307         :return: true or false
308         """
309 
310         request_is_valid = False
311 
312         if self.check_pin_local:
313             pin_match = self.check_pin(passw, user=user,
314                                          options=options)
315             if pin_match is True:
316                 request_is_valid = True
317 
318         return request_is_valid
319
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/privacyidea/lib/tokens/remotetoken.py b/privacyidea/lib/tokens/remotetoken.py
--- a/privacyidea/lib/tokens/remotetoken.py
+++ b/privacyidea/lib/tokens/remotetoken.py
@@ -282,6 +282,8 @@
                 result = response.get("result")
                 if result.get("value"):
                     otp_count = 1
+                    # Add the serial of the used remote token in the tokeninfo parameters
+                    self.add_tokeninfo("last_matching_remote_serial", response.get("detail", {}).get("serial"))
 
         except Exception as exx:  # pragma: no cover
             log.error("Error getting response from "

{"golden_diff": "diff --git a/privacyidea/lib/tokens/remotetoken.py b/privacyidea/lib/tokens/remotetoken.py\n--- a/privacyidea/lib/tokens/remotetoken.py\n+++ b/privacyidea/lib/tokens/remotetoken.py\n@@ -282,6 +282,8 @@\n                 result = response.get(\"result\")\n                 if result.get(\"value\"):\n                     otp_count = 1\n+                    # Add the serial of the used remote token in the tokeninfo parameters\n+                    self.add_tokeninfo(\"last_matching_remote_serial\", response.get(\"detail\", {}).get(\"serial\"))\n \n         except Exception as exx:  # pragma: no cover\n             log.error(\"Error getting response from \"\n", "issue": "Use information, which pyhsical token was used with a remote token\n**Is your feature request related to a problem? Please describe.**\r\n\r\nWith a remote token it would be great to know, what actually was the physical token that led to a successful authentication.\r\nThis can help with rollout scenarios and assigning those physical tokens.\r\n\r\nThis is interesting if the remote token object points to a user on the remote system. The user can have several tokens. But it would be interesting to know, which token was actually used for authentication.\r\n\r\n**Describe the solution you'd like**\r\n\r\nThe remote tokentype could store the information, which physical token was used for authentication. Returning it in the response does not work out well.\r\nIt can be stored in the tokeninfo.\r\n\r\n\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\n#\n#  privacyIDEA is a fork of LinOTP\n#  May 08, 2014 Cornelius K\u00f6lbel\n#  License:  AGPLv3\n#  contact:  http://www.privacyidea.org\n#\n#  2018-01-21 Cornelius K\u00f6lbel <cornelius.koelbel@netknights.it>\n#             Add tokenkind\n#  2015-01-28 Rewrite for migration to flask\n#             Cornelius K\u00f6lbel <cornelius@privacyidea.org>\n#\n#\n#  Copyright (C) 2010 - 2014 LSE Leading Security Experts GmbH\n#  License:  LSE\n#  contact:  http://www.linotp.org\n#            http://www.lsexperts.de\n#            linotp@lsexperts.de\n#\n# This code is free software; you can redistribute it and/or\n# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE\n# License as published by the Free Software Foundation; either\n# version 3 of the License, or any later version.\n#\n# This code is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU AFFERO GENERAL PUBLIC LICENSE for more details.\n#\n# You should have received a copy of the GNU Affero General Public\n# License along with this program.  If not, see <http://www.gnu.org/licenses/>.\n#\n__doc__ = \"\"\"This is the implementation of the remote token. The remote token\nforwards an authentication request to another privacyidea server.\n\nTo do this it uses the parameters remote.server, remote.realm,\nremote.resolver, remote.user or remote.serial.\nThe parameter remote.local_checkpin determines, whether the PIN should be\nchecked locally or remotely.\n\nThe code is tested in tests/test_lib_tokens_remote\n\"\"\"\n\nimport logging\nimport traceback\nimport requests\nfrom privacyidea.lib.utils import is_true\nfrom privacyidea.lib.decorators import check_token_locked\nfrom privacyidea.lib.config import get_from_config\nfrom privacyidea.api.lib.utils import getParam\nfrom privacyidea.lib.log import log_with\nfrom privacyidea.lib.policydecorators import challenge_response_allowed\nfrom privacyidea.lib.tokenclass import TokenClass, TOKENKIND\nfrom privacyidea.lib import _\nfrom privacyidea.lib.policy import SCOPE, ACTION, GROUP\n\noptional = True\nrequired = False\n\nlog = logging.getLogger(__name__)\n\n###############################################\n\n\nclass RemoteTokenClass(TokenClass):\n    \"\"\"\n    The Remote token forwards an authentication request to another privacyIDEA\n    server. The request can be forwarded to a user on the other server or to\n    a serial number on the other server. The PIN can be checked on the local\n    privacyIDEA server or on the remote server.\n\n    Using the Remote token you can assign one physical token to many\n    different users.\n    \"\"\"\n\n    def __init__(self, db_token):\n        \"\"\"\n        constructor - create a token class object with it's db token binding\n\n        :param aToken: the db bound token\n        \"\"\"\n        TokenClass.__init__(self, db_token)\n        self.set_type(u\"remote\")\n        self.mode = ['authenticate', 'challenge']\n\n    @staticmethod\n    def get_class_type():\n        \"\"\"\n        return the class type identifier\n        \"\"\"\n        return \"remote\"\n\n    @staticmethod\n    def get_class_prefix():\n        \"\"\"\n        return the token type prefix\n        \"\"\"\n        return \"PIRE\"\n\n    @staticmethod\n    @log_with(log)\n    def get_class_info(key=None, ret='all'):\n        \"\"\"\n        :param key: subsection identifier\n        :type key: string\n        :param ret: default return value, if nothing is found\n        :type ret: user defined\n        :return: subsection if key exists or user defined\n        :rtype: dict or string\n        \"\"\"\n        res = {'type': 'remote',\n               'title': 'Remote Token',\n               'description': _('Remote Token: Forward authentication request '\n                                'to another server.'),\n               'user': [],\n               # This tokentype is enrollable in the UI for...\n               'ui_enroll': [\"admin\"],\n               'policy': {\n                   SCOPE.ENROLL: {\n                       ACTION.MAXTOKENUSER: {\n                           'type': 'int',\n                           'desc': _(\"The user may only have this maximum number of remote tokens assigned.\"),\n                           'group': GROUP.TOKEN\n                       },\n                       ACTION.MAXACTIVETOKENUSER: {\n                           'type': 'int',\n                           'desc': _(\n                               \"The user may only have this maximum number of active remote tokens assigned.\"),\n                           'group': GROUP.TOKEN\n                       }\n                   }\n               },\n               }\n\n        if key:\n            ret = res.get(key, {})\n        else:\n            if ret == 'all':\n                ret = res\n        return ret\n\n    def update(self, param):\n        \"\"\"\n        second phase of the init process - updates parameters\n\n        :param param: the request parameters\n        :return: - nothing -\n        \"\"\"\n        # if another OTP length would be specified in /admin/init this would\n        # be overwritten by the parent class, which is ok.\n        self.set_otplen(6)\n        TokenClass.update(self, param)\n\n        remoteServer = getParam(param, \"remote.server\", required)\n        self.add_tokeninfo(\"remote.server\", remoteServer)\n\n        val = getParam(param, \"remote.local_checkpin\", optional) or 0\n        self.add_tokeninfo(\"remote.local_checkpin\", val)\n\n        for key in [\"remote.serial\", \"remote.user\", \"remote.path\",\n                    \"remote.realm\", \"remote.resolver\"]:\n            val = getParam(param, key, optional)\n            if val is not None:\n                self.add_tokeninfo(key, val)\n\n        self.add_tokeninfo(\"tokenkind\", TOKENKIND.VIRTUAL)\n\n    @property\n    def check_pin_local(self):\n        \"\"\"\n        lookup if pin should be checked locally or on remote host\n\n        :return: bool\n        \"\"\"\n        local_check = is_true(self.get_tokeninfo(\"remote.local_checkpin\"))\n        log.debug(\" local checking pin? {0!r}\".format(local_check))\n\n        return local_check\n\n    @log_with(log)\n    @check_token_locked\n    def authenticate(self, passw, user=None, options=None):\n        \"\"\"\n        do the authentication on base of password / otp and user and\n        options, the request parameters.\n\n        Here we contact the other privacyIDEA server to validate the OtpVal.\n\n        :param passw: the password / otp\n        :param user: the requesting user\n        :param options: the additional request parameters\n\n        :return: tuple of (success, otp_count - 0 or -1, reply)\n\n        \"\"\"\n        res = False\n        otp_counter = -1\n        reply = None\n        otpval = passw\n\n        # should we check the pin localy?\n        if self.check_pin_local:\n            (_res, pin, otpval) = self.split_pin_pass(passw, user,\n                                                      options=options)\n\n            if not TokenClass.check_pin(self, pin):\n                return False, otp_counter, {'message': \"Wrong PIN\"}\n\n        otp_count = self.check_otp(otpval, options=options)\n        if otp_count >= 0:\n            res = True\n            reply = {'message': 'matching 1 tokens',\n                     'serial': self.get_serial(),\n                     'type': self.get_tokentype()}\n        else:\n            reply = {'message': 'remote side denied access'}\n\n        return res, otp_count, reply\n\n    @check_token_locked\n    def check_otp(self, otpval, counter=None, window=None, options=None):\n        \"\"\"\n        run the http request against the remote host\n\n        :param otpval: the OTP value\n        :param counter: The counter for counter based otp values\n        :type counter: int\n        :param window: a counter window\n        :type counter: int\n        :param options: additional token specific options\n        :type options: dict\n        :return: counter of the matching OTP value.\n        :rtype: int\n        \"\"\"\n        otp_count = -1\n\n        remoteServer = self.get_tokeninfo(\"remote.server\") or \"\"\n\n        # in preparation of the ability to relocate privacyidea urls,\n        # we introduce the remote url path\n        remotePath = self.get_tokeninfo(\"remote.path\") or \"\"\n        remotePath = remotePath.strip()\n\n        remoteSerial = self.get_tokeninfo(\"remote.serial\") or \"\"\n\n        remoteUser = self.get_tokeninfo(\"remote.user\") or \"\"\n\n        remoteRealm = self.get_tokeninfo(\"remote.realm\") or \"\"\n\n        remoteResolver = self.get_tokeninfo(\"remote.resolver\") or \"\"\n\n        ssl_verify = get_from_config(\"remote.verify_ssl_certificate\",\n                                     False, return_bool=True) or False\n\n        # here we also need to check for remote.user and so on....\n        log.debug(\"checking OTP len:%r remotely on server: %r,\"\n                  \" serial: %r, user: %r\" %\n                  (len(otpval), remoteServer, remoteSerial, remoteUser))\n        params = {}\n\n        remotePath = remotePath or \"/validate/check\"\n        if remoteSerial:\n            params['serial'] = remoteSerial\n        elif remoteUser:\n            params['user'] = remoteUser\n            params['realm'] = remoteRealm\n            params['resolver'] = remoteResolver\n\n        else:\n            log.warning(\"The remote token does neither contain a \"\n                        \"remote.serial nor a remote.user.\")\n            return otp_count\n\n        params['pass'] = otpval\n        request_url = \"{0!s}{1!s}\".format(remoteServer, remotePath)\n\n        try:\n            r = requests.post(request_url, data=params, verify=ssl_verify)\n\n            if r.status_code == requests.codes.ok:\n                response = r.json()\n                result = response.get(\"result\")\n                if result.get(\"value\"):\n                    otp_count = 1\n\n        except Exception as exx:  # pragma: no cover\n            log.error(\"Error getting response from \"\n                      \"remote Server (%r): %r\" % (request_url, exx))\n            log.debug(\"{0!s}\".format(traceback.format_exc()))\n\n        return otp_count\n\n    @log_with(log)\n    @challenge_response_allowed\n    def is_challenge_request(self, passw, user=None, options=None):\n        \"\"\"\n        This method checks, if this is a request, that triggers a challenge.\n        It depends on the way, the pin is checked - either locally or remote\n\n        :param passw: password, which might be pin or pin+otp\n        :type passw: string\n        :param user: The user from the authentication request\n        :type user: User object\n        :param options: dictionary of additional request parameters\n        :type options: dict\n\n        :return: true or false\n        \"\"\"\n\n        request_is_valid = False\n\n        if self.check_pin_local:\n            pin_match = self.check_pin(passw, user=user,\n                                         options=options)\n            if pin_match is True:\n                request_is_valid = True\n\n        return request_is_valid\n", "path": "privacyidea/lib/tokens/remotetoken.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\n#\n#  privacyIDEA is a fork of LinOTP\n#  May 08, 2014 Cornelius K\u00f6lbel\n#  License:  AGPLv3\n#  contact:  http://www.privacyidea.org\n#\n#  2018-01-21 Cornelius K\u00f6lbel <cornelius.koelbel@netknights.it>\n#             Add tokenkind\n#  2015-01-28 Rewrite for migration to flask\n#             Cornelius K\u00f6lbel <cornelius@privacyidea.org>\n#\n#\n#  Copyright (C) 2010 - 2014 LSE Leading Security Experts GmbH\n#  License:  LSE\n#  contact:  http://www.linotp.org\n#            http://www.lsexperts.de\n#            linotp@lsexperts.de\n#\n# This code is free software; you can redistribute it and/or\n# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE\n# License as published by the Free Software Foundation; either\n# version 3 of the License, or any later version.\n#\n# This code is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU AFFERO GENERAL PUBLIC LICENSE for more details.\n#\n# You should have received a copy of the GNU Affero General Public\n# License along with this program.  If not, see <http://www.gnu.org/licenses/>.\n#\n__doc__ = \"\"\"This is the implementation of the remote token. The remote token\nforwards an authentication request to another privacyidea server.\n\nTo do this it uses the parameters remote.server, remote.realm,\nremote.resolver, remote.user or remote.serial.\nThe parameter remote.local_checkpin determines, whether the PIN should be\nchecked locally or remotely.\n\nThe code is tested in tests/test_lib_tokens_remote\n\"\"\"\n\nimport logging\nimport traceback\nimport requests\nfrom privacyidea.lib.utils import is_true\nfrom privacyidea.lib.decorators import check_token_locked\nfrom privacyidea.lib.config import get_from_config\nfrom privacyidea.api.lib.utils import getParam\nfrom privacyidea.lib.log import log_with\nfrom privacyidea.lib.policydecorators import challenge_response_allowed\nfrom privacyidea.lib.tokenclass import TokenClass, TOKENKIND\nfrom privacyidea.lib import _\nfrom privacyidea.lib.policy import SCOPE, ACTION, GROUP\n\noptional = True\nrequired = False\n\nlog = logging.getLogger(__name__)\n\n###############################################\n\n\nclass RemoteTokenClass(TokenClass):\n    \"\"\"\n    The Remote token forwards an authentication request to another privacyIDEA\n    server. The request can be forwarded to a user on the other server or to\n    a serial number on the other server. The PIN can be checked on the local\n    privacyIDEA server or on the remote server.\n\n    Using the Remote token you can assign one physical token to many\n    different users.\n    \"\"\"\n\n    def __init__(self, db_token):\n        \"\"\"\n        constructor - create a token class object with it's db token binding\n\n        :param aToken: the db bound token\n        \"\"\"\n        TokenClass.__init__(self, db_token)\n        self.set_type(u\"remote\")\n        self.mode = ['authenticate', 'challenge']\n\n    @staticmethod\n    def get_class_type():\n        \"\"\"\n        return the class type identifier\n        \"\"\"\n        return \"remote\"\n\n    @staticmethod\n    def get_class_prefix():\n        \"\"\"\n        return the token type prefix\n        \"\"\"\n        return \"PIRE\"\n\n    @staticmethod\n    @log_with(log)\n    def get_class_info(key=None, ret='all'):\n        \"\"\"\n        :param key: subsection identifier\n        :type key: string\n        :param ret: default return value, if nothing is found\n        :type ret: user defined\n        :return: subsection if key exists or user defined\n        :rtype: dict or string\n        \"\"\"\n        res = {'type': 'remote',\n               'title': 'Remote Token',\n               'description': _('Remote Token: Forward authentication request '\n                                'to another server.'),\n               'user': [],\n               # This tokentype is enrollable in the UI for...\n               'ui_enroll': [\"admin\"],\n               'policy': {\n                   SCOPE.ENROLL: {\n                       ACTION.MAXTOKENUSER: {\n                           'type': 'int',\n                           'desc': _(\"The user may only have this maximum number of remote tokens assigned.\"),\n                           'group': GROUP.TOKEN\n                       },\n                       ACTION.MAXACTIVETOKENUSER: {\n                           'type': 'int',\n                           'desc': _(\n                               \"The user may only have this maximum number of active remote tokens assigned.\"),\n                           'group': GROUP.TOKEN\n                       }\n                   }\n               },\n               }\n\n        if key:\n            ret = res.get(key, {})\n        else:\n            if ret == 'all':\n                ret = res\n        return ret\n\n    def update(self, param):\n        \"\"\"\n        second phase of the init process - updates parameters\n\n        :param param: the request parameters\n        :return: - nothing -\n        \"\"\"\n        # if another OTP length would be specified in /admin/init this would\n        # be overwritten by the parent class, which is ok.\n        self.set_otplen(6)\n        TokenClass.update(self, param)\n\n        remoteServer = getParam(param, \"remote.server\", required)\n        self.add_tokeninfo(\"remote.server\", remoteServer)\n\n        val = getParam(param, \"remote.local_checkpin\", optional) or 0\n        self.add_tokeninfo(\"remote.local_checkpin\", val)\n\n        for key in [\"remote.serial\", \"remote.user\", \"remote.path\",\n                    \"remote.realm\", \"remote.resolver\"]:\n            val = getParam(param, key, optional)\n            if val is not None:\n                self.add_tokeninfo(key, val)\n\n        self.add_tokeninfo(\"tokenkind\", TOKENKIND.VIRTUAL)\n\n    @property\n    def check_pin_local(self):\n        \"\"\"\n        lookup if pin should be checked locally or on remote host\n\n        :return: bool\n        \"\"\"\n        local_check = is_true(self.get_tokeninfo(\"remote.local_checkpin\"))\n        log.debug(\" local checking pin? {0!r}\".format(local_check))\n\n        return local_check\n\n    @log_with(log)\n    @check_token_locked\n    def authenticate(self, passw, user=None, options=None):\n        \"\"\"\n        do the authentication on base of password / otp and user and\n        options, the request parameters.\n\n        Here we contact the other privacyIDEA server to validate the OtpVal.\n\n        :param passw: the password / otp\n        :param user: the requesting user\n        :param options: the additional request parameters\n\n        :return: tuple of (success, otp_count - 0 or -1, reply)\n\n        \"\"\"\n        res = False\n        otp_counter = -1\n        reply = None\n        otpval = passw\n\n        # should we check the pin localy?\n        if self.check_pin_local:\n            (_res, pin, otpval) = self.split_pin_pass(passw, user,\n                                                      options=options)\n\n            if not TokenClass.check_pin(self, pin):\n                return False, otp_counter, {'message': \"Wrong PIN\"}\n\n        otp_count = self.check_otp(otpval, options=options)\n        if otp_count >= 0:\n            res = True\n            reply = {'message': 'matching 1 tokens',\n                     'serial': self.get_serial(),\n                     'type': self.get_tokentype()}\n        else:\n            reply = {'message': 'remote side denied access'}\n\n        return res, otp_count, reply\n\n    @check_token_locked\n    def check_otp(self, otpval, counter=None, window=None, options=None):\n        \"\"\"\n        run the http request against the remote host\n\n        :param otpval: the OTP value\n        :param counter: The counter for counter based otp values\n        :type counter: int\n        :param window: a counter window\n        :type counter: int\n        :param options: additional token specific options\n        :type options: dict\n        :return: counter of the matching OTP value.\n        :rtype: int\n        \"\"\"\n        otp_count = -1\n\n        remoteServer = self.get_tokeninfo(\"remote.server\") or \"\"\n\n        # in preparation of the ability to relocate privacyidea urls,\n        # we introduce the remote url path\n        remotePath = self.get_tokeninfo(\"remote.path\") or \"\"\n        remotePath = remotePath.strip()\n\n        remoteSerial = self.get_tokeninfo(\"remote.serial\") or \"\"\n\n        remoteUser = self.get_tokeninfo(\"remote.user\") or \"\"\n\n        remoteRealm = self.get_tokeninfo(\"remote.realm\") or \"\"\n\n        remoteResolver = self.get_tokeninfo(\"remote.resolver\") or \"\"\n\n        ssl_verify = get_from_config(\"remote.verify_ssl_certificate\",\n                                     False, return_bool=True) or False\n\n        # here we also need to check for remote.user and so on....\n        log.debug(\"checking OTP len:%r remotely on server: %r,\"\n                  \" serial: %r, user: %r\" %\n                  (len(otpval), remoteServer, remoteSerial, remoteUser))\n        params = {}\n\n        remotePath = remotePath or \"/validate/check\"\n        if remoteSerial:\n            params['serial'] = remoteSerial\n        elif remoteUser:\n            params['user'] = remoteUser\n            params['realm'] = remoteRealm\n            params['resolver'] = remoteResolver\n\n        else:\n            log.warning(\"The remote token does neither contain a \"\n                        \"remote.serial nor a remote.user.\")\n            return otp_count\n\n        params['pass'] = otpval\n        request_url = \"{0!s}{1!s}\".format(remoteServer, remotePath)\n\n        try:\n            r = requests.post(request_url, data=params, verify=ssl_verify)\n\n            if r.status_code == requests.codes.ok:\n                response = r.json()\n                result = response.get(\"result\")\n                if result.get(\"value\"):\n                    otp_count = 1\n                    # Add the serial of the used remote token in the tokeninfo parameters\n                    self.add_tokeninfo(\"last_matching_remote_serial\", response.get(\"detail\", {}).get(\"serial\"))\n\n        except Exception as exx:  # pragma: no cover\n            log.error(\"Error getting response from \"\n                      \"remote Server (%r): %r\" % (request_url, exx))\n            log.debug(\"{0!s}\".format(traceback.format_exc()))\n\n        return otp_count\n\n    @log_with(log)\n    @challenge_response_allowed\n    def is_challenge_request(self, passw, user=None, options=None):\n        \"\"\"\n        This method checks, if this is a request, that triggers a challenge.\n        It depends on the way, the pin is checked - either locally or remote\n\n        :param passw: password, which might be pin or pin+otp\n        :type passw: string\n        :param user: The user from the authentication request\n        :type user: User object\n        :param options: dictionary of additional request parameters\n        :type options: dict\n\n        :return: true or false\n        \"\"\"\n\n        request_is_valid = False\n\n        if self.check_pin_local:\n            pin_match = self.check_pin(passw, user=user,\n                                         options=options)\n            if pin_match is True:\n                request_is_valid = True\n\n        return request_is_valid\n", "path": "privacyidea/lib/tokens/remotetoken.py"}]}