diff --git a/README.md b/README.md index 7868f87..e3e901e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![Build Status](https://travis-ci.org/privacyidea/pam_python.svg?branch=master)](https://travis-ci.org/privacyidea/pam_python) This module is to be used with http://pam-python.sourceforge.net/. -It can be used to authenticate with OTP against privacyIDEA. It will also +It can be used to authenticate with OTP against privacyIDEA. It will also cache future OTP values to enable offline authentication. To be used like this:: @@ -10,28 +10,32 @@ To be used like this:: It can take the following parameters: -**url=https://your-server** +**url=https://your-server** default is https://localhost - + **debug** write debug information to the system log - + **realm=yourRealm** pass additional realm to privacyidea - + **nosslverify** Do not verify the SSL certificate - + **prompt=** The password prompt. Default is "Your OTP". - + +**api_token=** + + The API Token to access admin REST API for auto-enrollment. + **sqlfile=** - This is the SQLite file that is used to store the offline authentication + This is the SQLite file that is used to store the offline authentication information. The default file is /etc/privacyidea/pam.sqlite diff --git a/privacyidea_pam.py b/privacyidea_pam.py index f132796..b86d09f 100644 --- a/privacyidea_pam.py +++ b/privacyidea_pam.py @@ -80,11 +80,15 @@ def __init__(self, pamh, config): self.sslverify = cacerts self.realm = config.get("realm") self.debug = config.get("debug") + self.api_token = config.get("api_token") self.sqlfile = config.get("sqlfile", "/etc/privacyidea/pam.sqlite") - def make_request(self, data, endpoint="/validate/check"): + def make_request(self, data, endpoint="/validate/check", token=None): # add a user-agent to be displayed in the Client Application Type headers = {'user-agent': 'PAM/2.15.0'} + if token: + headers["Authorization"] = token + response = requests.post(self.URL + endpoint, data=data, headers=headers, verify=self.sslverify) @@ -95,6 +99,41 @@ def make_request(self, data, endpoint="/validate/check"): return json_response + def enroll_user(self, user, pin): + # Generate a new email Token with the provided pin + syslog.syslog(syslog.LOG_DEBUG, + "%s: %s" % (__name__, "Generating a new token")) + + data = {"user": self.user, + "genkey": "1", + "pin": pin, + "type": "email", + "dynamic_email": 1} + + if self.realm: + data["realm"] = self.realm + json_response = self.make_request(data, endpoint="/token/init", token=self.api_token) + + result = json_response.get("result") + detail = json_response.get("detail") + + if self.debug: + syslog.syslog(syslog.LOG_DEBUG, + "%s: result: %s" % (__name__, result)) + syslog.syslog(syslog.LOG_DEBUG, + "%s: detail: %s" % (__name__, detail)) + if result.get("status"): + if result.get("value"): + message = self.pamh.Message(self.pamh.PAM_PROMPT_ECHO_OFF, "Please re-enter your PIN: ") + response = self.pamh.conversation(message) + self.pamh.authtok = response.resp + return self.authenticate(self.pamh.authtok) + else: + syslog.syslog(syslog.LOG_ERR, + "%s: %s" % (__name__, + result.get("error").get("message"))) + return self.pamh.PAM_AUTH_ERR + def offline_refill(self, serial, password): # get refilltoken @@ -182,10 +221,10 @@ def authenticate(self, password): auth_item) else: transaction_id = detail.get("transaction_id") + message = detail.get("message").encode("utf-8") if transaction_id: attributes = detail.get("attributes") or {} - message = detail.get("message").encode("utf-8") if "u2fSignRequest" in attributes: rval = self.u2f_challenge_response( transaction_id, message, @@ -195,11 +234,28 @@ def authenticate(self, password): message, attributes) else: - rval = self.pamh.PAM_AUTH_ERR + if message == 'The user has no tokens assigned': + syslog.syslog(syslog.LOG_DEBUG, + "%s: detail: %s" % (__name__, len(password))) + if len(password)<4: + pam_message = self.pamh.Message(self.pamh.PAM_ERROR_MSG, "You must choose a 4-character minimum PIN.") + self.pamh.conversation(pam_message) + rval = self.pamh.PAM_AUTH_ERR + else: + return self.enroll_user(self.user, password) + + else: + syslog.syslog(syslog.LOG_ERR, + "%s: %s" % (__name__, message)) + pam_message = self.pamh.Message(self.pamh.PAM_ERROR_MSG, message) + self.pamh.conversation(pam_message) + rval = self.pamh.PAM_AUTH_ERR else: + error_msg = result.get("error").get("message") syslog.syslog(syslog.LOG_ERR, - "%s: %s" % (__name__, - result.get("error").get("message"))) + "%s: %s" % (__name__, error_msg)) + pam_message = self.pamh.Message(self.pamh.PAM_ERROR_MSG, str(error_msg)) + self.pamh.conversation(pam_message) return rval @@ -486,4 +542,3 @@ def _create_table(c): c.execute("CREATE TABLE refilltokens (serial text, refilltoken text)") except sqlite3.OperationalError: pass -