Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/source/tutorials/provider-specific-setup/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ providers/globus.md
providers/google.md
providers/mediawiki.md
providers/openshift.md
providers/oidc.md
providers/generic.md
```
108 changes: 4 additions & 104 deletions docs/source/tutorials/provider-specific-setup/providers/generic.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,12 @@

# Generic OAuthenticator setups for various identity providers

(tutorials:provider-specific:generic:oidc)=

## Setup for an OpenID Connect (OIDC) based identity provider

The GenericOAuthenticator can be configured to be used against an OpenID Connect
(OIDC) based identity provider, and this is an example demonstrating that.

```python
c.JupyterHub.authenticator_class = "generic-oauth"

# OAuth2 application info
# -----------------------
c.GenericOAuthenticator.client_id = "some-client-id"
c.GenericOAuthenticator.client_secret = "some-often-long-client-secret"

# Identity provider info
# ----------------------
c.GenericOAuthenticator.authorize_url =
c.GenericOAuthenticator.token_url = "https://accounts.example.com/auth/realms/example/protocol/openid-connect/token"
c.GenericOAuthenticator.userdata_url = "https://accounts.example.com/auth/realms/example/protocol/openid-connect/userinfo"

# What we request about the user
# ------------------------------
# scope represents requested information about the user, and since we configure
# this against an OIDC based identity provider, we should request "openid" at
# least.
#
# In this example we include "email" and "groups" as well, and then declare that
# we should set the username based on the "email" key in the response, and read
# group membership from the "groups" key in the response.
#
c.GenericOAuthenticator.scope = ["openid", "email", "groups"]
c.GenericOAuthenticator.username_claim = "email"
c.GenericOAuthenticator.auth_state_groups_key = "oauth_user.groups"

# Authorization
# -------------
c.GenericOAuthenticator.allowed_users = {"user1@example.com"}
c.GenericOAuthenticator.allowed_groups = {"staff"}
c.GenericOAuthenticator.admin_users = {"user2@example.com"}
c.GenericOAuthenticator.admin_groups = {"administrator"}
```{note}
OIDC Configuration has been simplified by adding [OIDCOAuthenticator](./oidc),
which is equivalent to GenericOAuthenticator but requires less configuration options
by following the OIDC Discovery standard.
```

(tutorials:provider-specific:generic:moodle)=
Expand Down Expand Up @@ -121,68 +86,3 @@ c.GenericOAuthenticator.authorize_url = "https://oauth.yandex.ru/authorize"
c.GenericOAuthenticator.token_url = "https://oauth.yandex.ru/token"
c.GenericOAuthenticator.userdata_url = "https://login.yandex.ru/info"
```

(tutorials:provider-specific:generic:awscognito)=

## Setup for AWS Cognito

First visit AWS official documentation on [Getting started with user pools] for
info on how to register and configure a cognito user pool and an associated
OAuth2 application.

[Getting started with user pools]: https://docs.aws.amazon.com/cognito/latest/developerguide/getting-started-user-pools.html

Set the above settings in your `jupyterhub_config.py`:

```python
c.JupyterHub.authenticator_class = "generic-oauth"
c.OAuthenticator.oauth_callback_url = "https://[your-host]/hub/oauth_callback"
c.OAuthenticator.client_id = "[your oauth2 application id]"
c.OAuthenticator.client_secret = "[your oauth2 application secret]"

c.GenericOAuthenticator.login_service = "AWS Cognito"
c.GenericOAuthenticator.username_claim = "login"

c.GenericOAuthenticator.authorize_url = "https://your-AWSCognito-domain/oauth2/authorize"
c.GenericOAuthenticator.token_url = "https://your-AWSCognito-domain/oauth2/token"
c.GenericOAuthenticator.userdata_url = "https://your-AWSCognito-domain/oauth2/userInfo"
```

## Setup for ORCID iD

```{note}
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`.
```

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.

Edit your `jupyterhub_config.py` with the following:

```python
c.JupyterHub.authenticator_class = "generic-oauth"

# Fill these in with your values
c.GenericOAuthenticator.oauth_callback_url = "YOUR CALLBACK URL"
c.GenericOAuthenticator.client_id = "YOUR CLIENT ID"
c.GenericOAuthenticator.client_secret = "YOUR CLIENT SECRET"

c.GenericOAuthenticator.login_service = "ORCID iD" # Text of login button
c.GenericOAuthenticator.authorize_url = "https://orcid.org/oauth/authorize"
c.GenericOAuthenticator.token_url = "https://orcid.org/oauth/token"
c.GenericOAuthenticator.scope = ["/authenticate", "openid"]
c.GenericOAuthenticator.userdata_url = "https://orcid.org/oauth/userinfo"
c.GenericOAuthenticator.username_claim = "sub"
```

The above `username_claim` value selects the ORCID iD from the JSON response as the individual's JupyterHub username. An example response is below:

```json
{
"sub": "0000-0002-2601-8132",
"name": "Credit Name",
"family_name": "Jones",
"given_name": "Tom"
}
```

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.
130 changes: 130 additions & 0 deletions docs/source/tutorials/provider-specific-setup/providers/oidc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
(tutorials:provider-specific:oidc)=
(tutorials:provider-specific:generic:oidc)=

# OpenID Connect (OIDC) Setup

{class}`.OIDCOAuthenticator` is an extension of GenericOAuthenticator,
but which loads some standard configuration from `.well-known/openid-configuration`.
This means you'll have fewer options that you need to configure for most
OIDC providers.

## JupyterHub configuration

Your `jupyterhub_config.py` file should look something like this:

```python
c.JupyterHub.authenticator_class = "oidc"
c.OAuthenticator.oauth_callback_url = "https://[your-domain]/hub/oauth_callback"
c.OAuthenticator.client_id = "[your oauth2 application id]"
c.OAuthenticator.client_secret = "[your oauth2 application secret]"
c.JupyterHub.openid_provider_url = "https://yourprovider.example.org"
```

`openid_provider_url` should be the base URL of your provider.
OIDCOAuthenticator will fetch `{openid_provider_url}/.well-known/openid-configuration`
to set up the following configuration:

| OAuthenticator option | openid-configuration key | notes |
| --------------------- | ------------------------ | ----------------------- |
| `authorize_url` | `authorization_endpoint` | |
| `token_url` | `token_endpoint` | |
| `jwks_uri` | `jwks_uri` | for verifying id tokens |
| `jwt_issuer` | `issuer` | for verifying id tokens |
| `userdata_url` | `userinfo_endpoint` | if defined (not always) |

You can get the exact same behavior as `OIDCOAuthenticator` with the base `OAuthenticator`, if you set all of these parameters by hand.

```{note}
not all providers define `userinfo_endpoint`.
You can _either_ set `c.OAuthenticator.userdata_url`,
or set `c.OAuthenticator.userdata_from_id_token = True` to rely on the claims in the `id_token` of the token response.
```

Examples of `openid_provider_url` for common providers:

- auth0: `https://$yourdomain.auth0.com`
- github: `https://github.com/login/oauth`
- google: `https://accounts.google.com`
- orcid: `https://orcid.org`

## Additional configuration

Typically, when configuring with OIDC, you'll need to configure the `scope`, which will always include `openid`.
The remaining scopes may vary.

The default `username_claim` for OIDCOAuthenticator is `sub`,
but is very likely to vary, depending on your provider.
Make sure that you use a _verified_ and _unique_ claim from your

```python
c.OIDCOAuthenticator.scope = ["openid", "email", "groups"]
c.OIDCOAuthenticator.username_claim = "sub" # the default
c.OIDCOAuthenticator.auth_state_groups_key = "oauth_user.groups"
```

You will likely want to set the `user`

And as with all Authenticators, you will need to `allow` specific users or groups.

(tutorials:provider-specific:generic:orcid)=
(tutorials:provider-specific:oidc:orcid)=

## Setup for ORCID iD

```{note}
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`.
```

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.

Edit your `jupyterhub_config.py` with the following:

```python
c.JupyterHub.authenticator_class = "oidc"

# Fill these in with your values
c.OIDCOAuthenticator.oauth_callback_url = "YOUR CALLBACK URL"
c.OIDCOAuthenticator.client_id = "YOUR CLIENT ID"
c.OIDCOAuthenticator.client_secret = "YOUR CLIENT SECRET"

c.OIDCOAuthenticator.login_service = "ORCID iD" # Text of login button
c.OIDCOAuthenticator.openid_provider_url = "https://orcid.org"
c.GenericOAuthenticator.scope = ["/authenticate", "openid"]
```

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:

```json
{
"sub": "0000-0002-2601-8132",
"name": "Credit Name",
"family_name": "Jones",
"given_name": "Tom"
}
```

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.

(tutorials:provider-specific:generic:awscognito)=
(tutorials:provider-specific:oidc:awscognito)=

## Setup for AWS Cognito

First visit AWS official documentation on [Getting started with user pools] for
info on how to register and configure a cognito user pool and an associated
OAuth2 application.

[Getting started with user pools]: https://docs.aws.amazon.com/cognito/latest/developerguide/getting-started-user-pools.html

Set the above settings in your `jupyterhub_config.py`:

```python
c.JupyterHub.authenticator_class = "oidc"
c.OAuthenticator.oauth_callback_url = "https://[your-host]/hub/oauth_callback"
c.OAuthenticator.client_id = "[your oauth2 application id]"
c.OAuthenticator.client_secret = "[your oauth2 application secret]"

c.OAuthenticator.login_service = "AWS Cognito"
c.OAuthenticator.username_claim = "login"
c.OIDCOAuthenticator.openid_provider_url = "https://your-AWSCognito-domain"
```
82 changes: 70 additions & 12 deletions oauthenticator/oauth2.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
Bool,
Callable,
Dict,
Instance,
List,
Set,
Unicode,
Expand Down Expand Up @@ -493,6 +494,21 @@ def _token_url_default(self):
""",
)

jwt_issuer = Unicode(
None,
allow_none=True,
config=True,
help="""
Set the issuer for validating JWT id_token issuers.

If set, issuer will be validated.

OIDC sets this from openid-configuration, no need to configure.

.. versionadded:: 17.4
""",
)

userdata_url = Unicode(
config=True,
help="""
Expand Down Expand Up @@ -1107,6 +1123,59 @@ async def get_token_info(self, handler, params):

return token_info

jwks_uri = Unicode(
config=True,
help="""
URI for JSON Web Keys (JWKs)

e.g. for OpenID Connect clients.

Used for verifying signatures of JWTs.

Default: unset, signatures will not be verified
(which is secure, per OIDC spec (core v1.0 § 3.1.3.7.6)).

OIDCOAuthenticator sets this from openid-configuration, no need to configure.

.. versionadded:: 17.4
""",
)

jwks_client = Instance(jwt.PyJWKClient, allow_none=True, default_value=None)

@default("jwks_client")
def _default_jwks_client(self):
if not self.jwks_uri:
return None
self.log.debug(f"Loading jwks client from {self.jwks_uri}")
return jwt.PyJWKClient(self.jwks_uri)

async def decode_jwt(self, token):
"""Validate and decode a JSON Web Token (JWT)"""

# if a jwks client is configured, verify signature
if self.jwks_client:
signing_key = self.jwks_client.get_signing_key_from_jwt(token)
else:
signing_key = None
# Here we parse the id token. Note that per OIDC spec (core v1.0 sect. 3.1.3.7.6) we can skip
# signature validation as the hub has obtained the tokens from the id provider directly (using https).
# Google suggests all token validation may be skipped assuming the provider is trusted.
# https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
# https://developers.google.com/identity/openid-connect/openid-connect#obtainuserinfo
return jwt.decode(
token,
key=signing_key,
audience=self.client_id,
issuer=self.jwt_issuer,
options=dict(
verify_signature=signing_key is not None,
verify_aud=True,
verify_exp=True,
verify_iss=self.jwt_issuer is not None,
),
)

async def token_to_user(self, token_info):
"""
Determines who the logged-in user by sending a "GET" request to
Expand All @@ -1132,18 +1201,7 @@ async def token_to_user(self, token_info):
f"An id token was not returned: {token_info}\nPlease configure authenticator.userdata_url",
)
try:
# Here we parse the id token. Note that per OIDC spec (core v1.0 sect. 3.1.3.7.6) we can skip
# signature validation as the hub has obtained the tokens from the id provider directly (using
# https). Google suggests all token validation may be skipped assuming the provider is trusted.
# https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
# https://developers.google.com/identity/openid-connect/openid-connect#obtainuserinfo
return jwt.decode(
id_token,
audience=self.client_id,
options=dict(
verify_signature=False, verify_aud=True, verify_exp=True
),
)
return await self.decode_jwt(id_token)
except Exception as err:
raise web.HTTPError(
500, f"Unable to decode id token: {id_token}\n{err}"
Expand Down
Loading