Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion docs/source/public_server.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ configuring the :attr:`NotebookApp.password` setting in

Prerequisite: A notebook configuration file
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Check to see if you have a notebook configuration file,
:file:`jupyter_notebook_config.py`. The default location for this file
is your Jupyter folder in your home directory, ``~/.jupyter``.
Expand All @@ -66,7 +67,20 @@ using the following command::

Preparing a hashed password
~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can prepare a hashed password using the function

As of notebook version 5.0, you can enter and store a password for your
notebook server with a single command.
:command:`jupyter notebook password` will prompt you for your password
and record the hashed password in your :file:`jupyter_notebook_config.json`.

.. code-block:: bash

$ jupyter notebook password
Enter password: ****
Verify password: ****
[NotebookPasswordApp] Wrote hashed password to /Users/you/.jupyter/jupyter_notebook_config.json

You can prepare a hashed password manually, using the function
:func:`notebook.auth.security.passwd`:

.. code-block:: ipython
Expand Down
14 changes: 13 additions & 1 deletion docs/source/security.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,20 @@ Once you have visited this URL,
a cookie will be set in your browser and you won't need to use the token again,
unless you switch browsers, clear your cookies, or start a notebook server on a new port.

Alternatives to token authentication
------------------------------------

You can disable authentication altogether by setting the token and password to empty strings,
If a generated token doesn't work well for you,
you can set a password for your notebook.
:command:`jupyter notebook password` will prompt you for a password,
and store the hashed password in your :file:`jupyter_notebook_config.json`.

.. versionadded:: 5.0

:command:`jupyter notebook password` command is added.


It is possible disable authentication altogether by setting the token and password to empty strings,
but this is **NOT RECOMMENDED**, unless authentication or access restrictions are handled at a different layer in your web application:

.. sourcecode:: python
Expand Down
49 changes: 48 additions & 1 deletion notebook/auth/security.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
"""
Password generation for the Notebook.
"""

from contextlib import contextmanager
import getpass
import hashlib
import io
import json
import os
import random
import traceback
import warnings

from ipython_genutils.py3compat import cast_bytes, str_to_bytes
from ipython_genutils.py3compat import cast_bytes, str_to_bytes, cast_unicode
from traitlets.config import Config, ConfigFileNotFound, JSONFileConfigLoader
from jupyter_core.paths import jupyter_config_dir

# Length of the salt in nr of hex chars, which implies salt_len * 4
# bits of randomness.
Expand Down Expand Up @@ -99,3 +108,41 @@ def passwd_check(hashed_passphrase, passphrase):
h.update(cast_bytes(passphrase, 'utf-8') + cast_bytes(salt, 'ascii'))

return h.hexdigest() == pw_digest

@contextmanager
def persist_config(config_file=None, mode=0o600):
"""Context manager that can be used to modify a config object

On exit of the context manager, the config will be written back to disk,
by default with user-only (600) permissions.
"""

if config_file is None:
config_file = os.path.join(jupyter_config_dir(), 'jupyter_notebook_config.json')

loader = JSONFileConfigLoader(os.path.basename(config_file), os.path.dirname(config_file))
try:
config = loader.load_config()
except ConfigFileNotFound:
config = Config()

yield config

with io.open(config_file, 'w', encoding='utf8') as f:
f.write(cast_unicode(json.dumps(config, indent=2)))

try:
os.chmod(config_file, mode)
except Exception as e:
tb = traceback.format_exc()
warnings.warn("Failed to set permissions on %s:\n%s" % (config_file, tb),
RuntimeWarning)


def set_password(password=None, config_file=None):
"""Ask user for password, store it in notebook json configuration file"""

hashed_password = passwd(password)

with persist_config(config_file) as config:
config.NotebookApp.password = hashed_password
20 changes: 20 additions & 0 deletions notebook/notebookapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
_examples = """
jupyter notebook # start the notebook
jupyter notebook --certfile=mycert.pem # use SSL/TLS certificate
jupyter notebook password # enter a password to protect the server
"""

DEV_NOTE_NPM = """It looks like you're running the notebook from source.
Expand Down Expand Up @@ -323,6 +324,24 @@ def init_handlers(self, settings):
return new_handlers


class NotebookPasswordApp(JupyterApp):
"""Set a password for the notebook server.

Setting a password secures the notebook server
and removes the need for token-based authentication.
"""

description = __doc__

def _config_file_default(self):
return os.path.join(self.config_dir, 'jupyter_notebook_config.json')

def start(self):
from .auth.security import set_password
set_password(config_file=self.config_file)
self.log.info("Wrote hashed password to %s" % self.config_file)


class NbserverListApp(JupyterApp):
version = __version__
description="List currently running notebook servers."
Expand Down Expand Up @@ -426,6 +445,7 @@ class NotebookApp(JupyterApp):

subcommands = dict(
list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
password=(NotebookPasswordApp, NotebookPasswordApp.description.splitlines()[0]),
)

_log_formatter_cls = LogFormatter
Expand Down
25 changes: 23 additions & 2 deletions notebook/tests/test_notebookapp.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
"""Test NotebookApp"""


import getpass
import logging
import os
import re
from subprocess import Popen, PIPE, STDOUT
import sys
from tempfile import NamedTemporaryFile

try:
from unittest.mock import patch
except ImportError:
from mock import patch # py2

import nose.tools as nt

from traitlets.tests.utils import check_help_all_output
Expand All @@ -14,7 +21,7 @@
from ipython_genutils.tempdir import TemporaryDirectory
from traitlets import TraitError
from notebook import notebookapp, __version__
from notebook import notebookapp
from notebook.auth.security import passwd_check
NotebookApp = notebookapp.NotebookApp


Expand Down Expand Up @@ -117,3 +124,17 @@ def raise_on_bad_version(version):

def test_current_version():
raise_on_bad_version(__version__)

def test_notebook_password():
password = 'secret'
with TemporaryDirectory() as td:
with patch.dict('os.environ', {
'JUPYTER_CONFIG_DIR': td,
}), patch.object(getpass, 'getpass', return_value=password):
app = notebookapp.NotebookPasswordApp(log_level=logging.ERROR)
app.initialize([])
app.start()
nb = NotebookApp()
nb.load_config_file()
nt.assert_not_equal(nb.password, '')
passwd_check(nb.password, password)