Skip to content

Commit

Permalink
Merge pull request #710 from minrk/azuread
Browse files Browse the repository at this point in the history
[AzureAD] Support `manage_groups`
  • Loading branch information
yuvipanda authored Dec 11, 2023
2 parents 280b988 + e29012d commit da8bd36
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 1 deletion.
17 changes: 17 additions & 0 deletions docs/source/tutorials/provider-specific-setup/providers/azuread.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,20 @@ AzureAdOAuthenticator expands OAuthenticator with the following config that may
be relevant to read more about in the configuration reference:

- {attr}`.AzureAdOAuthenticator.tenant_id`

## Loading user groups

The `AzureAdOAuthenticator` can load the group-membership of users from the access token.
This is done by setting the `AzureAdOAuthenticator.groups_claim` to the name of the claim that contains the
group-membership.

```python
c.JupyterHub.authenticator_class = "azuread"
# {...} other settings (see above)
c.AzureAdOAuthenticator.manage_groups = True
c.AzureAdOAuthenticator.user_groups_claim = 'groups' # this is the default
```

This requires Azure AD to be configured to include the group-membership in the access token.
19 changes: 19 additions & 0 deletions oauthenticator/azuread.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ def _login_service_default(self):
def _username_claim_default(self):
return "name"

user_groups_claim = Unicode(
"groups",
config=True,
help="""
Name of claim containing user group memberships.
Will populate JupyterHub groups if Authenticator.manage_groups is True.
""",
)

tenant_id = Unicode(
config=True,
help="""
Expand All @@ -44,6 +54,15 @@ def _authorize_url_default(self):
def _token_url_default(self):
return f"https://login.microsoftonline.com/{self.tenant_id}/oauth2/token"

async def update_auth_model(self, auth_model, **kwargs):
auth_model = await super().update_auth_model(auth_model, **kwargs)

if getattr(self, "manage_groups", False):
user_info = auth_model["auth_state"][self.user_auth_state_key]
auth_model["groups"] = user_info[self.user_groups_claim]

return auth_model

async def token_to_user(self, token_info):
id_token = token_info['id_token']
decoded = jwt.decode(
Expand Down
43 changes: 42 additions & 1 deletion oauthenticator/tests/test_azuread.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from unittest import mock

import jwt
import pytest
from pytest import fixture, mark
from traitlets.config import Config

Expand Down Expand Up @@ -44,6 +45,17 @@ def user_model(tenant_id, client_id, name):
"tid": tenant_id,
"nonce": "123523",
"aio": "Df2UVXL1ix!lMCWMSOJBcFatzcGfvFGhjKv8q5g0x732dR5MB5BisvGQO7YWByjd8iQDLq!eGbIDakyp5mnOrcdqHeYSnltepQmRp6AIZ8jY",
"groups": [
"96000b2c-7333-4f6e-a2c3-e7608fa2d131",
"a992b3d5-1966-4af4-abed-6ef021417be4",
"ceb90a42-030f-44f1-a0c7-825b572a3b07",
],
# different from 'groups' for tests
"grp": [
"96000b2c-7333-4f6e-a2c3",
"a992b3d5-1966-4af4-abed",
"ceb90a42-030f-44f1-a0c7",
],
},
os.urandom(5),
)
Expand Down Expand Up @@ -103,6 +115,23 @@ def user_model(tenant_id, client_id, name):
True,
None,
),
# test user_groups_claim
(
"30",
{"allow_all": True, "manage_groups": True},
True,
None,
),
(
"31",
{
"allow_all": True,
"manage_groups": True,
"user_groups_claim": "grp",
},
True,
None,
),
],
)
async def test_azuread(
Expand All @@ -119,6 +148,12 @@ async def test_azuread(
c.AzureAdOAuthenticator.client_id = str(uuid.uuid1())
c.AzureAdOAuthenticator.client_secret = str(uuid.uuid1())
authenticator = AzureAdOAuthenticator(config=c)
manage_groups = False
if "manage_groups" in class_config:
if hasattr(authenticator, "manage_groups"):
manage_groups = authenticator.manage_groups
else:
pytest.skip("manage_groups requires jupyterhub 2.2")

handled_user_model = user_model(
tenant_id=authenticator.tenant_id,
Expand All @@ -130,14 +165,20 @@ async def test_azuread(

if expect_allowed:
assert auth_model
assert set(auth_model) == {"name", "admin", "auth_state"}
expected_keys = {"name", "admin", "auth_state"}
if manage_groups:
expected_keys.add("groups")
assert set(auth_model) == expected_keys
assert auth_model["admin"] == expect_admin
auth_state = auth_model["auth_state"]
assert json.dumps(auth_state)
assert "access_token" in auth_state
user_info = auth_state[authenticator.user_auth_state_key]
assert user_info["aud"] == authenticator.client_id
assert auth_model["name"] == user_info[authenticator.username_claim]
if manage_groups:
groups = auth_model['groups']
assert groups == user_info[authenticator.user_groups_claim]
else:
assert auth_model == None

Expand Down

1 comment on commit da8bd36

@amedeopalopoli
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi yuvipanda can you help me how to configure AzureAdAuthenticator to enable an Azure AD Group to log in Jupyterhub? At the moment I'm able to login with allowed users, enabled manage_groups and I was able to see the "groups" field populated but I did not understand where configure in Jupyterhub which group should have access on and with which permissions (like admin_users and allowed_users..)

I used https://github.com/jupyterhub/helm-chart to deploy Jupyterhub in kubernetes

Below is my configuration

hub:
  config:
    Authenticator:
      admin_users:
        - test
    JupyterHub:
      authenticator_class: azuread
    AzureAdOAuthenticator:
      enable_auth_state: true
      manage_groups: true
      client_id: <myclientId>
      client_secret: <myclientSecret>
      tenant_id: <mytenantId>
      username_claim: "name"      
      oauth_callback_url: https://<jupyterurl>/hub/oauth_callback
      token_url: https://login.microsoftonline.com/<tenandId>/oauth2/token
      authorize_url: https://login.microsoftonline.com/<tenandId>/oauth2/authorize 
      scope:
        - email
        - openid
        - profile
        - name        

Below the logs

Loading /usr/local/etc/jupyterhub/secret/values.yaml
No config at /usr/local/etc/jupyterhub/existing-secret/values.yaml
[I 2023-12-19 20:31:55.757 JupyterHub app:2859] Running JupyterHub version 4.0.2
[I 2023-12-19 20:31:55.757 JupyterHub app:2889] Using Authenticator: oauthenticator.azuread.AzureAdOAuthenticator-16.2.1
[I 2023-12-19 20:31:55.758 JupyterHub app:2889] Using Spawner: kubespawner.spawner.KubeSpawner-6.2.0
[I 2023-12-19 20:31:55.758 JupyterHub app:2889] Using Proxy: jupyterhub.proxy.ConfigurableHTTPProxy-4.0.2
[I 2023-12-19 20:31:55.812 JupyterHub app:1984] Not using allowed_users. Any authenticated user will be allowed.
[I 2023-12-19 20:31:55.849 JupyterHub reflector:282] watching for pods with label selector='component=singleuser-server' in namespace jupyterhub
[W 2023-12-19 20:31:55.852 JupyterHub _version:37] Single-user server has no version header, which means it is likely < 0.8. Expected 4.0.2
[I 2023-12-19 20:31:55.852 JupyterHub app:2573] amedeo.palopoli still running
[I 2023-12-19 20:31:55.852 JupyterHub app:2928] Initialized 1 spawners in 0.016 seconds
[I 2023-12-19 20:31:55.855 JupyterHub metrics:278] Found 1 active users in the last ActiveUserPeriods.twenty_four_hours
[I 2023-12-19 20:31:55.855 JupyterHub metrics:278] Found 2 active users in the last ActiveUserPeriods.seven_days
[I 2023-12-19 20:31:55.856 JupyterHub metrics:278] Found 2 active users in the last ActiveUserPeriods.thirty_days
[I 2023-12-19 20:31:55.856 JupyterHub app:3142] Not starting proxy
[I 2023-12-19 20:31:55.859 JupyterHub app:3178] Hub API listening on http://:8081/hub/
[I 2023-12-19 20:31:55.859 JupyterHub app:3180] Private Hub API connect url http://hub:8081/hub/
[I 2023-12-19 20:31:55.859 JupyterHub app:3189] Starting managed service jupyterhub-idle-culler
[I 2023-12-19 20:31:55.859 JupyterHub service:385] Starting service 'jupyterhub-idle-culler': ['python3', '-m', 'jupyterhub_idle_culler', '--url=http://localhost:8081/hub/api', '--timeout=3600', '--cull-every=600', '--concurrency=10']
[I 2023-12-19 20:31:55.859 JupyterHub service:133] Spawning python3 -m jupyterhub_idle_culler --url=http://localhost:8081/hub/api --timeout=3600 --cull-every=600 --concurrency=10

Loading /usr/local/etc/jupyterhub/secret/values.yaml
No config at /usr/local/etc/jupyterhub/existing-secret/values.yaml
[I 2023-12-19 20:31:55.757 JupyterHub app:2859] Running JupyterHub version 4.0.2
[I 2023-12-19 20:31:55.757 JupyterHub app:2889] Using Authenticator: oauthenticator.azuread.AzureAdOAuthenticator-16.2.1
[I 2023-12-19 20:31:55.758 JupyterHub app:2889] Using Spawner: kubespawner.spawner.KubeSpawner-6.2.0
[I 2023-12-19 20:31:55.758 JupyterHub app:2889] Using Proxy: jupyterhub.proxy.ConfigurableHTTPProxy-4.0.2
[I 2023-12-19 20:31:55.812 JupyterHub app:1984] Not using allowed_users. Any authenticated user will be allowed.
[I 2023-12-19 20:31:55.849 JupyterHub reflector:282] watching for pods with label selector='component=singleuser-server' in namespace jupyterhub
[W 2023-12-19 20:31:55.852 JupyterHub _version:37] Single-user server has no version header, which means it is likely < 0.8. Expected 4.0.2
[I 2023-12-19 20:31:55.852 JupyterHub app:2573] amedeo.palopoli still running
[I 2023-12-19 20:31:55.852 JupyterHub app:2928] Initialized 1 spawners in 0.016 seconds
[I 2023-12-19 20:31:55.855 JupyterHub metrics:278] Found 1 active users in the last ActiveUserPeriods.twenty_four_hours
[I 2023-12-19 20:31:55.855 JupyterHub metrics:278] Found 2 active users in the last ActiveUserPeriods.seven_days
[I 2023-12-19 20:31:55.856 JupyterHub metrics:278] Found 2 active users in the last ActiveUserPeriods.thirty_days
[I 2023-12-19 20:31:55.856 JupyterHub app:3142] Not starting proxy
[I 2023-12-19 20:31:55.859 JupyterHub app:3178] Hub API listening on http://:8081/hub/
[I 2023-12-19 20:31:55.859 JupyterHub app:3180] Private Hub API connect url http://hub:8081/hub/
[I 2023-12-19 20:31:55.859 JupyterHub app:3189] Starting managed service jupyterhub-idle-culler
[I 2023-12-19 20:31:55.859 JupyterHub service:385] Starting service 'jupyterhub-idle-culler': ['python3', '-m', 'jupyterhub_idle_culler', '--url=http://localhost:8081/hub/api', '--timeout=3600', '--cull-every=600', '--concurrency=10']
[I 2023-12-19 20:31:55.859 JupyterHub service:133] Spawning python3 -m jupyterhub_idle_culler --url=http://localhost:8081/hub/api --timeout=3600 --cull-every=600 --concurrency=10
[I 2023-12-19 20:31:55.860 JupyterHub app:3247] JupyterHub is now running, internal Hub API at http://hub:8081/hub/[I 2023-12-19 20:31:55.860 JupyterHub app:3247] JupyterHub is now running, internal Hub API at http://hub:8081/hub/

I receive 403 and it's saying the user is not allowed even though I put the user in a group Jupyterhub has been receiveing from AAD after user logged in.

Please sign in to comment.