├── MANIFEST.in ├── .flake8 ├── jupyterhub_config.py ├── .github ├── dependabot.yaml └── workflows │ ├── release.yaml │ └── test.yaml ├── setup.py ├── tests ├── conftest.py └── test_tmpauthenticator.py ├── RELEASE.md ├── LICENSE ├── pyproject.toml ├── CONTRIBUTING.md ├── .pre-commit-config.yaml ├── README.md ├── .gitignore ├── tmpauthenticator └── __init__.py └── CHANGELOG.md /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | # flake8 is used for linting Python code setup to automatically run with 2 | # pre-commit. 3 | # 4 | # ref: https://flake8.pycqa.org/en/latest/user/configuration.html 5 | # 6 | [flake8] 7 | # E: style errors 8 | # W: style warnings 9 | # C: complexity 10 | # D: docstring warnings (unused pydocstyle extension) 11 | ignore = E, C, W, D 12 | -------------------------------------------------------------------------------- /jupyterhub_config.py: -------------------------------------------------------------------------------- 1 | """ 2 | sample jupyterhub config file for testing tmpauthenticator 3 | """ 4 | 5 | c = get_config() # noqa 6 | 7 | c.JupyterHub.authenticator_class = "tmp" # TmpAuthenticator 8 | c.JupyterHub.spawner_class = "simple" # SimpleLocalProcessSpawner 9 | 10 | # c.TmpAuthenticator.auto_login = False 11 | # c.TmpAuthenticator.login_service = "your inherent worth as a human being" 12 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # dependabot.yaml reference: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 2 | # 3 | # Notes: 4 | # - Status and logs from dependabot are provided at 5 | # https://github.com/jupyterhub/tmpauthenticator/network/updates. 6 | # 7 | version: 2 8 | updates: 9 | # Maintain dependencies in our GitHub Workflows 10 | - package-ecosystem: github-actions 11 | directory: / 12 | labels: [ci] 13 | schedule: 14 | interval: monthly 15 | time: "05:00" 16 | timezone: Etc/UTC 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | with open("README.md", encoding="utf8") as f: 4 | readme = f.read() 5 | 6 | setup( 7 | name="jupyterhub-tmpauthenticator", 8 | version="1.0.0", 9 | description="JupyterHub authenticator that hands out temporary accounts for everyone", 10 | url="https://github.com/jupyterhub/tmpauthenticator", 11 | author="Project Jupyter Contributors", # founded by Yuvi Panda 12 | author_email="jupyter@googlegroups.com", 13 | license="3 Clause BSD", 14 | long_description=readme, 15 | long_description_content_type="text/markdown", 16 | entry_points={ 17 | # Thanks to this, user are able to do: 18 | # 19 | # c.JupyterHub.authenticator_class = "tmp" 20 | # 21 | # ref: https://jupyterhub.readthedocs.io/en/4.0.0/reference/authenticators.html#registering-custom-authenticators-via-entry-points 22 | # 23 | "jupyterhub.authenticators": [ 24 | "tmp = tmpauthenticator:TmpAuthenticator", 25 | ], 26 | }, 27 | packages=find_packages(), 28 | python_requires=">=3.8", 29 | install_require={ 30 | "jupyterhub>=2.3.0", 31 | "traitlets", 32 | }, 33 | extras_require={ 34 | "test": [ 35 | "aiohttp", 36 | "pytest", 37 | "pytest-asyncio", 38 | "pytest-cov", 39 | "pytest-jupyterhub", 40 | ], 41 | }, 42 | ) 43 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import pytest 3 | from traitlets.config import Config 4 | 5 | # pytest-jupyterhub provides a pytest-plugin, and from it we get various 6 | # fixtures, where we make use of hub_app that builds on MockHub, which defaults 7 | # to providing a MockSpawner. 8 | # 9 | # ref: https://github.com/jupyterhub/pytest-jupyterhub 10 | # ref: https://github.com/jupyterhub/jupyterhub/blob/4.0.0/jupyterhub/tests/mocking.py#L224 11 | # 12 | pytest_plugins = [ 13 | "jupyterhub-spawners-plugin", 14 | ] 15 | 16 | 17 | @pytest.fixture 18 | async def hub_config(): 19 | """ 20 | Represents the base configuration of relevance to test TmpAuthenticator. 21 | """ 22 | config = Config() 23 | config.JupyterHub.authenticator_class = "tmp" 24 | 25 | # yarl or aiohttp used in tests doesn't handle escaped URLs correctly, so 26 | # the MockHub prefix of "/@/space%20word/" must be updated to workaround it. 27 | config.JupyterHub.base_url = "/prefix/" 28 | 29 | return config 30 | 31 | 32 | @pytest.fixture 33 | async def web_client_session(): 34 | """ 35 | Returns a ClientSession object from aiohttp, allowing cookies to be stored 36 | in between requests etc, allowing us to simulate a browser. 37 | 38 | ref: https://docs.aiohttp.org/en/stable/client_reference.html#client-session 39 | ref: https://docs.aiohttp.org/en/stable/client_reference.html#response-object 40 | """ 41 | async with aiohttp.ClientSession() as web_client_session: 42 | yield web_client_session 43 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # How to make a release 2 | 3 | `jupyterhub-tmpauthenticator` is a package available on [PyPI]. 4 | 5 | These are the instructions on how to make a release. 6 | 7 | ## Pre-requisites 8 | 9 | - Push rights to this GitHub repository 10 | 11 | ## Steps to make a release 12 | 13 | 1. Create a PR updating `CHANGELOG.md` with [github-activity] and continue when 14 | its merged. 15 | 16 | Advice on this procedure can be found in [this team compass 17 | issue](https://github.com/jupyterhub/team-compass/issues/563). 18 | 19 | 2. Checkout main and make sure it is up to date. 20 | 21 | ```shell 22 | git checkout main 23 | git fetch origin main 24 | git reset --hard origin/main 25 | ``` 26 | 27 | 3. Update the version, make commits, and push a git tag with `tbump`. 28 | 29 | ```shell 30 | pip install tbump 31 | ``` 32 | 33 | `tbump` will ask for confirmation before doing anything. 34 | 35 | ```shell 36 | # Example versions to set: 1.0.0, 1.0.0b1 37 | VERSION= 38 | tbump ${VERSION} 39 | ``` 40 | 41 | Following this, the [CI system] will build and publish a release. 42 | 43 | 4. Reset the version back to dev, e.g. `1.0.1.dev` after releasing `1.0.0`. 44 | 45 | ```shell 46 | # Example version to set: 1.0.1.dev 47 | NEXT_VERSION= 48 | tbump --no-tag ${NEXT_VERSION}.dev 49 | ``` 50 | 51 | [github-activity]: https://github.com/executablebooks/github-activity 52 | [pypi]: https://pypi.org/project/jupyterhub-tmpauthenticator/ 53 | [ci system]: https://github.com/jupyterhub/jupyterhub-tmpauthenticator/actions/workflows/release.yaml 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2016, Project Jupyter Contributors 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # autoflake is used for autoformatting Python code 2 | # 3 | # ref: https://github.com/PyCQA/autoflake#readme 4 | # 5 | [tool.autoflake] 6 | ignore-init-module-imports = true 7 | remove-all-unused-imports = true 8 | remove-duplicate-keys = true 9 | remove-unused-variables = true 10 | 11 | 12 | # black is used for autoformatting Python code 13 | # 14 | # ref: https://black.readthedocs.io/en/stable/ 15 | # 16 | [tool.black] 17 | target_version = [ 18 | "py38", 19 | "py39", 20 | "py310", 21 | "py311", 22 | "py312", 23 | ] 24 | 25 | 26 | # isort is used for autoformatting Python code 27 | # 28 | # ref: https://pycqa.github.io/isort/ 29 | # 30 | [tool.isort] 31 | profile = "black" 32 | 33 | 34 | # pytest is used for running Python based tests 35 | # 36 | # ref: https://docs.pytest.org/en/stable/ 37 | # 38 | [tool.pytest.ini_options] 39 | addopts = "--verbose --color=yes --durations=10" 40 | asyncio_mode = "auto" 41 | testpaths = ["tests"] 42 | 43 | 44 | # tbump is used to simplify and standardize the release process when updating 45 | # the version, making a git commit and tag, and pushing changes. 46 | # 47 | # ref: https://github.com/your-tools/tbump#readme 48 | # 49 | [tool.tbump] 50 | github_url = "https://github.com/jupyterhub/tmpauthenticator" 51 | 52 | [tool.tbump.version] 53 | current = "1.0.0" 54 | regex = ''' 55 | (?P\d+) 56 | \. 57 | (?P\d+) 58 | \. 59 | (?P\d+) 60 | (?P
((a|b|rc)\d+)|)
61 |     \.?
62 |     (?P(?<=\.)dev\d*|)
63 | '''
64 | 
65 | [tool.tbump.git]
66 | message_template = "Bump to {new_version}"
67 | tag_template = "v{new_version}"
68 | 
69 | [[tool.tbump.file]]
70 | src = "setup.py"
71 | 


--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
 1 | # This is a GitHub workflow defining a set of jobs with a set of steps.
 2 | # ref: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions
 3 | #
 4 | name: Release
 5 | 
 6 | # Always tests wheel building, but only publish to PyPI on pushed tags.
 7 | on:
 8 |   pull_request:
 9 |     paths-ignore:
10 |       - "docs/**"
11 |       - ".github/workflows/*.yaml"
12 |       - "!.github/workflows/release.yaml"
13 |   push:
14 |     paths-ignore:
15 |       - "docs/**"
16 |       - ".github/workflows/*.yaml"
17 |       - "!.github/workflows/release.yaml"
18 |     branches-ignore:
19 |       - "dependabot/**"
20 |       - "pre-commit-ci-update-config"
21 |     tags: ["**"]
22 |   workflow_dispatch:
23 | 
24 | jobs:
25 |   build-release:
26 |     runs-on: ubuntu-latest
27 |     permissions:
28 |       # id-token=write is required for pypa/gh-action-pypi-publish, and the PyPI
29 |       # project needs to be configured to trust this workflow.
30 |       #
31 |       # ref: https://github.com/jupyterhub/team-compass/issues/648
32 |       #
33 |       id-token: write
34 | 
35 |     steps:
36 |       - uses: actions/checkout@v6
37 |       - uses: actions/setup-python@v6
38 |         with:
39 |           python-version: "3.12"
40 | 
41 |       - name: install build package
42 |         run: |
43 |           pip install --upgrade pip
44 |           pip install build
45 |           pip freeze
46 | 
47 |       - name: build release
48 |         run: |
49 |           python -m build --sdist --wheel .
50 |           ls -l dist
51 | 
52 |       - name: publish to pypi
53 |         uses: pypa/gh-action-pypi-publish@release/v1
54 |         if: startsWith(github.ref, 'refs/tags/')
55 | 


--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
 1 | # Contributing
 2 | 
 3 | Welcome! As a [Jupyter](https://jupyter.org) project, we follow the [Jupyter contributor guide](https://docs.jupyter.org/en/latest/contributing/content-contributor.html).
 4 | 
 5 | ## Setting up a local development environment
 6 | 
 7 | We ship a `jupyterhub_config.py` file that helps you test `tmpauthenticator`
 8 | locally.
 9 | 
10 | 1.  Clone this repository
11 | 
12 |     ```sh
13 |     git clone https://github.com/jupyterhub/tmpauthenticator.git
14 |     ```
15 | 
16 | 2.  Setup a virtual environment. After cloning the repository, you should set up an
17 |     isolated environment to install libraries required for running / developing
18 |     kubespawner.
19 | 
20 |     There are many ways of doing this: conda envs, virtualenv, pipenv, etc. Pick
21 |     your favourite. We show you how to use venv:
22 | 
23 |     ```sh
24 |     cd tmpauthenticator
25 | 
26 |     python3 -m venv .
27 |     source bin/activate
28 |     ```
29 | 
30 | 3.  Install a locally editable version of tmpauthenticator and its dependencies for
31 |     running it and testing it.
32 | 
33 |     ```sh
34 |     pip install -e .
35 |     ```
36 | 
37 | 4.  Install the nodejs based [Configurable HTTP Proxy
38 |     (CHP)](https://github.com/jupyterhub/configurable-http-proxy), and make it
39 |     accessible to JupyterHub.
40 | 
41 |     ```sh
42 |     npm install configurable-http-proxy
43 |     export PATH=$(pwd)/node_modules/.bin:$PATH
44 |     ```
45 | 
46 | 5.  Start JupyterHub
47 | 
48 |     ```sh
49 |     # Run this from the tmpauthenticator repo's root directory where the preconfigured
50 |     # jupyterhub_config.py file resides!
51 |     jupyterhub
52 |     ```
53 | 
54 | 6.  Visit [http://localhost:8000/](http://localhost:8000/)!
55 | 


--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
 1 | # pre-commit is a tool to perform a predefined set of tasks manually and/or
 2 | # automatically before git commits are made.
 3 | #
 4 | # Config reference: https://pre-commit.com/#pre-commit-configyaml---top-level
 5 | #
 6 | # Common tasks
 7 | #
 8 | # - Run on all files:   pre-commit run --all-files
 9 | # - Register git hooks: pre-commit install --install-hooks
10 | #
11 | repos:
12 |   # Autoformat: Python code, syntax patterns are modernized
13 |   - repo: https://github.com/asottile/pyupgrade
14 |     rev: v3.21.2
15 |     hooks:
16 |       - id: pyupgrade
17 |         args:
18 |           - --py38-plus
19 | 
20 |   # Autoformat: Python code
21 |   - repo: https://github.com/PyCQA/autoflake
22 |     rev: v2.3.1
23 |     hooks:
24 |       - id: autoflake
25 |         # args ref: https://github.com/PyCQA/autoflake#advanced-usage
26 |         args:
27 |           - --in-place
28 | 
29 |   # Autoformat: Python code
30 |   - repo: https://github.com/pycqa/isort
31 |     rev: 7.0.0
32 |     hooks:
33 |       - id: isort
34 | 
35 |   # Autoformat: Python code
36 |   - repo: https://github.com/psf/black-pre-commit-mirror
37 |     rev: 25.11.0
38 |     hooks:
39 |       - id: black
40 | 
41 |   # Autoformat: markdown, yaml
42 |   - repo: https://github.com/pre-commit/mirrors-prettier
43 |     rev: v4.0.0-alpha.8
44 |     hooks:
45 |       - id: prettier
46 | 
47 |   # Misc...
48 |   - repo: https://github.com/pre-commit/pre-commit-hooks
49 |     rev: v6.0.0
50 |     # ref: https://github.com/pre-commit/pre-commit-hooks#hooks-available
51 |     hooks:
52 |       - id: end-of-file-fixer
53 |       - id: requirements-txt-fixer
54 |       - id: check-case-conflict
55 |       - id: check-executables-have-shebangs
56 | 
57 |   # Lint: Python code
58 |   - repo: https://github.com/PyCQA/flake8
59 |     rev: "7.3.0"
60 |     hooks:
61 |       - id: flake8
62 | 
63 | # pre-commit.ci config reference: https://pre-commit.ci/#configuration
64 | ci:
65 |   autoupdate_schedule: monthly
66 | 


--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
 1 | # This is a GitHub workflow defining a set of jobs with a set of steps.
 2 | # ref: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions
 3 | #
 4 | name: Tests
 5 | 
 6 | on:
 7 |   pull_request:
 8 |     paths-ignore:
 9 |       - "docs/**"
10 |       - "**.md"
11 |       - ".github/workflows/*.yaml"
12 |       - "!.github/workflows/test.yaml"
13 |   push:
14 |     paths-ignore:
15 |       - "docs/**"
16 |       - "**.md"
17 |       - ".github/workflows/*.yaml"
18 |       - "!.github/workflows/test.yaml"
19 |     branches-ignore:
20 |       - "dependabot/**"
21 |       - "pre-commit-ci-update-config"
22 |     tags: ["**"]
23 |   workflow_dispatch:
24 | 
25 | jobs:
26 |   test:
27 |     runs-on: "${{ matrix.runs-on || 'ubuntu-latest' }}"
28 | 
29 |     strategy:
30 |       fail-fast: false
31 |       matrix:
32 |         include:
33 |           # oldest supported python and jupyterhub version
34 |           - python-version: "3.8"
35 |             pip-install-spec: "jupyterhub==2.3.0 sqlalchemy==1.*"
36 |             runs-on: ubuntu-22.04
37 |           - python-version: "3.9"
38 |             pip-install-spec: "jupyterhub==2.* sqlalchemy==1.*"
39 |           - python-version: "3.10"
40 |             pip-install-spec: "jupyterhub==3.*"
41 |           - python-version: "3.11"
42 |             pip-install-spec: "jupyterhub==4.*"
43 |           - python-version: "3.12"
44 |             pip-install-spec: "jupyterhub==5.*"
45 | 
46 |           # latest version of python and jupyterhub (including pre-releases)
47 |           - python-version: "3.x"
48 |             pip-install-spec: "--pre jupyterhub"
49 | 
50 |     steps:
51 |       - uses: actions/checkout@v6
52 |       - uses: actions/setup-node@v6
53 |         with:
54 |           node-version: "lts/*"
55 |       - uses: actions/setup-python@v6
56 |         with:
57 |           python-version: "${{ matrix.python-version }}"
58 | 
59 |       - name: Install Node dependencies
60 |         run: |
61 |           npm install -g configurable-http-proxy
62 | 
63 |       - name: Install Python dependencies
64 |         run: |
65 |           pip install ${{ matrix.pip-install-spec }}
66 |           pip install -e ".[test]"
67 |           pip freeze
68 | 
69 |       - name: Run tests
70 |         run: |
71 |           pytest --cov=tmpauthenticator
72 | 
73 |       # GitHub action reference: https://github.com/codecov/codecov-action
74 |       - uses: codecov/codecov-action@v5
75 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
 1 | # Temporary JupyterHub Authenticator
 2 | 
 3 | [![Latest PyPI version](https://img.shields.io/pypi/v/jupyterhub-tmpauthenticator?logo=pypi)](https://pypi.python.org/pypi/jupyterhub-tmpauthenticator)
 4 | [![GitHub Workflow Status - Test](https://img.shields.io/github/actions/workflow/status/jupyterhub/tmpauthenticator/test.yaml?logo=github&label=tests)](https://github.com/jupyterhub/tmpauthenticator/actions)
 5 | [![Test coverage of code](https://codecov.io/gh/jupyterhub/tmpauthenticator/branch/main/graph/badge.svg)](https://codecov.io/gh/jupyterhub/tmpauthenticator)
 6 | [![Issue tracking - GitHub](https://img.shields.io/badge/issue_tracking-github-blue?logo=github)](https://github.com/jupyterhub/tmpauthenticator/issues)
 7 | [![Help forum - Discourse](https://img.shields.io/badge/help_forum-discourse-blue?logo=discourse)](https://discourse.jupyter.org/c/jupyterhub)
 8 | 
 9 | Simple authenticator for [JupyterHub](http://github.com/jupyter/jupyterhub/)
10 | that gives anyone who visits the home page a user account without having to
11 | log in using any UI at all. It also spawns a single-user server and directs
12 | the user to it immediately, without them having to press a button.
13 | 
14 | Built primarily to help run [tmpnb](https://github.com/jupyter/tmpnb) with JupyterHub.
15 | 
16 | ## Installation
17 | 
18 | ```
19 | pip install jupyterhub-tmpauthenticator
20 | ```
21 | 
22 | Should install it. It has no additional dependencies beyond JupyterHub.
23 | 
24 | You can then use this as your authenticator by adding the following line to
25 | your `jupyterhub_config.py`:
26 | 
27 | ```python
28 | c.JupyterHub.authenticator_class = "tmp"
29 | ```
30 | 
31 | ## Configuration
32 | 
33 | `tmpauthenticator` does not have a lot of configurable knobs, but will respect
34 | many relevant config options in the [base JupyterHub Authenticator class](https://jupyterhub.readthedocs.io/en/stable/reference/api/auth.html).
35 | Here are a few that are particularly useful.
36 | 
37 | ### `TmpAuthenticator.auto_login`
38 | 
39 | By default, `tmpauthenticator` will automatically log the user in as soon
40 | as they hit the landing page of the JupyterHub, without showing them any UI.
41 | This behavior can be turned off by setting `TmpAuthenticator.auto_login` to
42 | `False`, allowing a home page to be shown. There will be a `Sign in` button here
43 | that will automatically authenticate the user.
44 | 
45 | ```python
46 | c.TmpAuthenticator.auto_login = False
47 | ```
48 | 
49 | ### `TmpAuthenticator.login_service`
50 | 
51 | If `auto_login` is set to `False`, the value of `TmpAuthenticator.login_service`
52 | will determine the text shown next to `Sign in` in the default home page. It
53 | defaults to `Automatic Temporary Credentials, so the button will read as
54 | `Sign in with Automatic Temporary Credentials`.
55 | 
56 | ```python
57 | c.TmpAuthenticator.auto_login = False
58 | c.TmpAuthenticator.login_service = "your inherent worth as a human being"
59 | ```
60 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
  1 | # Manually added parts to .gitignore
  2 | # ----------------------------------
  3 | #
  4 | 
  5 | # JupyterHub running for development purposes
  6 | jupyterhub-proxy.pid
  7 | jupyterhub.sqlite
  8 | jupyterhub_cookie_secret
  9 | 
 10 | # Editors
 11 | .vscode
 12 | .idea
 13 | 
 14 | 
 15 | # Python .gitignore from https://github.com/github/gitignore/blob/HEAD/Python.gitignore
 16 | # -------------------------------------------------------------------------------------
 17 | #
 18 | # Byte-compiled / optimized / DLL files
 19 | __pycache__/
 20 | *.py[cod]
 21 | *$py.class
 22 | 
 23 | # C extensions
 24 | *.so
 25 | 
 26 | # Distribution / packaging
 27 | .Python
 28 | build/
 29 | develop-eggs/
 30 | dist/
 31 | downloads/
 32 | eggs/
 33 | .eggs/
 34 | lib/
 35 | lib64/
 36 | parts/
 37 | sdist/
 38 | var/
 39 | wheels/
 40 | share/python-wheels/
 41 | *.egg-info/
 42 | .installed.cfg
 43 | *.egg
 44 | MANIFEST
 45 | 
 46 | # PyInstaller
 47 | #  Usually these files are written by a python script from a template
 48 | #  before PyInstaller builds the exe, so as to inject date/other infos into it.
 49 | *.manifest
 50 | *.spec
 51 | 
 52 | # Installer logs
 53 | pip-log.txt
 54 | pip-delete-this-directory.txt
 55 | 
 56 | # Unit test / coverage reports
 57 | htmlcov/
 58 | .tox/
 59 | .nox/
 60 | .coverage
 61 | .coverage.*
 62 | .cache
 63 | nosetests.xml
 64 | coverage.xml
 65 | *.cover
 66 | *.py,cover
 67 | .hypothesis/
 68 | .pytest_cache/
 69 | cover/
 70 | 
 71 | # Translations
 72 | *.mo
 73 | *.pot
 74 | 
 75 | # Django stuff:
 76 | *.log
 77 | local_settings.py
 78 | db.sqlite3
 79 | db.sqlite3-journal
 80 | 
 81 | # Flask stuff:
 82 | instance/
 83 | .webassets-cache
 84 | 
 85 | # Scrapy stuff:
 86 | .scrapy
 87 | 
 88 | # Sphinx documentation
 89 | docs/_build/
 90 | 
 91 | # PyBuilder
 92 | .pybuilder/
 93 | target/
 94 | 
 95 | # Jupyter Notebook
 96 | .ipynb_checkpoints
 97 | 
 98 | # IPython
 99 | profile_default/
100 | ipython_config.py
101 | 
102 | # pyenv
103 | #   For a library or package, you might want to ignore these files since the code is
104 | #   intended to run in multiple environments; otherwise, check them in:
105 | # .python-version
106 | 
107 | # pipenv
108 | #   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
109 | #   However, in case of collaboration, if having platform-specific dependencies or dependencies
110 | #   having no cross-platform support, pipenv may install dependencies that don't work, or not
111 | #   install all needed dependencies.
112 | #Pipfile.lock
113 | 
114 | # poetry
115 | #   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
116 | #   This is especially recommended for binary packages to ensure reproducibility, and is more
117 | #   commonly ignored for libraries.
118 | #   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
119 | #poetry.lock
120 | 
121 | # pdm
122 | #   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
123 | #pdm.lock
124 | #   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
125 | #   in version control.
126 | #   https://pdm.fming.dev/#use-with-ide
127 | .pdm.toml
128 | 
129 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
130 | __pypackages__/
131 | 
132 | # Celery stuff
133 | celerybeat-schedule
134 | celerybeat.pid
135 | 
136 | # SageMath parsed files
137 | *.sage.py
138 | 
139 | # Environments
140 | .env
141 | .venv
142 | env/
143 | venv/
144 | ENV/
145 | env.bak/
146 | venv.bak/
147 | 
148 | # Spyder project settings
149 | .spyderproject
150 | .spyproject
151 | 
152 | # Rope project settings
153 | .ropeproject
154 | 
155 | # mkdocs documentation
156 | /site
157 | 
158 | # mypy
159 | .mypy_cache/
160 | .dmypy.json
161 | dmypy.json
162 | 
163 | # Pyre type checker
164 | .pyre/
165 | 
166 | # pytype static type analyzer
167 | .pytype/
168 | 
169 | # Cython debug symbols
170 | cython_debug/
171 | 
172 | # PyCharm
173 | #  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
174 | #  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
175 | #  and can be added to the global gitignore or merged into this file.  For a more nuclear
176 | #  option (not recommended) you can uncomment the following to ignore the entire idea folder.
177 | #.idea/
178 | 


--------------------------------------------------------------------------------
/tests/test_tmpauthenticator.py:
--------------------------------------------------------------------------------
  1 | import pytest
  2 | from yarl import URL
  3 | 
  4 | 
  5 | async def _get_username(web_client_session, app_url):
  6 |     """
  7 |     Visits /hub/home to get an _xsrf token set to cookies, that we can then
  8 |     pass as a X-XSRFToken header when accessing /hub/api, then the function
  9 |     visit /hub/api/user to get the username as recognized by JupyterHub
 10 |     based on cookies passed.
 11 |     """
 12 |     hub_url = str(app_url / "hub/")
 13 |     home_url = str(app_url / "hub/home")
 14 |     api_user_url = str(app_url / "hub/api/user")
 15 | 
 16 |     r = await web_client_session.get(home_url)
 17 |     assert r.status == 200
 18 | 
 19 |     hub_cookies = web_client_session.cookie_jar.filter_cookies(home_url)
 20 |     if "_xsrf" in hub_cookies:
 21 |         _xsrf = hub_cookies["_xsrf"].value
 22 |     else:
 23 |         _xsrf = ""
 24 |     headers = {
 25 |         "X-XSRFToken": _xsrf,  # required for jupyterhub>=4
 26 |         "Referer": hub_url,  # required for jupyterhub<4
 27 |         "Accept": "application/json",
 28 |     }
 29 | 
 30 |     r = await web_client_session.get(api_user_url, headers=headers)
 31 |     assert r.status == 200
 32 |     user_api_response = await r.json()
 33 |     return user_api_response["name"]
 34 | 
 35 | 
 36 | @pytest.mark.parametrize(
 37 |     "test_config, test_status, test_location, test_url",
 38 |     [
 39 |         ({}, 302, "hub/tmplogin", None),
 40 |         ({"TmpAuthenticator": {"auto_login": True}}, 302, "hub/tmplogin", None),
 41 |         ({"TmpAuthenticator": {"auto_login": False}}, 200, None, "hub/login"),
 42 |     ],
 43 | )
 44 | async def test_auto_login_config(
 45 |     hub_app,
 46 |     hub_config,
 47 |     web_client_session,
 48 |     test_config,
 49 |     test_status,
 50 |     test_location,
 51 |     test_url,
 52 | ):
 53 |     """
 54 |     Tests TmpAuthenticator.auto_login's behavior when its default value is used,
 55 |     and when its explicitly set.
 56 |     """
 57 |     hub_config.merge(test_config)
 58 | 
 59 |     app = await hub_app(hub_config)
 60 |     app_port = URL(app.bind_url).port
 61 |     app_url = URL(f"http://localhost:{app_port}{app.base_url}")
 62 | 
 63 |     login_url = str(app_url / "hub/login")
 64 |     r = await web_client_session.get(login_url, allow_redirects=False)
 65 | 
 66 |     assert r.status == test_status
 67 |     if test_url:
 68 |         assert test_url in str(r.url)
 69 |     if test_location:
 70 |         assert test_location in r.headers["Location"]
 71 | 
 72 | 
 73 | async def test_login(
 74 |     hub_app,
 75 |     hub_config,
 76 |     web_client_session,
 77 | ):
 78 |     """
 79 |     Tests that the user is redirected and finally authorized for /hub/home.
 80 |     """
 81 |     app = await hub_app(hub_config)
 82 |     app_port = URL(app.bind_url).port
 83 |     app_url = URL(f"http://localhost:{app_port}{app.base_url}")
 84 | 
 85 |     home_url = str(app_url / "hub/home")
 86 |     r = await web_client_session.get(home_url)
 87 | 
 88 |     assert r.status == 200
 89 | 
 90 | 
 91 | @pytest.mark.parametrize(
 92 |     "test_setting_admin_to, test_status",
 93 |     [
 94 |         (True, 200),
 95 |         (False, 403),
 96 |     ],
 97 | )
 98 | async def test_post_auth_hook_config(
 99 |     hub_app,
100 |     hub_config,
101 |     web_client_session,
102 |     test_setting_admin_to,
103 |     test_status,
104 | ):
105 |     """
106 |     Tests that the inherited Authenticator.post_auth_hook is respected by
107 |     updating the authentication dictionary's admin key and accessing /hub/admin,
108 |     which should result in a forbidden response if not configured as admin via
109 |     the post_auth_hook.
110 |     """
111 | 
112 |     def set_admin_post_auth_hook(authenticator, handler, authentication):
113 |         authentication["admin"] = test_setting_admin_to
114 |         return authentication
115 | 
116 |     hub_config.TmpAuthenticator.post_auth_hook = set_admin_post_auth_hook
117 | 
118 |     app = await hub_app(hub_config)
119 |     app_port = URL(app.bind_url).port
120 |     app_url = URL(f"http://localhost:{app_port}{app.base_url}")
121 | 
122 |     admin_url = str(app_url / "hub/admin")
123 |     r = await web_client_session.get(admin_url)
124 | 
125 |     assert r.status == test_status
126 | 
127 | 
128 | async def test_revisit_tmplogin(
129 |     hub_app,
130 |     hub_config,
131 |     web_client_session,
132 | ):
133 |     """
134 |     Tests that we get a new user if visiting /hub/tmplogin, even if we already
135 |     were authenticated as one as recognized by cookies.
136 | 
137 |     This is done by first visiting /hub/home which should get us logged in and
138 |     inspecting the user via a cookie, and then /hub/tmplogin to again inspect
139 |     the user via a cookie.
140 |     """
141 |     app = await hub_app(hub_config)
142 |     app_port = URL(app.bind_url).port
143 |     app_url = URL(f"http://localhost:{app_port}{app.base_url}")
144 | 
145 |     # first access, so we receive a new user
146 |     first_username = await _get_username(web_client_session, app_url)
147 |     assert first_username
148 | 
149 |     # when we visit /hub/home again, we are recognized and that doesn't make us
150 |     # arrive at /hub/login -> /hub/tmplogin, and therefore we shouldn't get a
151 |     # new user
152 |     assert first_username == await _get_username(web_client_session, app_url)
153 | 
154 |     # we visit /hub/tmplogin and should get a _new_ user
155 |     tmplogin_url = str(app_url / "hub/tmplogin")
156 |     r = await web_client_session.get(tmplogin_url)
157 |     assert r.status == 200
158 |     second_username = await _get_username(web_client_session, app_url)
159 |     assert first_username != second_username
160 | 


--------------------------------------------------------------------------------
/tmpauthenticator/__init__.py:
--------------------------------------------------------------------------------
  1 | import uuid
  2 | 
  3 | from jupyterhub.auth import Authenticator
  4 | from jupyterhub.handlers import BaseHandler
  5 | from jupyterhub.utils import url_path_join
  6 | from traitlets import Unicode, default
  7 | 
  8 | 
  9 | class TmpAuthenticateHandler(BaseHandler):
 10 |     """
 11 |     Provides a GET web request handler for /hub/tmplogin, as registered by
 12 |     TmpAuthenticator's override of Authenticator.get_handlers.
 13 | 
 14 |     JupyterHub will redirect here if it doesn't recognize a user via a cookie,
 15 |     but users can also visit /hub/tmplogin explicitly to get setup with a new
 16 |     user.
 17 |     """
 18 | 
 19 |     async def get(self):
 20 |         """
 21 |         Authenticate as a new random user no matter what.
 22 | 
 23 |         This GET request handler mimics parts of what's done by JupyterHub's
 24 |         LoginHandler when a user isn't recognized: to first call
 25 |         BaseHandler.login_user and then redirect the user onwards. The
 26 |         difference is that here users always login as a new user.
 27 | 
 28 |         By overwriting any previous user's identifying cookie, it acts as a
 29 |         combination of a logout and login handler.
 30 | 
 31 |         JupyterHub's LoginHandler ref: https://github.com/jupyterhub/jupyterhub/blob/4.0.0/jupyterhub/handlers/login.py#L129-L138
 32 |         """
 33 |         # Login as a new user, without checking if we were already logged in
 34 |         #
 35 |         user = await self.login_user(None)
 36 | 
 37 |         # Set or overwrite the login cookie to recognize the new user.
 38 |         #
 39 |         # login_user calls set_login_cookie(user), that sets a login cookie for
 40 |         # the user via set_hub_cookie(user), but only if it doesn't recognize a
 41 |         # user from an pre-existing login cookie. Due to that, we
 42 |         # unconditionally call self.set_hub_cookie(user) here.
 43 |         #
 44 |         # BaseHandler.login_user:                   https://github.com/jupyterhub/jupyterhub/blob/4.0.0/jupyterhub/handlers/base.py#L823-L843
 45 |         # - BaseHandler.authenticate:               https://github.com/jupyterhub/jupyterhub/blob/4.0.0/jupyterhub/handlers/base.py#L643-L644
 46 |         #   - Authenticator.get_authenticated_user: https://github.com/jupyterhub/jupyterhub/blob/4.0.0/jupyterhub/auth.py#L472-L534
 47 |         # - BaseHandler.auth_to_user:               https://github.com/jupyterhub/jupyterhub/blob/4.0.0/jupyterhub/handlers/base.py#L774-L821
 48 |         # - BaseHandler.set_login_cookie:           https://github.com/jupyterhub/jupyterhub/blob/4.0.0/jupyterhub/handlers/base.py#L627-L628
 49 |         #   - BaseHandler.set_session_cookie:       https://github.com/jupyterhub/jupyterhub/blob/4.0.0/jupyterhub/handlers/base.py#L601-L613
 50 |         #   - BaseHandler.set_hub_cookie:           https://github.com/jupyterhub/jupyterhub/blob/4.0.0/jupyterhub/handlers/base.py#L623-L625
 51 |         #
 52 |         self.set_hub_cookie(user)
 53 | 
 54 |         # Login complete, redirect the user.
 55 |         #
 56 |         # BaseHandler.get_next_url ref: https://github.com/jupyterhub/jupyterhub/blob/4.0.0/jupyterhub/handlers/base.py#L646-L653
 57 |         #
 58 |         next_url = self.get_next_url(user)
 59 |         self.redirect(next_url)
 60 | 
 61 | 
 62 | class TmpAuthenticator(Authenticator):
 63 |     """
 64 |     When JupyterHub is configured to use this authenticator, visiting the home
 65 |     page immediately logs the user in with a randomly generated UUID if they are
 66 |     already not logged in, and spawns a server for them.
 67 |     """
 68 | 
 69 |     @default("auto_login")
 70 |     def _auto_login_default(self):
 71 |         """
 72 |         The Authenticator base class' config auto_login defaults to False, but
 73 |         we change that default to True in TmpAuthenticator. This makes users
 74 |         automatically get logged in when they hit the hub's home page, without
 75 |         requiring them to click a 'login' button.
 76 | 
 77 |         JupyterHub admins can still opt back to present the /hub/login page with
 78 |         the login button like this:
 79 | 
 80 |             c.TmpAuthenticator.auto_login = False
 81 |         """
 82 |         return True
 83 | 
 84 |     @default("allow_all")
 85 |     def _allow_all_default(self):
 86 |         """
 87 |         If no allow config is specified, then by default nobody will have access.
 88 |         Prior to JupyterHub 5.0, the opposite was true.
 89 | 
 90 |         Setting allow_all to True to preserve expected behavior of allowing
 91 |         everyone to access the hub in a JupyterHub 5.0 setup.
 92 |         """
 93 |         return True
 94 | 
 95 |     login_service = Unicode(
 96 |         "Automatic Temporary Credentials",
 97 |         help="""
 98 |         Text to be shown with the 'Sign in with ...' button, when auto_login is
 99 |         False.
100 | 
101 |         The Authenticator base class' login_service isn't tagged as a
102 |         configurable traitlet, so we redefine it to allow it to be configurable
103 |         like this:
104 | 
105 |             c.TmpAuthenticator.login_service = "your inherent worth as a human being"
106 |         """,
107 |     ).tag(config=True)
108 | 
109 |     async def authenticate(self, handler, data):
110 |         """
111 |         Always authenticate a new user by generating a universally unique
112 |         identifier (uuid).
113 |         """
114 |         username = str(uuid.uuid4())
115 |         return {
116 |             "name": username,
117 |         }
118 | 
119 |     def get_handlers(self, app):
120 |         """
121 |         Registers a dedicated endpoint and web request handler for logging in
122 |         with TmpAuthenticator. This is needed as /hub/login is reserved for
123 |         redirecting to what's returned by login_url.
124 | 
125 |         ref: https://github.com/jupyterhub/jupyterhub/pull/1066
126 |         """
127 |         return [("/tmplogin", TmpAuthenticateHandler)]
128 | 
129 |     def login_url(self, base_url):
130 |         """
131 |         login_url is overridden as intended for Authenticator subclasses that
132 |         provides a custom login handler (for /hub/tmplogin).
133 | 
134 |         JupyterHub redirects users to this destination from /hub/login if
135 |         auto_login is set, or if its not set and users press the "Sign in ..."
136 |         button.
137 | 
138 |         ref: https://github.com/jupyterhub/jupyterhub/blob/4.0.0/jupyterhub/auth.py#L708-L723
139 |         ref: https://github.com/jupyterhub/jupyterhub/blob/4.0.0/jupyterhub/handlers/login.py#L118-L147
140 |         """
141 |         return url_path_join(base_url, "tmplogin")
142 | 


--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
 1 | # Changelog
 2 | 
 3 | ## 1.0
 4 | 
 5 | ### [1.0.0] - 2023-05-22
 6 | 
 7 | #### Breaking Changes
 8 | 
 9 | - Python >=3.8 and JupyterHub >=2.3.0 is now required
10 | - Logging in while already logged in by visiting `/hub/tmplogin` now provides
11 |   the visitor with a new user identity to allow startup of a new server. The
12 |   `TmpAuthenticator.force_new_server` config is removed as no longer relevant as
13 |   part of this change.
14 | - The `TmpAuthenticator.process_user` function is no longer provided for
15 |   subclasses to override. The configurable [`Authenticator.post_auth_hook`] can
16 |   be used to accomplish the same things though.
17 | 
18 | [`Authenticator.post_auth_hook`]: https://jupyterhub.readthedocs.io/en/stable/reference/api/auth.html#jupyterhub.auth.Authenticator.post_auth_hook
19 | 
20 | #### Bugs fixed
21 | 
22 | - BREAKING: Logout current user when new user logs in (removes `force_new_server`) [#22](https://github.com/jupyterhub/tmpauthenticator/pull/22) ([@yuvipanda](https://github.com/yuvipanda))
23 | - Fix reference to unbound variable [#25](https://github.com/jupyterhub/tmpauthenticator/pull/25) ([@yuvipanda](https://github.com/yuvipanda))
24 | 
25 | #### Maintenance improvements
26 | 
27 | - setup.py: update author, comment about being founded by Yuvi [#48](https://github.com/jupyterhub/tmpauthenticator/pull/48) ([@consideratio](https://github.com/consideratio))
28 | - Add tests, require py38 and jupyterhub 2.3+ [#47](https://github.com/jupyterhub/tmpauthenticator/pull/47) ([@consideratio](https://github.com/consideratio))
29 | - Remove process_user and fix support for post_auth_hook [#45](https://github.com/jupyterhub/tmpauthenticator/pull/45) ([@consideratio](https://github.com/consideratio))
30 | 
31 | #### Documentation improvements
32 | 
33 | - docs: backfill changelog for 0.1-0.6, add changelog for 1.0.0 [#32](https://github.com/jupyterhub/tmpauthenticator/pull/32) ([@consideratio](https://github.com/consideratio))
34 | - Update CONTRIBUTING.md [#24](https://github.com/jupyterhub/tmpauthenticator/pull/24) ([@evanlinde](https://github.com/evanlinde))
35 | 
36 | #### Contributors to this release
37 | 
38 | [@evanlinde](https://github.com/search?q=repo%3Ajupyterhub%2Ftmpauthenticator+involves%3Aevanlinde+updated%3A2016-12-30..2023-04-19&type=Issues) | [@fm75](https://github.com/search?q=repo%3Ajupyterhub%2Ftmpauthenticator+involves%3Afm75+updated%3A2016-12-30..2023-04-19&type=Issues) | [@hilhert](https://github.com/search?q=repo%3Ajupyterhub%2Ftmpauthenticator+involves%3Ahilhert+updated%3A2016-12-30..2023-04-19&type=Issues) | [@manics](https://github.com/search?q=repo%3Ajupyterhub%2Ftmpauthenticator+involves%3Amanics+updated%3A2016-12-30..2023-04-19&type=Issues) | [@minrk](https://github.com/search?q=repo%3Ajupyterhub%2Ftmpauthenticator+involves%3Aminrk+updated%3A2016-12-30..2023-04-19&type=Issues) | [@mohirio](https://github.com/search?q=repo%3Ajupyterhub%2Ftmpauthenticator+involves%3Amohirio+updated%3A2016-12-30..2023-04-19&type=Issues) | [@sridhar562345](https://github.com/search?q=repo%3Ajupyterhub%2Ftmpauthenticator+involves%3Asridhar562345+updated%3A2016-12-30..2023-04-19&type=Issues) | [@takluyver](https://github.com/search?q=repo%3Ajupyterhub%2Ftmpauthenticator+involves%3Atakluyver+updated%3A2016-12-30..2023-04-19&type=Issues) | [@tkw1536](https://github.com/search?q=repo%3Ajupyterhub%2Ftmpauthenticator+involves%3Atkw1536+updated%3A2016-12-30..2023-04-19&type=Issues) | [@willingc](https://github.com/search?q=repo%3Ajupyterhub%2Ftmpauthenticator+involves%3Awillingc+updated%3A2016-12-30..2023-04-19&type=Issues) | [@yuvipanda](https://github.com/search?q=repo%3Ajupyterhub%2Ftmpauthenticator+involves%3Ayuvipanda+updated%3A2016-12-30..2023-04-19&type=Issues)
39 | 
40 | ## 0.6
41 | 
42 | ### [0.6] - 2019-05-18
43 | 
44 | - Compatibility with JupyterHub v1.0.0 [#18](https://github.com/jupyterhub/tmpauthenticator/pull/18) ([@mohirio](https://github.com/mohirio))
45 | 
46 | ## 0.5
47 | 
48 | ### [0.5] - 2017-11-02
49 | 
50 | - Bump version number [#9](https://github.com/jupyterhub/tmpauthenticator/pull/9) ([@yuvipanda](https://github.com/yuvipanda))
51 | - Add MANIFEST.in [#8](https://github.com/jupyterhub/tmpauthenticator/pull/8) ([@takluyver](https://github.com/takluyver))
52 | - Respect 'next' parameter after login [#6](https://github.com/jupyterhub/tmpauthenticator/pull/6) ([@tkw1536](https://github.com/tkw1536))
53 | - Update LICENSE [#4](https://github.com/jupyterhub/tmpauthenticator/pull/4) ([@fm75](https://github.com/fm75))
54 | - Update to work with JupyterHub 0.8 [#2](https://github.com/jupyterhub/tmpauthenticator/pull/2) ([@yuvipanda](https://github.com/yuvipanda))
55 | - Create CONTRIBUTING.md [c9db294](https://github.com/jupyterhub/tmpauthenticator/commit/c9db294) ([@yuvipanda](https://github.com/willingc))
56 | - simplify logic a bit [#1](https://github.com/jupyterhub/tmpauthenticator/pull/1) ([@minrk](https://github.com/minrk))
57 | - Respect force_new_server setting properly [1804b92](https://github.com/jupyterhub/tmpauthenticator/commit/1804b92) ([@yuvipanda](https://github.com/yuvipanda))
58 | 
59 | ## 0.4
60 | 
61 | ### [0.4] - 2017-05-08
62 | 
63 | - Bump package version [6964764](https://github.com/jupyterhub/tmpauthenticator/commit/6964764) ([@yuvipanda](https://github.com/yuvipanda))
64 | - Attempt to fix force_new_servers [313b8f2](https://github.com/jupyterhub/tmpauthenticator/commit/313b8f2) ([@yuvipanda](https://github.com/yuvipanda))
65 | - Add a hack to make the force_new_server 'work'[4ee14d8](https://github.com/jupyterhub/tmpauthenticator/commit/4ee14d8) ([@yuvipanda](https://github.com/yuvipanda))
66 | - Make starting a new server actually stop the old one [6300859](https://github.com/jupyterhub/tmpauthenticator/commit/6300859) ([@yuvipanda](https://github.com/yuvipanda))
67 | - Bump version number [45b2062](https://github.com/jupyterhub/tmpauthenticator/commit/45b2062) ([@yuvipanda](https://github.com/yuvipanda))
68 | 
69 | ## 0.3
70 | 
71 | ### [0.3] - 2017-05-04
72 | 
73 | - Add ability for subclasses to modify created user before spawning [8231fda](https://github.com/jupyterhub/tmpauthenticator/commit/8231fda) ([@yuvipanda](https://github.com/yuvipanda))
74 | - Add feature to force a new single-user server each time [b7aa546](https://github.com/jupyterhub/tmpauthenticator/commit/b7aa546) ([@yuvipanda](https://github.com/yuvipanda))
75 | 
76 | ## 0.2
77 | 
78 | ### [0.2] - 2017-05-01
79 | 
80 | - Version bump [0c81ac0](https://github.com/jupyterhub/tmpauthenticator/commit/0c81ac0) ([@yuvipanda](https://github.com/yuvipanda))
81 | - Add .gitignore [bed1a79](https://github.com/jupyterhub/tmpauthenticator/commit/bed1a79) ([@yuvipanda](https://github.com/yuvipanda))
82 | 
83 | ## 0.1
84 | 
85 | ### [0.1] - 2016-12-30
86 | 
87 | - Initial commit [cf52bc2](https://github.com/jupyterhub/tmpauthenticator/commit/cf52bc2) ([@yuvipanda](https://github.com/yuvipanda))
88 | 
89 | [1.0.0]: https://github.com/jupyterhub/tmpauthenticator/compare/v0.6...v1.0.0
90 | [0.6]: https://github.com/jupyterhub/tmpauthenticator/compare/v0.5...v0.6
91 | [0.5]: https://github.com/jupyterhub/tmpauthenticator/compare/v0.4...v0.5
92 | [0.4]: https://github.com/jupyterhub/tmpauthenticator/compare/v0.3...v0.4
93 | [0.3]: https://github.com/jupyterhub/tmpauthenticator/compare/v0.2...v0.3
94 | [0.2]: https://github.com/jupyterhub/tmpauthenticator/compare/v0.1...v0.2
95 | [0.1]: https://github.com/jupyterhub/tmpauthenticator/commit/cf52bc2
96 | 


--------------------------------------------------------------------------------