ipyauth-pkce 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. ipyauth_pkce-1.0.0/.gitignore +30 -0
  2. ipyauth_pkce-1.0.0/LICENSE +22 -0
  3. ipyauth_pkce-1.0.0/PKG-INFO +239 -0
  4. ipyauth_pkce-1.0.0/README.md +218 -0
  5. ipyauth_pkce-1.0.0/install.json +5 -0
  6. ipyauth_pkce-1.0.0/ipyauth_pkce/__init__.py +10 -0
  7. ipyauth_pkce-1.0.0/ipyauth_pkce/ipyauth_callback/__init__.py +2 -0
  8. ipyauth_pkce-1.0.0/ipyauth_pkce/ipyauth_callback/ipyauth-pkce.json +7 -0
  9. ipyauth_pkce-1.0.0/ipyauth_pkce/ipyauth_callback/server_extension.py +336 -0
  10. ipyauth_pkce-1.0.0/ipyauth_pkce/ipyauth_callback/templates/assets/main.js +46 -0
  11. ipyauth_pkce-1.0.0/ipyauth_pkce/ipyauth_callback/templates/assets/util.js +136 -0
  12. ipyauth_pkce-1.0.0/ipyauth_pkce/ipyauth_callback/templates/index.html +16 -0
  13. ipyauth_pkce-1.0.0/ipyauth_pkce/ipyauth_widget/__init__.py +7 -0
  14. ipyauth_pkce-1.0.0/ipyauth_pkce/ipyauth_widget/_config.py +7 -0
  15. ipyauth_pkce-1.0.0/ipyauth_pkce/ipyauth_widget/_params_auth0.py +37 -0
  16. ipyauth_pkce-1.0.0/ipyauth_pkce/ipyauth_widget/_params_base.py +73 -0
  17. ipyauth_pkce-1.0.0/ipyauth_pkce/ipyauth_widget/_params_google.py +39 -0
  18. ipyauth_pkce-1.0.0/ipyauth_pkce/ipyauth_widget/_params_keycloak.py +45 -0
  19. ipyauth_pkce-1.0.0/ipyauth_pkce/ipyauth_widget/_util.py +95 -0
  20. ipyauth_pkce-1.0.0/ipyauth_pkce/ipyauth_widget/_widget_box.py +174 -0
  21. ipyauth_pkce-1.0.0/ipyauth_pkce/ipyauth_widget/img/auth0-logo.png +0 -0
  22. ipyauth_pkce-1.0.0/ipyauth_pkce/ipyauth_widget/img/google-logo.png +0 -0
  23. ipyauth_pkce-1.0.0/ipyauth_pkce/ipyauth_widget/img/keycloak-logo.png +0 -0
  24. ipyauth_pkce-1.0.0/ipyauth_pkce/js/.babelrc +8 -0
  25. ipyauth_pkce-1.0.0/ipyauth_pkce/js/.eslintignore +3 -0
  26. ipyauth_pkce-1.0.0/ipyauth_pkce/js/.eslintrc.json +36 -0
  27. ipyauth_pkce-1.0.0/ipyauth_pkce/js/.prettierrc +6 -0
  28. ipyauth_pkce-1.0.0/ipyauth_pkce/js/README.md +25 -0
  29. ipyauth_pkce-1.0.0/ipyauth_pkce/js/package-lock.json +11829 -0
  30. ipyauth_pkce-1.0.0/ipyauth_pkce/js/package.json +69 -0
  31. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/__tests__/auth-error-display.test.js +163 -0
  32. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/__tests__/authorize-url.test.js +105 -0
  33. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/__tests__/auto-login-attempt.test.js +92 -0
  34. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/__tests__/automatic-renewal.test.js +61 -0
  35. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/__tests__/broadcast-channel-isolation.test.js +90 -0
  36. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/__tests__/callback-state-validation.test.js +144 -0
  37. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/__tests__/callback-util.test.js +149 -0
  38. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/__tests__/focus-next-cell.test.js +107 -0
  39. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/__tests__/id-token-validation.test.js +188 -0
  40. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/__tests__/oidc-providers.test.js +122 -0
  41. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/__tests__/popup-manual-close.test.js +200 -0
  42. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/__tests__/popup-user-activation.test.js +59 -0
  43. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/__tests__/property-auth-url-pkce.test.js +122 -0
  44. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/__tests__/property-jwt-roundtrip.test.js +95 -0
  45. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/__tests__/property-keycloak-audience.test.js +138 -0
  46. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/__tests__/property-origin-mismatch.test.js +136 -0
  47. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/__tests__/property-pkce-challenge.test.js +70 -0
  48. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/__tests__/property-pkce-verifier.test.js +52 -0
  49. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/__tests__/property-popup-blocked-bug-condition.test.js +256 -0
  50. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/__tests__/property-popup-blocked-preservation.test.js +163 -0
  51. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/__tests__/property-security-bug-condition.test.js +324 -0
  52. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/__tests__/property-security-preservation.test.js +199 -0
  53. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/__tests__/seconds-to-exp.test.js +27 -0
  54. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/app_commands.js +95 -0
  55. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/labplugin.js +32 -0
  56. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/providers/auth0.js +14 -0
  57. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/providers/google.js +71 -0
  58. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/providers/index.js +21 -0
  59. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/providers/keycloak.js +15 -0
  60. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/test-setup/setup-crypto.js +42 -0
  61. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/widget.js +1 -0
  62. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/widget_auth.js +626 -0
  63. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/widget_box.js +125 -0
  64. ipyauth_pkce-1.0.0/ipyauth_pkce/js/src/widget_util.js +344 -0
  65. ipyauth_pkce-1.0.0/ipyauth_pkce/labextension/package.json +73 -0
  66. ipyauth_pkce-1.0.0/ipyauth_pkce/labextension/static/218.f367b7ba1b52d7bdfccd.js +1 -0
  67. ipyauth_pkce-1.0.0/ipyauth_pkce/labextension/static/442.460d9c3da800991e4a71.js +2 -0
  68. ipyauth_pkce-1.0.0/ipyauth_pkce/labextension/static/442.460d9c3da800991e4a71.js.LICENSE.txt +10 -0
  69. ipyauth_pkce-1.0.0/ipyauth_pkce/labextension/static/remoteEntry.68f8fd1f6638911fce79.js +1 -0
  70. ipyauth_pkce-1.0.0/ipyauth_pkce/labextension/static/style.js +4 -0
  71. ipyauth_pkce-1.0.0/ipyauth_pkce/labextension/static/third-party-licenses.json +34 -0
  72. ipyauth_pkce-1.0.0/pyproject.toml +102 -0
@@ -0,0 +1,30 @@
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Build artifacts
5
+ ipyauth_pkce/js/dist/
6
+ ipyauth_pkce/labextension/
7
+ dist/
8
+ build/
9
+
10
+ # Python
11
+ __pycache__/
12
+ *.py[cod]
13
+ *.egg-info/
14
+ *.egg
15
+ .pytest_cache/
16
+ .hypothesis/
17
+
18
+ # Virtual environments
19
+ .venv*/
20
+
21
+ # Environment / secrets
22
+ .env
23
+ *.env.local
24
+ *.env
25
+
26
+ # IDE / tools
27
+ .kiro/
28
+
29
+ # Local scratch notebooks
30
+ notebooks/test-*.ipynb
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Olivier Borderies
4
+ Copyright (c) 2026 Daniel Fernández (https://github.com/danifr)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in
14
+ all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ THE SOFTWARE.
@@ -0,0 +1,239 @@
1
+ Metadata-Version: 2.5
2
+ Name: ipyauth-pkce
3
+ Version: 1.0.0
4
+ Summary: Jupyter widget for OAuth2 authentication with PKCE — sign in from a notebook cell and use the token in Python
5
+ Project-URL: Homepage, https://github.com/danifr/ipyauth-pkce
6
+ Project-URL: Repository, https://github.com/danifr/ipyauth-pkce
7
+ Author: danifr
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: Auth0,Google,Keycloak,OAuth2,PKCE,ipywidget,javascript
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Python: >=3.12
17
+ Requires-Dist: ipywidgets<9,>=8.0
18
+ Requires-Dist: jupyter-server>=2.13.0
19
+ Requires-Dist: python-dotenv>=1.0
20
+ Description-Content-Type: text/markdown
21
+
22
+ # ipyauth-pkce
23
+
24
+ A [Jupyter widget](https://ipywidgets.readthedocs.io/en/stable/) that lets notebook users authenticate via OAuth2 (Authorization Code with PKCE) directly from a notebook cell.
25
+
26
+ After signing in, the access token is available in the Python kernel for making requests to protected APIs.
27
+
28
+ ## Demo
29
+
30
+ ![demo](https://raw.githubusercontent.com/danifr/ipyauth-pkce/main/assets/screen_recording.gif)
31
+
32
+ ## Supported Providers
33
+
34
+ - Keycloak (fully tested with PKCE)
35
+ - Auth0
36
+ - Google
37
+
38
+ ## Requirements
39
+
40
+ - Python >= 3.12
41
+ - JupyterLab >= 4.0
42
+ - ipywidgets >= 8.0, < 9
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ pip install ipyauth-pkce
48
+ ```
49
+
50
+ The package installs both the Python widget and the JupyterLab extension automatically.
51
+
52
+ ## Usage (Keycloak)
53
+
54
+ ```python
55
+ from ipyauth_pkce import ParamsKeycloak, Auth
56
+
57
+ params = ParamsKeycloak(
58
+ base_url='https://your-keycloak-server/auth',
59
+ realm='your-realm',
60
+ client_id='your-client-id',
61
+ redirect_uri='http://localhost:8888/callback/',
62
+ scope='openid profile',
63
+ )
64
+
65
+ session = Auth(params=params)
66
+ session # displays the widget — click "Sign In"
67
+ ```
68
+
69
+ `redirect_uri` must be your Jupyter server's URL followed by `/callback/`, and
70
+ it must be registered as an allowed redirect URI for the client at your
71
+ provider. The `scope` must include `openid` and `profile`. Values can also be
72
+ loaded from a dotenv file: `ParamsKeycloak(dotenv_folder='.',
73
+ dotenv_file='ipyauth-keycloak-myrealm.env')` (the file name must start with
74
+ `ipyauth-<provider>-` and end with `.env`); explicit arguments override it.
75
+
76
+ The widget tries to open the sign-in popup as soon as it is displayed; if the
77
+ browser blocks that, click "Sign In".
78
+
79
+ After authenticating:
80
+
81
+ ```python
82
+ session.access_token # the JWT access token
83
+ session.headers # {'Authorization': 'Bearer eyJ...'}
84
+ session.is_authenticated # True / False
85
+ session.logged_as # user email or name
86
+ session.expires_at # expiry timestamp
87
+
88
+ # Use with requests
89
+ import requests
90
+ r = requests.get('https://api.example.com/data', headers=session.headers)
91
+ ```
92
+
93
+ ### Other Providers
94
+
95
+ ```python
96
+ from ipyauth_pkce import ParamsAuth0, Auth
97
+
98
+ params = ParamsAuth0(
99
+ domain='your-tenant.auth0.com',
100
+ client_id='your-client-id',
101
+ redirect_uri='http://localhost:8888/callback/',
102
+ scope='openid profile',
103
+ )
104
+ session = Auth(params=params)
105
+ ```
106
+
107
+ ```python
108
+ from ipyauth_pkce import ParamsGoogle, Auth
109
+
110
+ params = ParamsGoogle(
111
+ client_id='your-client-id.apps.googleusercontent.com',
112
+ redirect_uri='http://localhost:8888/callback/',
113
+ scope='openid profile email',
114
+ )
115
+ session = Auth(params=params)
116
+ ```
117
+
118
+ ## How It Works
119
+
120
+ 1. The widget opens a popup window pointing to the OAuth provider's authorization endpoint
121
+ 2. The user authenticates in the popup
122
+ 3. The provider redirects back to `/callback/` with an authorization code
123
+ 4. The callback page sends the code back to the widget via `postMessage`
124
+ 5. The widget asks the Jupyter server to exchange the code for tokens (PKCE: a public client, no client secret)
125
+ 6. The widget validates the ID token, and the access token becomes available in the Python kernel
126
+
127
+ When the token expires the widget clears it; sign in again to get a new one (refresh tokens are not used).
128
+
129
+ ## Security
130
+
131
+ - Authorization Code flow with PKCE (no implicit flow, no client secrets in the browser)
132
+ - Server-side token exchange via Jupyter server extension
133
+ - XSRF protection on the token exchange endpoint
134
+ - Origin validation on postMessage callbacks
135
+ - Content-Security-Policy headers on the callback page
136
+ - Input validation on all OAuth parameters (SSRF protection, RFC 7636 compliance)
137
+ - OIDC ID-token validation (issuer, audience, subject, expiry, issued-at, nonce, and provider JWKS signature)
138
+ - Google bearer tokens are sent in Authorization headers rather than API query strings
139
+
140
+ ### Token endpoint configuration
141
+
142
+ The server-side token exchange decides which token endpoint to send the
143
+ authorization code and PKCE verifier to — it never blindly trusts a
144
+ browser-supplied URL, because that endpoint is the one place a leaked code
145
+ could be redeemed by an attacker.
146
+
147
+ - **Google** works with no configuration: its fixed endpoint
148
+ (`https://oauth2.googleapis.com/token`) is allowed by default.
149
+ - **Any other provider** (Keycloak, Auth0, other OIDC servers, or local
150
+ development) requires a single environment variable on the Jupyter server,
151
+ set before Jupyter starts:
152
+
153
+ ```bash
154
+ export IPYAUTH_TOKEN_ENDPOINT="https://your-provider/realms/your-realm/protocol/openid-connect/token"
155
+ ```
156
+
157
+ When `IPYAUTH_TOKEN_ENDPOINT` is set, the server uses that value and ignores
158
+ whatever the browser sends, so the token exchange always targets a trusted
159
+ endpoint. The value must be an HTTPS URL (plain `http` is accepted only for
160
+ `localhost` during development) with no credentials, query string, or fragment.
161
+
162
+ If a non-Google provider is used without `IPYAUTH_TOKEN_ENDPOINT` set, the
163
+ token exchange is rejected and the widget never receives an access token. The
164
+ server responds with:
165
+
166
+ > Token exchange endpoint not configured. Set the IPYAUTH_TOKEN_ENDPOINT
167
+ > environment variable on the Jupyter server to your provider's token endpoint.
168
+
169
+ In managed or multi-user deployments this variable is set once by the server
170
+ operator in the Jupyter server environment.
171
+
172
+ ## Architecture
173
+
174
+ - `ipyauth_pkce/ipyauth_callback/` — Jupyter server extension that serves the OAuth callback page and handles token exchange
175
+ - `ipyauth_pkce/ipyauth_widget/` — Python widget (ipywidgets VBox)
176
+ - `ipyauth_pkce/js/` — JavaScript frontend (JupyterLab extension)
177
+
178
+ ## Development
179
+
180
+ Requires Node.js (24 or newer) in addition to Python. The JupyterLab extension in
181
+ `ipyauth_pkce/labextension/` is a build output and is not committed: it is built
182
+ from `ipyauth_pkce/js` whenever the package is built or installed from source.
183
+
184
+ ```bash
185
+ # Virtual environment (the dev/ scripts expect it at .venv; override with VENV=...)
186
+ python3 -m venv .venv && source .venv/bin/activate
187
+
188
+ # Editable install; also builds the JupyterLab extension
189
+ pip install -e . pytest "jupyterlab>=4,<5" jupyter-builder
190
+ cd ipyauth_pkce/js && npm ci
191
+
192
+ # JS lint and tests (from ipyauth_pkce/js)
193
+ npm run lint
194
+ npm test
195
+
196
+ # Rebuild the JupyterLab extension after changing ipyauth_pkce/js/src
197
+ npm run build
198
+
199
+ # Python tests (from the repo root)
200
+ pytest
201
+
202
+ # Build the wheel and sdist (the version comes from the latest git tag).
203
+ # An existing ipyauth_pkce/labextension is reused, so run `npm run build` first.
204
+ hatch build
205
+ ```
206
+
207
+ To release, publish a GitHub release with a new tag `vX.Y.Z`: CI runs the tests,
208
+ builds the extension and uploads version `X.Y.Z` to PyPI.
209
+
210
+ The JavaScript widget has its own version (`ipyauth_pkce/js/package.json`, and
211
+ `_FRONTEND_VERSION` in `_widget_box.py`, which must match). Bump both only when
212
+ the contract between the Python and JavaScript sides changes, such as a synced
213
+ trait being added, renamed or removed.
214
+
215
+ ### Containers
216
+
217
+ `dev/` contains helper scripts that use Podman (the Dockerfiles also work with
218
+ Docker if you run the commands by hand). They use the `.venv` environment
219
+ from above (or the one set in `VENV`); `run-test.sh` additionally needs
220
+ `pip install hatch-vcs hatch-jupyter-builder` in it.
221
+
222
+ ```bash
223
+ # Build the wheel and a JupyterLab image that installs it, then run it.
224
+ # Pass the token endpoint for non-Google providers.
225
+ dev/run-test.sh "https://keycloak.example.com/realms/my-realm/protocol/openid-connect/token"
226
+
227
+ # Fast loop: editable install with the source bind-mounted.
228
+ # --js rebuilds the labextension first; --watch rebuilds on every change.
229
+ dev/run-dev.sh "https://keycloak.example.com/realms/my-realm/protocol/openid-connect/token" --js
230
+ ```
231
+
232
+ Both start JupyterLab on http://localhost:8888 (override with `PORT=...`).
233
+ `run-test.sh` keeps Jupyter's token login (open the URL with `?token=...` from its
234
+ output); `run-dev.sh` disables authentication for convenience, so only use it on
235
+ a local machine.
236
+
237
+ ## Credits
238
+
239
+ This project is based on [ipyauth](https://gitlab.com/oscar6echo/ipyauth) by oscar6echo, with Keycloak support from the [e-ucm fork](https://github.com/e-ucm/ipyauth/tree/newKeycloak). This is a modernized version that uses the Authorization Code flow with PKCE instead of the legacy implicit flow. Tested with Python 3.12+, JupyterLab 4+, and modern versions of Keycloak that support PKCE.
@@ -0,0 +1,218 @@
1
+ # ipyauth-pkce
2
+
3
+ A [Jupyter widget](https://ipywidgets.readthedocs.io/en/stable/) that lets notebook users authenticate via OAuth2 (Authorization Code with PKCE) directly from a notebook cell.
4
+
5
+ After signing in, the access token is available in the Python kernel for making requests to protected APIs.
6
+
7
+ ## Demo
8
+
9
+ ![demo](https://raw.githubusercontent.com/danifr/ipyauth-pkce/main/assets/screen_recording.gif)
10
+
11
+ ## Supported Providers
12
+
13
+ - Keycloak (fully tested with PKCE)
14
+ - Auth0
15
+ - Google
16
+
17
+ ## Requirements
18
+
19
+ - Python >= 3.12
20
+ - JupyterLab >= 4.0
21
+ - ipywidgets >= 8.0, < 9
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install ipyauth-pkce
27
+ ```
28
+
29
+ The package installs both the Python widget and the JupyterLab extension automatically.
30
+
31
+ ## Usage (Keycloak)
32
+
33
+ ```python
34
+ from ipyauth_pkce import ParamsKeycloak, Auth
35
+
36
+ params = ParamsKeycloak(
37
+ base_url='https://your-keycloak-server/auth',
38
+ realm='your-realm',
39
+ client_id='your-client-id',
40
+ redirect_uri='http://localhost:8888/callback/',
41
+ scope='openid profile',
42
+ )
43
+
44
+ session = Auth(params=params)
45
+ session # displays the widget — click "Sign In"
46
+ ```
47
+
48
+ `redirect_uri` must be your Jupyter server's URL followed by `/callback/`, and
49
+ it must be registered as an allowed redirect URI for the client at your
50
+ provider. The `scope` must include `openid` and `profile`. Values can also be
51
+ loaded from a dotenv file: `ParamsKeycloak(dotenv_folder='.',
52
+ dotenv_file='ipyauth-keycloak-myrealm.env')` (the file name must start with
53
+ `ipyauth-<provider>-` and end with `.env`); explicit arguments override it.
54
+
55
+ The widget tries to open the sign-in popup as soon as it is displayed; if the
56
+ browser blocks that, click "Sign In".
57
+
58
+ After authenticating:
59
+
60
+ ```python
61
+ session.access_token # the JWT access token
62
+ session.headers # {'Authorization': 'Bearer eyJ...'}
63
+ session.is_authenticated # True / False
64
+ session.logged_as # user email or name
65
+ session.expires_at # expiry timestamp
66
+
67
+ # Use with requests
68
+ import requests
69
+ r = requests.get('https://api.example.com/data', headers=session.headers)
70
+ ```
71
+
72
+ ### Other Providers
73
+
74
+ ```python
75
+ from ipyauth_pkce import ParamsAuth0, Auth
76
+
77
+ params = ParamsAuth0(
78
+ domain='your-tenant.auth0.com',
79
+ client_id='your-client-id',
80
+ redirect_uri='http://localhost:8888/callback/',
81
+ scope='openid profile',
82
+ )
83
+ session = Auth(params=params)
84
+ ```
85
+
86
+ ```python
87
+ from ipyauth_pkce import ParamsGoogle, Auth
88
+
89
+ params = ParamsGoogle(
90
+ client_id='your-client-id.apps.googleusercontent.com',
91
+ redirect_uri='http://localhost:8888/callback/',
92
+ scope='openid profile email',
93
+ )
94
+ session = Auth(params=params)
95
+ ```
96
+
97
+ ## How It Works
98
+
99
+ 1. The widget opens a popup window pointing to the OAuth provider's authorization endpoint
100
+ 2. The user authenticates in the popup
101
+ 3. The provider redirects back to `/callback/` with an authorization code
102
+ 4. The callback page sends the code back to the widget via `postMessage`
103
+ 5. The widget asks the Jupyter server to exchange the code for tokens (PKCE: a public client, no client secret)
104
+ 6. The widget validates the ID token, and the access token becomes available in the Python kernel
105
+
106
+ When the token expires the widget clears it; sign in again to get a new one (refresh tokens are not used).
107
+
108
+ ## Security
109
+
110
+ - Authorization Code flow with PKCE (no implicit flow, no client secrets in the browser)
111
+ - Server-side token exchange via Jupyter server extension
112
+ - XSRF protection on the token exchange endpoint
113
+ - Origin validation on postMessage callbacks
114
+ - Content-Security-Policy headers on the callback page
115
+ - Input validation on all OAuth parameters (SSRF protection, RFC 7636 compliance)
116
+ - OIDC ID-token validation (issuer, audience, subject, expiry, issued-at, nonce, and provider JWKS signature)
117
+ - Google bearer tokens are sent in Authorization headers rather than API query strings
118
+
119
+ ### Token endpoint configuration
120
+
121
+ The server-side token exchange decides which token endpoint to send the
122
+ authorization code and PKCE verifier to — it never blindly trusts a
123
+ browser-supplied URL, because that endpoint is the one place a leaked code
124
+ could be redeemed by an attacker.
125
+
126
+ - **Google** works with no configuration: its fixed endpoint
127
+ (`https://oauth2.googleapis.com/token`) is allowed by default.
128
+ - **Any other provider** (Keycloak, Auth0, other OIDC servers, or local
129
+ development) requires a single environment variable on the Jupyter server,
130
+ set before Jupyter starts:
131
+
132
+ ```bash
133
+ export IPYAUTH_TOKEN_ENDPOINT="https://your-provider/realms/your-realm/protocol/openid-connect/token"
134
+ ```
135
+
136
+ When `IPYAUTH_TOKEN_ENDPOINT` is set, the server uses that value and ignores
137
+ whatever the browser sends, so the token exchange always targets a trusted
138
+ endpoint. The value must be an HTTPS URL (plain `http` is accepted only for
139
+ `localhost` during development) with no credentials, query string, or fragment.
140
+
141
+ If a non-Google provider is used without `IPYAUTH_TOKEN_ENDPOINT` set, the
142
+ token exchange is rejected and the widget never receives an access token. The
143
+ server responds with:
144
+
145
+ > Token exchange endpoint not configured. Set the IPYAUTH_TOKEN_ENDPOINT
146
+ > environment variable on the Jupyter server to your provider's token endpoint.
147
+
148
+ In managed or multi-user deployments this variable is set once by the server
149
+ operator in the Jupyter server environment.
150
+
151
+ ## Architecture
152
+
153
+ - `ipyauth_pkce/ipyauth_callback/` — Jupyter server extension that serves the OAuth callback page and handles token exchange
154
+ - `ipyauth_pkce/ipyauth_widget/` — Python widget (ipywidgets VBox)
155
+ - `ipyauth_pkce/js/` — JavaScript frontend (JupyterLab extension)
156
+
157
+ ## Development
158
+
159
+ Requires Node.js (24 or newer) in addition to Python. The JupyterLab extension in
160
+ `ipyauth_pkce/labextension/` is a build output and is not committed: it is built
161
+ from `ipyauth_pkce/js` whenever the package is built or installed from source.
162
+
163
+ ```bash
164
+ # Virtual environment (the dev/ scripts expect it at .venv; override with VENV=...)
165
+ python3 -m venv .venv && source .venv/bin/activate
166
+
167
+ # Editable install; also builds the JupyterLab extension
168
+ pip install -e . pytest "jupyterlab>=4,<5" jupyter-builder
169
+ cd ipyauth_pkce/js && npm ci
170
+
171
+ # JS lint and tests (from ipyauth_pkce/js)
172
+ npm run lint
173
+ npm test
174
+
175
+ # Rebuild the JupyterLab extension after changing ipyauth_pkce/js/src
176
+ npm run build
177
+
178
+ # Python tests (from the repo root)
179
+ pytest
180
+
181
+ # Build the wheel and sdist (the version comes from the latest git tag).
182
+ # An existing ipyauth_pkce/labextension is reused, so run `npm run build` first.
183
+ hatch build
184
+ ```
185
+
186
+ To release, publish a GitHub release with a new tag `vX.Y.Z`: CI runs the tests,
187
+ builds the extension and uploads version `X.Y.Z` to PyPI.
188
+
189
+ The JavaScript widget has its own version (`ipyauth_pkce/js/package.json`, and
190
+ `_FRONTEND_VERSION` in `_widget_box.py`, which must match). Bump both only when
191
+ the contract between the Python and JavaScript sides changes, such as a synced
192
+ trait being added, renamed or removed.
193
+
194
+ ### Containers
195
+
196
+ `dev/` contains helper scripts that use Podman (the Dockerfiles also work with
197
+ Docker if you run the commands by hand). They use the `.venv` environment
198
+ from above (or the one set in `VENV`); `run-test.sh` additionally needs
199
+ `pip install hatch-vcs hatch-jupyter-builder` in it.
200
+
201
+ ```bash
202
+ # Build the wheel and a JupyterLab image that installs it, then run it.
203
+ # Pass the token endpoint for non-Google providers.
204
+ dev/run-test.sh "https://keycloak.example.com/realms/my-realm/protocol/openid-connect/token"
205
+
206
+ # Fast loop: editable install with the source bind-mounted.
207
+ # --js rebuilds the labextension first; --watch rebuilds on every change.
208
+ dev/run-dev.sh "https://keycloak.example.com/realms/my-realm/protocol/openid-connect/token" --js
209
+ ```
210
+
211
+ Both start JupyterLab on http://localhost:8888 (override with `PORT=...`).
212
+ `run-test.sh` keeps Jupyter's token login (open the URL with `?token=...` from its
213
+ output); `run-dev.sh` disables authentication for convenience, so only use it on
214
+ a local machine.
215
+
216
+ ## Credits
217
+
218
+ This project is based on [ipyauth](https://gitlab.com/oscar6echo/ipyauth) by oscar6echo, with Keycloak support from the [e-ucm fork](https://github.com/e-ucm/ipyauth/tree/newKeycloak). This is a modernized version that uses the Authorization Code flow with PKCE instead of the legacy implicit flow. Tested with Python 3.12+, JupyterLab 4+, and modern versions of Keycloak that support PKCE.
@@ -0,0 +1,5 @@
1
+ {
2
+ "packageManager": "python",
3
+ "packageName": "ipyauth-pkce",
4
+ "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package ipyauth-pkce"
5
+ }
@@ -0,0 +1,10 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ try:
4
+ __version__ = version("ipyauth-pkce")
5
+ except PackageNotFoundError: # running from a source checkout without installing
6
+ __version__ = "0.0.0+unknown"
7
+
8
+ from .ipyauth_widget import Auth, ParamsAuth0, ParamsGoogle, ParamsKeycloak
9
+
10
+ __all__ = ['Auth', 'ParamsAuth0', 'ParamsGoogle', 'ParamsKeycloak']
@@ -0,0 +1,2 @@
1
+ def _jupyter_server_extension_points():
2
+ return [{'module': 'ipyauth_pkce.ipyauth_callback.server_extension'}]
@@ -0,0 +1,7 @@
1
+ {
2
+ "ServerApp": {
3
+ "jpserver_extensions": {
4
+ "ipyauth_pkce.ipyauth_callback.server_extension": true
5
+ }
6
+ }
7
+ }