jupyterlab_passkey_extension
A passkey bridge for JupyterLab that exposes the passkey (WebAuthn) capability of the user's browser or operating system to local clients with no browser of their own and returns the browser-side ceremony result to the requesting local process.
- Purpose - the bridge commands perform no cryptography, each caller supplies its parameters, and a tool that needs a WebAuthn PRF value keeps all key handling to itself
- Scope - the CLI (
jupyterlab-passkey, installed with the package) sends every request over an authenticated HTTP API, and the password vault is the one part that encrypts and stores secrets
Standards
| Standard | Status | What the vault does |
|---|---|---|
| NIST SP 800-38D, RFC 5116 | ✅ Met | AES-256-GCM for all entries under one random 32-byte data key |
| RFC 9106 Argon2id | ✅ Met | the recovery and unlock-password slots; a slot upgrades on its next use |
| RFC 7914 scrypt | ✅ Met | older recovery and unlock-password slots still open, N=2^17, r=8, p=1 |
| RFC 5869 HKDF-SHA256 | ✅ Met | the passkey slots, keyed from the WebAuthn PRF |
| RFC 6238 TOTP | ✅ Met | 6-digit codes of the authenticator app, a used code refused |
| W3C WebAuthn Level 3, PRF extension | ✅ Met | each passkey slot is bound to one hostname |
| NIST SP 800-63B, AAL2 idle limit | ✅ Met | 30-minute default unlock duration |
| NIST SP 800-57 key management | ✅ Met | the data key rotates, with a one-year default cryptoperiod |
Features
- Passkeys -
createregisters andgetasserts Windows Hello, Touch ID or a security key from a process with no browser - Approval - the user approves the ceremony in the open tab, and the call blocks until the result returns
- PRF -
get --prf-saltreturns the same 32 bytes for the same credential and salt, so a vault can derive its key and store none passphrase- prompts in the browser and prints only a reference to the staged value (keyctl:...orfile:...)- No terminal copy - the value never appears in the terminal, the shell history, a process argument or the CLI
copy- places a value on the clipboard through a one-shot relayshow- renders a code as a distorted image absent from the DOM text and the accessibility tree
Working with an AI agent
passphrase and copy let an AI agent handle a secret without printing it.
- Secret entry -
passphrase --once --prompt "..."gives an agent a reference, never a secret - Pipe -
pass-clican send bytes intocopywithout printing them - Help and skill -
--helpsays what each subcommand prints and whether it waits, andSKILL.mdadds the rules it cannot state - Why link - the wheel installs the skill at
<sys.prefix>/share/jupyter/agents/skills/jupyterlab-passkey-extension/SKILL.md, and no agent reads that directory, so one of the two links below makes it readable
mkdir -p ~/.agents/skills && ln -sfn "$(python -c 'import sys; print(sys.prefix)')/share/jupyter/agents/skills/jupyterlab-passkey-extension" ~/.agents/skills/jupyterlab-passkey-extension
ln -sfn "$PWD/.agents/skills/jupyterlab-passkey-extension" ~/.claude/skills/jupyterlab-passkey-extension
How it works
A browser page can send data only over HTTP, so the ceremony result returns through an authenticated endpoint that writes a relay - a keyctl key or an atomic 0600 file - the local client reads.
- Ceremony - the browser sends the ceremony result to the local client
copy- the local client sends a secret to the browser, and the notification contains only a nonce (see Security)
flowchart LR
subgraph BROWSER["Browser - JupyterLab page"]
direction TB
TRIG["consumer triggers<br/>passkey:run"]
CER["navigator.credentials<br/>get / create"]
OS(["OS / authenticator<br/>Windows Hello, security key"])
TRIG --> CER
CER <--> OS
end
subgraph SERVER["Jupyter server"]
direction TB
EP["POST /result<br/>authenticated"]
RELAY[("relay<br/>keyctl key or 0600 file")]
EP --> RELAY
end
LOCAL["local client<br/>terminal / CLI / API"]
CER -->|"POST JSON result"| EP
RELAY -->|"reads, then deletes"| LOCAL
style BROWSER stroke:#6b7280,stroke-width:3px
style SERVER stroke:#6b7280,stroke-width:3px
style TRIG stroke:#f59e0b,stroke-width:2px
style CER stroke:#10b981,stroke-width:2px
style OS stroke:#0284c7,stroke-width:2px
style EP stroke:#10b981,stroke-width:2px
style RELAY stroke:#3b82f6,stroke-width:2px
style LOCAL stroke:#10b981,stroke-width:2px
flowchart LR
LOCAL2["local client<br/>CLI / agent"]
subgraph SERVER2["Jupyter server"]
direction TB
ING["notifications ingest<br/>separate extension"]
RELAY2[("relay<br/>keyctl key or 0600 file")]
EP2["POST /secret<br/>authenticated"]
RELAY2 -->|"reads, then unlinks"| EP2
end
subgraph BROWSER2["Browser - JupyterLab page"]
direction TB
BTN["notification button<br/>passkey:copy"]
CLIP(["user's clipboard"])
BTN --> CLIP
end
LOCAL2 -->|"stages the secret"| RELAY2
LOCAL2 -->|"posts the nonce only"| ING
ING -.->|"sends to the page"| BTN
BTN -->|"collects, once"| EP2
EP2 -->|"the value"| BTN
style BROWSER2 stroke:#6b7280,stroke-width:3px
style SERVER2 stroke:#6b7280,stroke-width:3px
style LOCAL2 stroke:#10b981,stroke-width:2px
style ING stroke:#f59e0b,stroke-width:2px
style RELAY2 stroke:#3b82f6,stroke-width:2px
style EP2 stroke:#10b981,stroke-width:2px
style BTN stroke:#f59e0b,stroke-width:2px
style CLIP stroke:#0284c7,stroke-width:2px
Install
pip install jupyterlab_passkey_extension
Command line
jupyterlab-passkey is installed with the package and posts the notification with the request, waits for your click, and prints the result.
- Caller - every notification starts with
Asked by <name>:, set with--caller NAMEon any subcommand orJLAB_PASSKEY_CALLERfrom Python - Caller default - without a name the caller is the project directory, which tells the person who clicks which project asks and proves nothing
- Server - the CLI posts only to the Jupyter server its environment names, and stops when it cannot identify which running server is the lab (flags: cli-reference.md)
- Exit status -
0succeeded,1refused, timed out or could not reach the server,2an argument the parser rejects - Stdout - only the result goes to stdout, so
$(...)captures only the result copy- returns once posted,--blockwaits for collection only, and a terminal stdin is refusedshow- reports nothing back
| Command | Does | Prints |
|---|---|---|
create |
registers a new passkey | its cred_id |
get |
asserts a passkey | the PRF (with --prf-salt) or the cred_id |
passphrase |
prompts you for a secret in the browser | a reference (keyctl:... or file:...), never the value |
copy |
puts a secret on your clipboard | nothing - it posts a button and returns |
show |
shows a code as a scraper-resistant image | nothing - it posts a button and returns |
cred_id=$(jupyterlab-passkey create --rp-id lab.example.com)
salt=$(head -c32 /dev/urandom | base64 | tr '+/' '-_' | tr -d '=')
prf=$(jupyterlab-passkey get --rp-id lab.example.com --cred-id "$cred_id" --prf-salt "$salt")
pass-cli get github/api --field password --quiet --no-clipboard \
| jupyterlab-passkey copy --label "GitHub token"
Documentation
The vault, its panel and configuration, the server API, the frontend commands and the security notes are in docs/usage.md.
Requirements
- Server and gesture - JupyterLab >= 4.0.0 over HTTPS or localhost (a secure context), and an open tab supplies the gesture
- Hostname - a passkey belongs to a hostname, never an IP
- Dependencies - the notifications extension is installed with the package
- Passkeys -
createandgetneed a passkey, whilepassphrase,copyandshowneed none - GnuPG - the vault needs it (
apt install gnupg) so gpg-agent can hold the unlocked key where the kernel keyring is not allowed - Without GnuPG - the vault still works with the key in server memory and reports this
Uninstall
pip uninstall jupyterlab_passkey_extension
License
BSD-3-Clause; see LICENSE.
Metadata
Release files for jupyterlab-passkey-extension 1.1.58
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| jupyterlab_passkey_extension-1.1.58.tar.gz | 766.0 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| jupyterlab_passkey_extension-1.1.58-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.0 MB
Release files / jupyterlab_passkey_extension-1.1.58.tar.gz
| Download URL | jupyterlab_passkey_extension-1.1.58.tar.gz |
|---|---|
| Size | 766.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7ea81116772c8054aad2aedd2c125debeebe0f7fd630f3f8c655d755ce5deb9f
|
|
BLAKE2b-256 checksum How to use checksums |
1c6a525bf3ae4b0312c9810a3e222c15db9d22e725dc8d1f79b41bfcb69888d8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.9
|
Release files / jupyterlab_passkey_extension-1.1.58-py3-none-any.whl
| Download URL | jupyterlab_passkey_extension-1.1.58-py3-none-any.whl |
|---|---|
| Size | 238.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
151df257228ffd6ba08a4c84dde13c79f67e4602e96547f5d4cffcb6cfd0fe5a
|
|
BLAKE2b-256 checksum How to use checksums |
31330f5c01cac37857ef14f369baa93344756d8d6283bc60a8fe5decdc237f22
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.9
|
Security for jupyterlab-passkey-extension 1.1.58
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.