Metadata-Version: 2.4
Name: jupyterhub-tessera
Version: 1.0.0rc1
Summary: JupyterHub/JupyterLab plugin to acquire and securely store OAuth2/OIDC tokens, with a per-server status button.
Project-URL: Homepage, https://github.com/KaminoU/jupyterhub-tessera
Project-URL: Repository, https://github.com/KaminoU/jupyterhub-tessera
Project-URL: Issues, https://github.com/KaminoU/jupyterhub-tessera/issues
Project-URL: Changelog, https://github.com/KaminoU/jupyterhub-tessera/blob/main/CHANGELOG.md
Author: Michel TRUONG
Maintainer: Michel TRUONG
License: MIT License
        
        Copyright (c) 2026 Michel TRUONG
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: access-token,jupyter,jupyterhub,jupyterhub-service,jupyterlab,oauth2,oidc,pkce,refresh-token,sso,token,token-storage
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: Jupyter
Classifier: Framework :: Jupyter :: JupyterLab
Classifier: Framework :: Jupyter :: JupyterLab :: 4
Classifier: Framework :: Jupyter :: JupyterLab :: Extensions
Classifier: Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx<1,>=0.28
Requires-Dist: jupyterhub<6,>=4
Requires-Dist: kstlib[db-crypto]<4,>=3.6.2
Requires-Dist: pyyaml<7,>=6
Requires-Dist: rich>=13
Requires-Dist: tornado>=6.4
Requires-Dist: typer>=0.12
Provides-Extra: dev
Requires-Dist: jupyterlab<5,>=4.0.0; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-cov>=5; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: types-pyyaml<7,>=6; extra == 'dev'
Provides-Extra: docs
Requires-Dist: furo<2026,>=2025.9; extra == 'docs'
Requires-Dist: markdown-it-py<4,>=3; extra == 'docs'
Requires-Dist: myst-parser<5,>=4.0; extra == 'docs'
Requires-Dist: pygments>=2.20.0; extra == 'docs'
Requires-Dist: sphinx-design<0.7,>=0.6; extra == 'docs'
Requires-Dist: sphinx-togglebutton<1,>=0.3; extra == 'docs'
Requires-Dist: sphinx<9,>=8.1; extra == 'docs'
Provides-Extra: tox
Requires-Dist: tox-uv<2,>=1.35; extra == 'tox'
Requires-Dist: tox<5,>=4.31; extra == 'tox'
Description-Content-Type: text/markdown

<p align="center">
  <img src="https://raw.githubusercontent.com/KaminoU/jupyterhub-tessera/main/assets/tessera_i.svg" alt="tessera logo" width="240">
</p>

<p align="center">
  <strong>Acquire and securely store OAuth2/OIDC tokens in JupyterHub.</strong>
</p>

<p align="center">
  <a href="https://pypi.org/project/jupyterhub-tessera/"><img src="https://img.shields.io/pypi/v/jupyterhub-tessera?color=blue" alt="PyPI"></a>
  <img src="https://img.shields.io/badge/python-≥3.10-blue" alt="Python">
  <a href="https://github.com/KaminoU/jupyterhub-tessera/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="License"></a>
</p>

---

tessera adds a per-server button in JupyterLab. On click, the user runs an
OAuth2/OIDC authorization-code flow (confidential client, PKCE). A JupyterHub
Service receives the callback, exchanges the code, and stores the refresh token
encrypted at rest. Notebooks then read a fresh access token from the Service on
demand. The button is green when a valid refresh token exists, red otherwise.
tessera is generic: any OAuth2/OIDC provider is a configuration entry.

> **tessera is not an Authenticator.** It never signs anyone in to JupyterHub:
> your Hub keeps whatever Authenticator it already uses (PAM, LDAP, OAuth, or
> any other). tessera runs a separate step after that login, acquiring tokens
> from external OAuth2/OIDC providers so notebooks can call the APIs those
> providers protect.

<p align="center">
  <img src="https://raw.githubusercontent.com/KaminoU/jupyterhub-tessera/main/assets/poc-viya.png" alt="The tessera panel in JupyterLab: one button per declared server, green when a valid refresh token is stored, with token details on demand" width="720">
</p>

## How it works

1. **One button per declared server.** The panel shows a button for each OAuth
   server in your configuration, green when a valid refresh token is stored for
   that user, red otherwise.
2. **A click starts an authorization-code flow with PKCE.** The Service
   generates the `state` and the PKCE verifier, sends the browser to the
   provider with the S256 challenge, and keeps the verifier to itself.
3. **The provider calls back to the Service**, which validates the `state`
   against the flow it started and exchanges the code for tokens. The browser
   never handles a token.
4. **The refresh token is encrypted at rest**, with the encryption key held
   separately from the database file, so the database on its own reveals
   nothing.
5. **Notebooks read an access token on demand.** The kernel client
   (`get_token("my-idp")`, or `TESSERA_TOKEN["my-idp"]`) fetches a fresh token
   from the Service on each access. Nothing is cached kernel-side and no token
   ever sits in an environment variable, so a token revoked at the provider is
   never served from a stale copy.

## Architecture (three surfaces)

| Surface   | Stack            | Role                                                                                            |
| --------- | ---------------- | ----------------------------------------------------------------------------------------------- |
| Frontend  | TypeScript/React | Per-server status button (green/red), shipped as a prebuilt JupyterLab extension.               |
| Service   | Python 3.10+     | JupyterHub Service exposing `/login`, `/callback`, `/token`; encrypted token store.             |
| Admin CLI | Python (Typer)   | Deployment and ops helpers: `config-snippet`, `init-config`, `doctor`, `install-kernel-config`. |

## Install

```bash
pip install jupyterhub-tessera
```

Installing the Python package also ships the prebuilt JupyterLab extension; no
manual frontend copy is required.

## Project layout

| Path            | What lives there                                                     |
| --------------- | -------------------------------------------------------------------- |
| `src/tessera/`  | Service, admin CLI, kernel client, and the prebuilt lab extension    |
| `frontend/src/` | TypeScript/React panel sources, with their tests alongside them      |
| `tests/`        | Python test suite (config, flow, store, service, kernel, end to end) |
| `docs/`         | Sphinx documentation sources                                         |

## Development

Requirements: Python 3.10+, Node.js 20+.

```bash
# Python (use a virtual environment created outside any synced folder)
pip install -e ".[dev]"

# Frontend
npm install
npm run build
```

## Security

A refresh token and a client secret are encrypted at rest, with the encryption
key held separately from the database. The frontend never reads, stores, or
logs a token; it only consumes a boolean status.

## License

[MIT](LICENSE), Copyright (c) 2026 Michel TRUONG.
