Skip to content

Commit 6bb6bc0

Browse files
authored
Merge pull request #822 from minrk/oidc
add OIDCOAuthenticator
2 parents 6128138 + 02c3305 commit 6bb6bc0

9 files changed

Lines changed: 593 additions & 118 deletions

File tree

docs/source/tutorials/provider-specific-setup/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ providers/globus.md
2424
providers/google.md
2525
providers/mediawiki.md
2626
providers/openshift.md
27+
providers/oidc.md
2728
providers/generic.md
2829
```

docs/source/tutorials/provider-specific-setup/providers/generic.md

Lines changed: 4 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -2,47 +2,12 @@
22

33
# Generic OAuthenticator setups for various identity providers
44

5-
(tutorials:provider-specific:generic:oidc)=
6-
75
## Setup for an OpenID Connect (OIDC) based identity provider
86

9-
The GenericOAuthenticator can be configured to be used against an OpenID Connect
10-
(OIDC) based identity provider, and this is an example demonstrating that.
11-
12-
```python
13-
c.JupyterHub.authenticator_class = "generic-oauth"
14-
15-
# OAuth2 application info
16-
# -----------------------
17-
c.GenericOAuthenticator.client_id = "some-client-id"
18-
c.GenericOAuthenticator.client_secret = "some-often-long-client-secret"
19-
20-
# Identity provider info
21-
# ----------------------
22-
c.GenericOAuthenticator.authorize_url =
23-
c.GenericOAuthenticator.token_url = "https://accounts.example.com/auth/realms/example/protocol/openid-connect/token"
24-
c.GenericOAuthenticator.userdata_url = "https://accounts.example.com/auth/realms/example/protocol/openid-connect/userinfo"
25-
26-
# What we request about the user
27-
# ------------------------------
28-
# scope represents requested information about the user, and since we configure
29-
# this against an OIDC based identity provider, we should request "openid" at
30-
# least.
31-
#
32-
# In this example we include "email" and "groups" as well, and then declare that
33-
# we should set the username based on the "email" key in the response, and read
34-
# group membership from the "groups" key in the response.
35-
#
36-
c.GenericOAuthenticator.scope = ["openid", "email", "groups"]
37-
c.GenericOAuthenticator.username_claim = "email"
38-
c.GenericOAuthenticator.auth_state_groups_key = "oauth_user.groups"
39-
40-
# Authorization
41-
# -------------
42-
c.GenericOAuthenticator.allowed_users = {"user1@example.com"}
43-
c.GenericOAuthenticator.allowed_groups = {"staff"}
44-
c.GenericOAuthenticator.admin_users = {"user2@example.com"}
45-
c.GenericOAuthenticator.admin_groups = {"administrator"}
7+
```{note}
8+
OIDC Configuration has been simplified by adding [OIDCOAuthenticator](./oidc),
9+
which is equivalent to GenericOAuthenticator but requires less configuration options
10+
by following the OIDC Discovery standard.
4611
```
4712

4813
(tutorials:provider-specific:generic:moodle)=
@@ -121,68 +86,3 @@ c.GenericOAuthenticator.authorize_url = "https://oauth.yandex.ru/authorize"
12186
c.GenericOAuthenticator.token_url = "https://oauth.yandex.ru/token"
12287
c.GenericOAuthenticator.userdata_url = "https://login.yandex.ru/info"
12388
```
124-
125-
(tutorials:provider-specific:generic:awscognito)=
126-
127-
## Setup for AWS Cognito
128-
129-
First visit AWS official documentation on [Getting started with user pools] for
130-
info on how to register and configure a cognito user pool and an associated
131-
OAuth2 application.
132-
133-
[Getting started with user pools]: https://docs.aws.amazon.com/cognito/latest/developerguide/getting-started-user-pools.html
134-
135-
Set the above settings in your `jupyterhub_config.py`:
136-
137-
```python
138-
c.JupyterHub.authenticator_class = "generic-oauth"
139-
c.OAuthenticator.oauth_callback_url = "https://[your-host]/hub/oauth_callback"
140-
c.OAuthenticator.client_id = "[your oauth2 application id]"
141-
c.OAuthenticator.client_secret = "[your oauth2 application secret]"
142-
143-
c.GenericOAuthenticator.login_service = "AWS Cognito"
144-
c.GenericOAuthenticator.username_claim = "login"
145-
146-
c.GenericOAuthenticator.authorize_url = "https://your-AWSCognito-domain/oauth2/authorize"
147-
c.GenericOAuthenticator.token_url = "https://your-AWSCognito-domain/oauth2/token"
148-
c.GenericOAuthenticator.userdata_url = "https://your-AWSCognito-domain/oauth2/userInfo"
149-
```
150-
151-
## Setup for ORCID iD
152-
153-
```{note}
154-
The `GenericOAuthenticator` will by default lowercase your username. For example, an ORCID iD of `0000-0002-9079-593X` will produce a JupyterHub username of `0000-0002-9079-593x`.
155-
```
156-
157-
Follow the ORCID [API Tutorial](https://info.orcid.org/documentation/api-tutorials/api-tutorial-get-and-authenticated-orcid-id/) to create an application via the Developer Tools submenu after clicking on your name in the top right of the page.
158-
159-
Edit your `jupyterhub_config.py` with the following:
160-
161-
```python
162-
c.JupyterHub.authenticator_class = "generic-oauth"
163-
164-
# Fill these in with your values
165-
c.GenericOAuthenticator.oauth_callback_url = "YOUR CALLBACK URL"
166-
c.GenericOAuthenticator.client_id = "YOUR CLIENT ID"
167-
c.GenericOAuthenticator.client_secret = "YOUR CLIENT SECRET"
168-
169-
c.GenericOAuthenticator.login_service = "ORCID iD" # Text of login button
170-
c.GenericOAuthenticator.authorize_url = "https://orcid.org/oauth/authorize"
171-
c.GenericOAuthenticator.token_url = "https://orcid.org/oauth/token"
172-
c.GenericOAuthenticator.scope = ["/authenticate", "openid"]
173-
c.GenericOAuthenticator.userdata_url = "https://orcid.org/oauth/userinfo"
174-
c.GenericOAuthenticator.username_claim = "sub"
175-
```
176-
177-
The above `username_claim` value selects the ORCID iD from the JSON response as the individual's JupyterHub username. An example response is below:
178-
179-
```json
180-
{
181-
"sub": "0000-0002-2601-8132",
182-
"name": "Credit Name",
183-
"family_name": "Jones",
184-
"given_name": "Tom"
185-
}
186-
```
187-
188-
Please refer to the [Authorization Code Flow](https://github.com/ORCID/ORCID-Source/blob/main/orcid-web/ORCID_AUTH_WITH_OPENID_CONNECT.md#authorization-code-flow) section of the ORCID documentation for more information.
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
(tutorials:provider-specific:oidc)=
2+
(tutorials:provider-specific:generic:oidc)=
3+
4+
# OpenID Connect (OIDC) Setup
5+
6+
{class}`.OIDCOAuthenticator` is an extension of GenericOAuthenticator,
7+
but which loads some standard configuration from `.well-known/openid-configuration`.
8+
This means you'll have fewer options that you need to configure for most
9+
OIDC providers.
10+
11+
## JupyterHub configuration
12+
13+
Your `jupyterhub_config.py` file should look something like this:
14+
15+
```python
16+
c.JupyterHub.authenticator_class = "oidc"
17+
c.OAuthenticator.oauth_callback_url = "https://[your-domain]/hub/oauth_callback"
18+
c.OAuthenticator.client_id = "[your oauth2 application id]"
19+
c.OAuthenticator.client_secret = "[your oauth2 application secret]"
20+
c.OIDCOAuthenticator.openid_provider_url = "https://yourprovider.example.org"
21+
```
22+
23+
`openid_provider_url` should be the base URL of your provider.
24+
OIDCOAuthenticator will fetch `{openid_provider_url}/.well-known/openid-configuration`
25+
to set up the following configuration:
26+
27+
| OAuthenticator option | openid-configuration key | notes |
28+
| --------------------- | ------------------------ | ----------------------- |
29+
| `authorize_url` | `authorization_endpoint` | |
30+
| `token_url` | `token_endpoint` | |
31+
| `jwks_uri` | `jwks_uri` | for verifying id tokens |
32+
| `jwt_issuer` | `issuer` | for verifying id tokens |
33+
| `userdata_url` | `userinfo_endpoint` | if defined (not always) |
34+
35+
You can get the exact same behavior as `OIDCOAuthenticator` with the base `OAuthenticator`, if you set all of these parameters by hand.
36+
37+
```{note}
38+
not all providers define `userinfo_endpoint`.
39+
You can _either_ set `c.OAuthenticator.userdata_url`,
40+
or set `c.OAuthenticator.userdata_from_id_token = True` to rely on the claims in the `id_token` of the token response.
41+
```
42+
43+
Examples of `openid_provider_url` for common providers:
44+
45+
- auth0: `https://$yourdomain.auth0.com`
46+
- github: `https://github.com/login/oauth`
47+
- google: `https://accounts.google.com`
48+
- orcid: `https://orcid.org`
49+
50+
## Additional configuration
51+
52+
Typically, when configuring with OIDC, you'll need to configure the `scope`, which will always include `openid`.
53+
The remaining scopes may vary.
54+
55+
The default `username_claim` for OIDCOAuthenticator is `sub`,
56+
but is very likely to vary, depending on your provider.
57+
Make sure that you use a _verified_ and _unique_ claim from your
58+
59+
```python
60+
c.OIDCOAuthenticator.scope = ["openid", "email", "groups"]
61+
c.OIDCOAuthenticator.username_claim = "sub" # the default
62+
c.OIDCOAuthenticator.auth_state_groups_key = "oauth_user.groups"
63+
```
64+
65+
You will likely want to set the `user`
66+
67+
And as with all Authenticators, you will need to `allow` specific users or groups.
68+
69+
(tutorials:provider-specific:generic:orcid)=
70+
(tutorials:provider-specific:oidc:orcid)=
71+
72+
## Setup for ORCID iD
73+
74+
```{note}
75+
The `OAuthenticator` will by default lowercase your username. For example, an ORCID iD of `0000-0002-9079-593X` will produce a JupyterHub username of `0000-0002-9079-593x`.
76+
```
77+
78+
Follow the ORCID [API Tutorial](https://info.orcid.org/documentation/api-tutorials/api-tutorial-get-and-authenticated-orcid-id/) to create an application via the Developer Tools submenu after clicking on your name in the top right of the page.
79+
80+
Edit your `jupyterhub_config.py` with the following:
81+
82+
```python
83+
c.JupyterHub.authenticator_class = "oidc"
84+
85+
# Fill these in with your values
86+
c.OIDCOAuthenticator.oauth_callback_url = "YOUR CALLBACK URL"
87+
c.OIDCOAuthenticator.client_id = "YOUR CLIENT ID"
88+
c.OIDCOAuthenticator.client_secret = "YOUR CLIENT SECRET"
89+
90+
c.OIDCOAuthenticator.login_service = "ORCID iD" # Text of login button
91+
c.OIDCOAuthenticator.openid_provider_url = "https://orcid.org"
92+
c.GenericOAuthenticator.scope = ["/authenticate", "openid"]
93+
```
94+
95+
The default `username_claim` of `sub` selects the ORCID iD from the JSON response as the individual's JupyterHub username. An example response is below:
96+
97+
```json
98+
{
99+
"sub": "0000-0002-2601-8132",
100+
"name": "Credit Name",
101+
"family_name": "Jones",
102+
"given_name": "Tom"
103+
}
104+
```
105+
106+
Please refer to the [Authorization Code Flow](https://github.com/ORCID/ORCID-Source/blob/main/orcid-web/ORCID_AUTH_WITH_OPENID_CONNECT.md#authorization-code-flow) section of the ORCID documentation for more information.
107+
108+
(tutorials:provider-specific:generic:awscognito)=
109+
(tutorials:provider-specific:oidc:awscognito)=
110+
111+
## Setup for AWS Cognito
112+
113+
First visit AWS official documentation on [Getting started with user pools] for
114+
info on how to register and configure a cognito user pool and an associated
115+
OAuth2 application.
116+
117+
[Getting started with user pools]: https://docs.aws.amazon.com/cognito/latest/developerguide/getting-started-user-pools.html
118+
119+
Set the above settings in your `jupyterhub_config.py`:
120+
121+
```python
122+
c.JupyterHub.authenticator_class = "oidc"
123+
c.OAuthenticator.oauth_callback_url = "https://[your-host]/hub/oauth_callback"
124+
c.OAuthenticator.client_id = "[your oauth2 application id]"
125+
c.OAuthenticator.client_secret = "[your oauth2 application secret]"
126+
127+
c.OAuthenticator.login_service = "AWS Cognito"
128+
c.OAuthenticator.username_claim = "login"
129+
c.OIDCOAuthenticator.openid_provider_url = "https://your-AWSCognito-domain"
130+
```

oauthenticator/oauth2.py

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
Bool,
2929
Callable,
3030
Dict,
31+
Instance,
3132
List,
3233
Set,
3334
Unicode,
@@ -493,6 +494,21 @@ def _token_url_default(self):
493494
""",
494495
)
495496

497+
jwt_issuer = Unicode(
498+
None,
499+
allow_none=True,
500+
config=True,
501+
help="""
502+
Set the issuer for validating JWT id_token issuers.
503+
504+
If set, issuer will be validated.
505+
506+
OIDC sets this from openid-configuration, no need to configure.
507+
508+
.. versionadded:: 17.4
509+
""",
510+
)
511+
496512
userdata_url = Unicode(
497513
config=True,
498514
help="""
@@ -1107,6 +1123,59 @@ async def get_token_info(self, handler, params):
11071123

11081124
return token_info
11091125

1126+
jwks_uri = Unicode(
1127+
config=True,
1128+
help="""
1129+
URI for JSON Web Keys (JWKs)
1130+
1131+
e.g. for OpenID Connect clients.
1132+
1133+
Used for verifying signatures of JWTs.
1134+
1135+
Default: unset, signatures will not be verified
1136+
(which is secure, per OIDC spec (core v1.0 § 3.1.3.7.6)).
1137+
1138+
OIDCOAuthenticator sets this from openid-configuration, no need to configure.
1139+
1140+
.. versionadded:: 17.4
1141+
""",
1142+
)
1143+
1144+
jwks_client = Instance(jwt.PyJWKClient, allow_none=True, default_value=None)
1145+
1146+
@default("jwks_client")
1147+
def _default_jwks_client(self):
1148+
if not self.jwks_uri:
1149+
return None
1150+
self.log.debug(f"Loading jwks client from {self.jwks_uri}")
1151+
return jwt.PyJWKClient(self.jwks_uri)
1152+
1153+
async def decode_jwt(self, token):
1154+
"""Validate and decode a JSON Web Token (JWT)"""
1155+
1156+
# if a jwks client is configured, verify signature
1157+
if self.jwks_client:
1158+
signing_key = self.jwks_client.get_signing_key_from_jwt(token)
1159+
else:
1160+
signing_key = None
1161+
# Here we parse the id token. Note that per OIDC spec (core v1.0 sect. 3.1.3.7.6) we can skip
1162+
# signature validation as the hub has obtained the tokens from the id provider directly (using https).
1163+
# Google suggests all token validation may be skipped assuming the provider is trusted.
1164+
# https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
1165+
# https://developers.google.com/identity/openid-connect/openid-connect#obtainuserinfo
1166+
return jwt.decode(
1167+
token,
1168+
key=signing_key,
1169+
audience=self.client_id,
1170+
issuer=self.jwt_issuer,
1171+
options=dict(
1172+
verify_signature=signing_key is not None,
1173+
verify_aud=True,
1174+
verify_exp=True,
1175+
verify_iss=self.jwt_issuer is not None,
1176+
),
1177+
)
1178+
11101179
async def token_to_user(self, token_info):
11111180
"""
11121181
Determines who the logged-in user by sending a "GET" request to
@@ -1132,18 +1201,7 @@ async def token_to_user(self, token_info):
11321201
f"An id token was not returned: {token_info}\nPlease configure authenticator.userdata_url",
11331202
)
11341203
try:
1135-
# Here we parse the id token. Note that per OIDC spec (core v1.0 sect. 3.1.3.7.6) we can skip
1136-
# signature validation as the hub has obtained the tokens from the id provider directly (using
1137-
# https). Google suggests all token validation may be skipped assuming the provider is trusted.
1138-
# https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
1139-
# https://developers.google.com/identity/openid-connect/openid-connect#obtainuserinfo
1140-
return jwt.decode(
1141-
id_token,
1142-
audience=self.client_id,
1143-
options=dict(
1144-
verify_signature=False, verify_aud=True, verify_exp=True
1145-
),
1146-
)
1204+
return await self.decode_jwt(id_token)
11471205
except Exception as err:
11481206
raise web.HTTPError(
11491207
500, f"Unable to decode id token: {id_token}\n{err}"

0 commit comments

Comments
 (0)