We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
PAM/AD auth doesn't create homedir on first login
So from what i can see in auth.py, the 'login' pam service is used to authenticate the user. For some reason I don't think the 'session' part of this service is honoured. I've tested this by using pamtester with the login service (see below) and the home directory is created.

pamtester login <username> open_session close_session

Any idea why this may be? I think it has something to do with the simplepam python module functionality but i'm not certain.

any help is appreciated. Cheers


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `jupyterhub/auth.py`
Content:
```
1 """Simple PAM authenticator"""
2 
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
5 
6 from grp import getgrnam
7 import pwd
8 from subprocess import check_call, check_output, CalledProcessError
9 
10 from tornado import gen
11 import simplepam
12 
13 from traitlets.config import LoggingConfigurable
14 from traitlets import Bool, Set, Unicode, Any
15 
16 from .handlers.login import LoginHandler
17 from .utils import url_path_join
18 
19 class Authenticator(LoggingConfigurable):
20     """A class for authentication.
21     
22     The API is one method, `authenticate`, a tornado gen.coroutine.
23     """
24     
25     db = Any()
26     admin_users = Set(config=True,
27         help="""set of usernames of admin users
28 
29         If unspecified, only the user that launches the server will be admin.
30         """
31     )
32     whitelist = Set(config=True,
33         help="""Username whitelist.
34         
35         Use this to restrict which users can login.
36         If empty, allow any user to attempt login.
37         """
38     )
39     custom_html = Unicode('',
40         help="""HTML login form for custom handlers.
41         Override in form-based custom authenticators
42         that don't use username+password,
43         or need custom branding.
44         """
45     )
46     login_service = Unicode('',
47         help="""Name of the login service for external
48         login services (e.g. 'GitHub').
49         """
50     )
51     
52     @gen.coroutine
53     def authenticate(self, handler, data):
54         """Authenticate a user with login form data.
55         
56         This must be a tornado gen.coroutine.
57         It must return the username on successful authentication,
58         and return None on failed authentication.
59         """
60 
61     def check_whitelist(self, user):
62         """
63         Return True if the whitelist is empty or user is in the whitelist.
64         """
65         # Parens aren't necessary here, but they make this easier to parse.
66         return (not self.whitelist) or (user in self.whitelist)
67 
68     def add_user(self, user):
69         """Add a new user
70         
71         By default, this just adds the user to the whitelist.
72         
73         Subclasses may do more extensive things,
74         such as adding actual unix users.
75         """
76         if self.whitelist:
77             self.whitelist.add(user.name)
78     
79     def delete_user(self, user):
80         """Triggered when a user is deleted.
81         
82         Removes the user from the whitelist.
83         """
84         self.whitelist.discard(user.name)
85     
86     def login_url(self, base_url):
87         """Override to register a custom login handler"""
88         return url_path_join(base_url, 'login')
89     
90     def logout_url(self, base_url):
91         """Override to register a custom logout handler"""
92         return url_path_join(base_url, 'logout')
93     
94     def get_handlers(self, app):
95         """Return any custom handlers the authenticator needs to register
96         
97         (e.g. for OAuth)
98         """
99         return [
100             ('/login', LoginHandler),
101         ]
102 
103 class LocalAuthenticator(Authenticator):
104     """Base class for Authenticators that work with local *ix users
105     
106     Checks for local users, and can attempt to create them if they exist.
107     """
108     
109     create_system_users = Bool(False, config=True,
110         help="""If a user is added that doesn't exist on the system,
111         should I try to create the system user?
112         """
113     )
114 
115     group_whitelist = Set(
116         config=True,
117         help="Automatically whitelist anyone in this group.",
118     )
119 
120     def _group_whitelist_changed(self, name, old, new):
121         if self.whitelist:
122             self.log.warn(
123                 "Ignoring username whitelist because group whitelist supplied!"
124             )
125 
126     def check_whitelist(self, username):
127         if self.group_whitelist:
128             return self.check_group_whitelist(username)
129         else:
130             return super().check_whitelist(username)
131 
132     def check_group_whitelist(self, username):
133         if not self.group_whitelist:
134             return False
135         for grnam in self.group_whitelist:
136             try:
137                 group = getgrnam(grnam)
138             except KeyError:
139                 self.log.error('No such group: [%s]' % grnam)
140                 continue
141             if username in group.gr_mem:
142                 return True
143         return False
144 
145     @gen.coroutine
146     def add_user(self, user):
147         """Add a new user
148         
149         By default, this just adds the user to the whitelist.
150         
151         Subclasses may do more extensive things,
152         such as adding actual unix users.
153         """
154         user_exists = yield gen.maybe_future(self.system_user_exists(user))
155         if not user_exists:
156             if self.create_system_users:
157                 yield gen.maybe_future(self.add_system_user(user))
158             else:
159                 raise KeyError("User %s does not exist." % user.name)
160         
161         yield gen.maybe_future(super().add_user(user))
162     
163     @staticmethod
164     def system_user_exists(user):
165         """Check if the user exists on the system"""
166         try:
167             pwd.getpwnam(user.name)
168         except KeyError:
169             return False
170         else:
171             return True
172     
173     @staticmethod
174     def add_system_user(user):
175         """Create a new *ix user on the system. Works on FreeBSD and Linux, at least."""
176         name = user.name
177         for useradd in (
178             ['pw', 'useradd', '-m'],
179             ['useradd', '-m'],
180         ):
181             try:
182                 check_output(['which', useradd[0]])
183             except CalledProcessError:
184                 continue
185             else:
186                 break
187         else:
188             raise RuntimeError("I don't know how to add users on this system.")
189     
190         check_call(useradd + [name])
191 
192 
193 class PAMAuthenticator(LocalAuthenticator):
194     """Authenticate local *ix users with PAM"""
195     encoding = Unicode('utf8', config=True,
196         help="""The encoding to use for PAM"""
197     )
198     service = Unicode('login', config=True,
199         help="""The PAM service to use for authentication."""
200     )
201     
202     @gen.coroutine
203     def authenticate(self, handler, data):
204         """Authenticate with PAM, and return the username if login is successful.
205     
206         Return None otherwise.
207         """
208         username = data['username']
209         if not self.check_whitelist(username):
210             return
211         # simplepam wants bytes, not unicode
212         # see simplepam#3
213         busername = username.encode(self.encoding)
214         bpassword = data['password'].encode(self.encoding)
215         if simplepam.authenticate(busername, bpassword, service=self.service):
216             return username
217     
218
```
--- 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/jupyterhub/auth.py b/jupyterhub/auth.py
--- a/jupyterhub/auth.py
+++ b/jupyterhub/auth.py
@@ -8,7 +8,7 @@
 from subprocess import check_call, check_output, CalledProcessError
 
 from tornado import gen
-import simplepam
+import pamela
 
 from traitlets.config import LoggingConfigurable
 from traitlets import Bool, Set, Unicode, Any
@@ -208,10 +208,11 @@
         username = data['username']
         if not self.check_whitelist(username):
             return
-        # simplepam wants bytes, not unicode
-        # see simplepam#3
-        busername = username.encode(self.encoding)
-        bpassword = data['password'].encode(self.encoding)
-        if simplepam.authenticate(busername, bpassword, service=self.service):
+        try:
+            pamela.authenticate(username, data['password'], service=self.service)
+            pamela.open_session(username, service=self.service)
+        except pamela.PAMError as e:
+            self.log.warn("PAM Authentication failed: %s", e)
+        else:
             return username

{"golden_diff": "diff --git a/jupyterhub/auth.py b/jupyterhub/auth.py\n--- a/jupyterhub/auth.py\n+++ b/jupyterhub/auth.py\n@@ -8,7 +8,7 @@\n from subprocess import check_call, check_output, CalledProcessError\n \n from tornado import gen\n-import simplepam\n+import pamela\n \n from traitlets.config import LoggingConfigurable\n from traitlets import Bool, Set, Unicode, Any\n@@ -208,10 +208,11 @@\n         username = data['username']\n         if not self.check_whitelist(username):\n             return\n-        # simplepam wants bytes, not unicode\n-        # see simplepam#3\n-        busername = username.encode(self.encoding)\n-        bpassword = data['password'].encode(self.encoding)\n-        if simplepam.authenticate(busername, bpassword, service=self.service):\n+        try:\n+            pamela.authenticate(username, data['password'], service=self.service)\n+            pamela.open_session(username, service=self.service)\n+        except pamela.PAMError as e:\n+            self.log.warn(\"PAM Authentication failed: %s\", e)\n+        else:\n             return username\n", "issue": "PAM/AD auth doesn't create homedir on first login\nSo from what i can see in auth.py, the 'login' pam service is used to authenticate the user. For some reason I don't think the 'session' part of this service is honoured. I've tested this by using pamtester with the login service (see below) and the home directory is created.\n\npamtester login <username> open_session close_session\n\nAny idea why this may be? I think it has something to do with the simplepam python module functionality but i'm not certain.\n\nany help is appreciated. Cheers\n\n", "before_files": [{"content": "\"\"\"Simple PAM authenticator\"\"\"\n\n# Copyright (c) IPython Development Team.\n# Distributed under the terms of the Modified BSD License.\n\nfrom grp import getgrnam\nimport pwd\nfrom subprocess import check_call, check_output, CalledProcessError\n\nfrom tornado import gen\nimport simplepam\n\nfrom traitlets.config import LoggingConfigurable\nfrom traitlets import Bool, Set, Unicode, Any\n\nfrom .handlers.login import LoginHandler\nfrom .utils import url_path_join\n\nclass Authenticator(LoggingConfigurable):\n    \"\"\"A class for authentication.\n    \n    The API is one method, `authenticate`, a tornado gen.coroutine.\n    \"\"\"\n    \n    db = Any()\n    admin_users = Set(config=True,\n        help=\"\"\"set of usernames of admin users\n\n        If unspecified, only the user that launches the server will be admin.\n        \"\"\"\n    )\n    whitelist = Set(config=True,\n        help=\"\"\"Username whitelist.\n        \n        Use this to restrict which users can login.\n        If empty, allow any user to attempt login.\n        \"\"\"\n    )\n    custom_html = Unicode('',\n        help=\"\"\"HTML login form for custom handlers.\n        Override in form-based custom authenticators\n        that don't use username+password,\n        or need custom branding.\n        \"\"\"\n    )\n    login_service = Unicode('',\n        help=\"\"\"Name of the login service for external\n        login services (e.g. 'GitHub').\n        \"\"\"\n    )\n    \n    @gen.coroutine\n    def authenticate(self, handler, data):\n        \"\"\"Authenticate a user with login form data.\n        \n        This must be a tornado gen.coroutine.\n        It must return the username on successful authentication,\n        and return None on failed authentication.\n        \"\"\"\n\n    def check_whitelist(self, user):\n        \"\"\"\n        Return True if the whitelist is empty or user is in the whitelist.\n        \"\"\"\n        # Parens aren't necessary here, but they make this easier to parse.\n        return (not self.whitelist) or (user in self.whitelist)\n\n    def add_user(self, user):\n        \"\"\"Add a new user\n        \n        By default, this just adds the user to the whitelist.\n        \n        Subclasses may do more extensive things,\n        such as adding actual unix users.\n        \"\"\"\n        if self.whitelist:\n            self.whitelist.add(user.name)\n    \n    def delete_user(self, user):\n        \"\"\"Triggered when a user is deleted.\n        \n        Removes the user from the whitelist.\n        \"\"\"\n        self.whitelist.discard(user.name)\n    \n    def login_url(self, base_url):\n        \"\"\"Override to register a custom login handler\"\"\"\n        return url_path_join(base_url, 'login')\n    \n    def logout_url(self, base_url):\n        \"\"\"Override to register a custom logout handler\"\"\"\n        return url_path_join(base_url, 'logout')\n    \n    def get_handlers(self, app):\n        \"\"\"Return any custom handlers the authenticator needs to register\n        \n        (e.g. for OAuth)\n        \"\"\"\n        return [\n            ('/login', LoginHandler),\n        ]\n\nclass LocalAuthenticator(Authenticator):\n    \"\"\"Base class for Authenticators that work with local *ix users\n    \n    Checks for local users, and can attempt to create them if they exist.\n    \"\"\"\n    \n    create_system_users = Bool(False, config=True,\n        help=\"\"\"If a user is added that doesn't exist on the system,\n        should I try to create the system user?\n        \"\"\"\n    )\n\n    group_whitelist = Set(\n        config=True,\n        help=\"Automatically whitelist anyone in this group.\",\n    )\n\n    def _group_whitelist_changed(self, name, old, new):\n        if self.whitelist:\n            self.log.warn(\n                \"Ignoring username whitelist because group whitelist supplied!\"\n            )\n\n    def check_whitelist(self, username):\n        if self.group_whitelist:\n            return self.check_group_whitelist(username)\n        else:\n            return super().check_whitelist(username)\n\n    def check_group_whitelist(self, username):\n        if not self.group_whitelist:\n            return False\n        for grnam in self.group_whitelist:\n            try:\n                group = getgrnam(grnam)\n            except KeyError:\n                self.log.error('No such group: [%s]' % grnam)\n                continue\n            if username in group.gr_mem:\n                return True\n        return False\n\n    @gen.coroutine\n    def add_user(self, user):\n        \"\"\"Add a new user\n        \n        By default, this just adds the user to the whitelist.\n        \n        Subclasses may do more extensive things,\n        such as adding actual unix users.\n        \"\"\"\n        user_exists = yield gen.maybe_future(self.system_user_exists(user))\n        if not user_exists:\n            if self.create_system_users:\n                yield gen.maybe_future(self.add_system_user(user))\n            else:\n                raise KeyError(\"User %s does not exist.\" % user.name)\n        \n        yield gen.maybe_future(super().add_user(user))\n    \n    @staticmethod\n    def system_user_exists(user):\n        \"\"\"Check if the user exists on the system\"\"\"\n        try:\n            pwd.getpwnam(user.name)\n        except KeyError:\n            return False\n        else:\n            return True\n    \n    @staticmethod\n    def add_system_user(user):\n        \"\"\"Create a new *ix user on the system. Works on FreeBSD and Linux, at least.\"\"\"\n        name = user.name\n        for useradd in (\n            ['pw', 'useradd', '-m'],\n            ['useradd', '-m'],\n        ):\n            try:\n                check_output(['which', useradd[0]])\n            except CalledProcessError:\n                continue\n            else:\n                break\n        else:\n            raise RuntimeError(\"I don't know how to add users on this system.\")\n    \n        check_call(useradd + [name])\n\n\nclass PAMAuthenticator(LocalAuthenticator):\n    \"\"\"Authenticate local *ix users with PAM\"\"\"\n    encoding = Unicode('utf8', config=True,\n        help=\"\"\"The encoding to use for PAM\"\"\"\n    )\n    service = Unicode('login', config=True,\n        help=\"\"\"The PAM service to use for authentication.\"\"\"\n    )\n    \n    @gen.coroutine\n    def authenticate(self, handler, data):\n        \"\"\"Authenticate with PAM, and return the username if login is successful.\n    \n        Return None otherwise.\n        \"\"\"\n        username = data['username']\n        if not self.check_whitelist(username):\n            return\n        # simplepam wants bytes, not unicode\n        # see simplepam#3\n        busername = username.encode(self.encoding)\n        bpassword = data['password'].encode(self.encoding)\n        if simplepam.authenticate(busername, bpassword, service=self.service):\n            return username\n    \n", "path": "jupyterhub/auth.py"}], "after_files": [{"content": "\"\"\"Simple PAM authenticator\"\"\"\n\n# Copyright (c) IPython Development Team.\n# Distributed under the terms of the Modified BSD License.\n\nfrom grp import getgrnam\nimport pwd\nfrom subprocess import check_call, check_output, CalledProcessError\n\nfrom tornado import gen\nimport pamela\n\nfrom traitlets.config import LoggingConfigurable\nfrom traitlets import Bool, Set, Unicode, Any\n\nfrom .handlers.login import LoginHandler\nfrom .utils import url_path_join\n\nclass Authenticator(LoggingConfigurable):\n    \"\"\"A class for authentication.\n    \n    The API is one method, `authenticate`, a tornado gen.coroutine.\n    \"\"\"\n    \n    db = Any()\n    admin_users = Set(config=True,\n        help=\"\"\"set of usernames of admin users\n\n        If unspecified, only the user that launches the server will be admin.\n        \"\"\"\n    )\n    whitelist = Set(config=True,\n        help=\"\"\"Username whitelist.\n        \n        Use this to restrict which users can login.\n        If empty, allow any user to attempt login.\n        \"\"\"\n    )\n    custom_html = Unicode('',\n        help=\"\"\"HTML login form for custom handlers.\n        Override in form-based custom authenticators\n        that don't use username+password,\n        or need custom branding.\n        \"\"\"\n    )\n    login_service = Unicode('',\n        help=\"\"\"Name of the login service for external\n        login services (e.g. 'GitHub').\n        \"\"\"\n    )\n    \n    @gen.coroutine\n    def authenticate(self, handler, data):\n        \"\"\"Authenticate a user with login form data.\n        \n        This must be a tornado gen.coroutine.\n        It must return the username on successful authentication,\n        and return None on failed authentication.\n        \"\"\"\n\n    def check_whitelist(self, user):\n        \"\"\"\n        Return True if the whitelist is empty or user is in the whitelist.\n        \"\"\"\n        # Parens aren't necessary here, but they make this easier to parse.\n        return (not self.whitelist) or (user in self.whitelist)\n\n    def add_user(self, user):\n        \"\"\"Add a new user\n        \n        By default, this just adds the user to the whitelist.\n        \n        Subclasses may do more extensive things,\n        such as adding actual unix users.\n        \"\"\"\n        if self.whitelist:\n            self.whitelist.add(user.name)\n    \n    def delete_user(self, user):\n        \"\"\"Triggered when a user is deleted.\n        \n        Removes the user from the whitelist.\n        \"\"\"\n        self.whitelist.discard(user.name)\n    \n    def login_url(self, base_url):\n        \"\"\"Override to register a custom login handler\"\"\"\n        return url_path_join(base_url, 'login')\n    \n    def logout_url(self, base_url):\n        \"\"\"Override to register a custom logout handler\"\"\"\n        return url_path_join(base_url, 'logout')\n    \n    def get_handlers(self, app):\n        \"\"\"Return any custom handlers the authenticator needs to register\n        \n        (e.g. for OAuth)\n        \"\"\"\n        return [\n            ('/login', LoginHandler),\n        ]\n\nclass LocalAuthenticator(Authenticator):\n    \"\"\"Base class for Authenticators that work with local *ix users\n    \n    Checks for local users, and can attempt to create them if they exist.\n    \"\"\"\n    \n    create_system_users = Bool(False, config=True,\n        help=\"\"\"If a user is added that doesn't exist on the system,\n        should I try to create the system user?\n        \"\"\"\n    )\n\n    group_whitelist = Set(\n        config=True,\n        help=\"Automatically whitelist anyone in this group.\",\n    )\n\n    def _group_whitelist_changed(self, name, old, new):\n        if self.whitelist:\n            self.log.warn(\n                \"Ignoring username whitelist because group whitelist supplied!\"\n            )\n\n    def check_whitelist(self, username):\n        if self.group_whitelist:\n            return self.check_group_whitelist(username)\n        else:\n            return super().check_whitelist(username)\n\n    def check_group_whitelist(self, username):\n        if not self.group_whitelist:\n            return False\n        for grnam in self.group_whitelist:\n            try:\n                group = getgrnam(grnam)\n            except KeyError:\n                self.log.error('No such group: [%s]' % grnam)\n                continue\n            if username in group.gr_mem:\n                return True\n        return False\n\n    @gen.coroutine\n    def add_user(self, user):\n        \"\"\"Add a new user\n        \n        By default, this just adds the user to the whitelist.\n        \n        Subclasses may do more extensive things,\n        such as adding actual unix users.\n        \"\"\"\n        user_exists = yield gen.maybe_future(self.system_user_exists(user))\n        if not user_exists:\n            if self.create_system_users:\n                yield gen.maybe_future(self.add_system_user(user))\n            else:\n                raise KeyError(\"User %s does not exist.\" % user.name)\n        \n        yield gen.maybe_future(super().add_user(user))\n    \n    @staticmethod\n    def system_user_exists(user):\n        \"\"\"Check if the user exists on the system\"\"\"\n        try:\n            pwd.getpwnam(user.name)\n        except KeyError:\n            return False\n        else:\n            return True\n    \n    @staticmethod\n    def add_system_user(user):\n        \"\"\"Create a new *ix user on the system. Works on FreeBSD and Linux, at least.\"\"\"\n        name = user.name\n        for useradd in (\n            ['pw', 'useradd', '-m'],\n            ['useradd', '-m'],\n        ):\n            try:\n                check_output(['which', useradd[0]])\n            except CalledProcessError:\n                continue\n            else:\n                break\n        else:\n            raise RuntimeError(\"I don't know how to add users on this system.\")\n    \n        check_call(useradd + [name])\n\n\nclass PAMAuthenticator(LocalAuthenticator):\n    \"\"\"Authenticate local *ix users with PAM\"\"\"\n    encoding = Unicode('utf8', config=True,\n        help=\"\"\"The encoding to use for PAM\"\"\"\n    )\n    service = Unicode('login', config=True,\n        help=\"\"\"The PAM service to use for authentication.\"\"\"\n    )\n    \n    @gen.coroutine\n    def authenticate(self, handler, data):\n        \"\"\"Authenticate with PAM, and return the username if login is successful.\n    \n        Return None otherwise.\n        \"\"\"\n        username = data['username']\n        if not self.check_whitelist(username):\n            return\n        try:\n            pamela.authenticate(username, data['password'], service=self.service)\n            pamela.open_session(username, service=self.service)\n        except pamela.PAMError as e:\n            self.log.warn(\"PAM Authentication failed: %s\", e)\n        else:\n            return username\n    \n", "path": "jupyterhub/auth.py"}]}