Control admin group membership from an AWS Cognito user group

I have AWS Cognito setup with JupyterHub for user authentication. Now I am attempting to manage access to the Admin screen in jupyterhub by membership of the admin group in AWS Cognito.
I have attempted the following configuration but Admin access is not bestowed to the user in JupyterHub

debug:
  enabled: true
singleuser:
  defaultUrl: "/lab"
  extraEnv:
    JUPYTERHUB_SINGLEUSER_APP: "jupyter_server.serverapp.ServerApp"
ingress:
  enabled: true
  hosts:
    - myhost.com
  ingressClassName: alb
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:xxxxxxxxxx
    alb.ingress.kubernetes.io/ssl-policy: ELBSecurityPolicy-TLS-1-1-2017-01 
    alb.ingress.kubernetes.io/ssl-redirect: '443'
    alb.ingress.kubernetes.io/target-type: 'ip'
proxy:
  service:
    type: ClusterIP
hub:
  config:
    GenericOAuthenticator:
      client_id: zzzzzzzzz
      client_secret: xxxxxxxxxxxxxxxxxxxx
      oauth_callback_url: https://myhost.com/hub/oauth_callback
      authorize_url: https://mycognito.com/oauth2/authorize
      token_url: https://mycognito.com/oauth2/token
      userdata_url: https://mycognito.com/oauth2/userInfo
      logout_redirect_url: https://mycognito.com/logout?client_id=zzzzzzzzz&logout_uri=https://myhost.com/
      auto_login: True
      allow_all: True
      scope:
        - openid
        - profile
      username_claim: preferred_username
      claim_groups_key: cognito:groups
      admin_groups:
        - admin
    JupyterHub:
      authenticator_class: generic-oauth

Can you turn on debug logging and show us your JupyterHub logs corresponding to a login attempt?

1 Like

Looking at the logs at the bottom I can see The claim_groups_key cognito:groups does not exist in the user token

However if I manually do an oauth call I can see cognito:groups in the token

Generate token manually…

aws cognito-idp initiate-auth --auth-flow USER_PASSWORD_AUTH --auth-parameters "USERNAME=uuuuuuu,PASSWORD=pppppppp,SECRET_HASH=hhhhhhhhhhhh" --client-id xxxxxxxxxxxxxx

Manual token contents…

{
  "sub": "32582b67-7587-46c5-9afe-0620fc39018f",
  "cognito:groups": [
    "admin"
  ],
  "email_verified": true,
  "iss": "https://cognito-idp.us-west-2.amazonaws.com/wwwwww_xxxxxxx",
  "cognito:username": "32582b67-7587-46c5-9afe-0620fc39018f",
  "preferred_username": "testuser",
  "origin_jti": "4ab29b5a-8baf-46de-84bd-e4d13a17e039",
  "aud": "63t607f1gfidr5us8ek01rcr5m",
  "event_id": "d3f719b0-f37e-4912-abdd-d9229ba021c6",
  "token_use": "id",
  "auth_time": 1710746657,
  "exp": 1710750256,
  "iat": 1710746657,
  "jti": "91df7f92-5a8c-484b-9283-32fdd80b0ff4",
  "email": "uuuuuuuuu"
}

Logs…

[I 2024-03-18 07:27:01.122 JupyterHub log:191] 302 GET / -> /hub/ (@::ffff:172.31.40.86) 0.42ms
[I 2024-03-18 07:27:01.260 JupyterHub log:191] 302 GET /hub/ -> /hub/login?next=%2Fhub%2F (@::ffff:172.31.40.86) 0.54ms
[I 2024-03-18 07:27:01.399 JupyterHub log:191] 302 GET /hub/login?next=%2Fhub%2F -> /hub/oauth_login?next=%2Fhub%2F (@::ffff:172.31.40.86) 1.02ms
[I 2024-03-18 07:27:01.538 JupyterHub oauth2:97] OAuth redirect: https://myhost.com/hub/oauth_callback
[D 2024-03-18 07:27:01.538 JupyterHub base:587] Setting cookie oauthenticator-state: {'httponly': True, 'expires_days': 1}
[I 2024-03-18 07:27:01.539 JupyterHub log:191] 302 GET /hub/oauth_login?next=%2Fhub%2F -> https://mycognito.com/oauth2/authorize?response_type=code&redirect_uri=https%3A%2F%2Fmyhost.com%2Fhub%2Foauth_callback&client_id=xxxxxxxxxxxxxxxx&state=[secret]&scope=openid+profile (@::ffff:172.31.40.86) 1.26ms
[I 2024-03-18 07:27:02.924 JupyterHub log:191] 302 GET / -> /hub/ (@::ffff:172.31.40.86) 0.60ms
[I 2024-03-18 07:27:02.968 JupyterHub log:191] 302 GET / -> /hub/ (@::ffff:172.31.29.53) 0.57ms
[D 2024-03-18 07:27:03.120 JupyterHub log:191] 200 GET /hub/health (@172.31.31.225) 0.60ms
[D 2024-03-18 07:27:04.029 JupyterHub reflector:374] events watcher timeout
[D 2024-03-18 07:27:04.030 JupyterHub reflector:289] Connecting events watcher
[D 2024-03-18 07:27:04.623 JupyterHub reflector:374] pods watcher timeout
[D 2024-03-18 07:27:04.623 JupyterHub reflector:289] Connecting pods watcher
[D 2024-03-18 07:27:05.121 JupyterHub log:191] 200 GET /hub/health (@172.31.31.225) 0.97ms
[D 2024-03-18 07:27:05.121 JupyterHub log:191] 200 GET /hub/health (@172.31.31.225) 0.93ms
[W 2024-03-18 07:27:05.182 JupyterHub web:1869] 403 POST /hub/api/users/testuser/activity (172.31.27.80): Missing or invalid credentials.
[W 2024-03-18 07:27:05.182 JupyterHub log:191] 403 POST /hub/api/users/testuser/activity (@172.31.27.80) 2.02ms
[W 2024-03-18 07:27:05.543 JupyterHub web:1869] 403 POST /hub/api/users/testuser/activity (172.31.27.80): Missing or invalid credentials.
[W 2024-03-18 07:27:05.544 JupyterHub log:191] 403 POST /hub/api/users/testuser/activity (@172.31.27.80) 1.80ms
[W 2024-03-18 07:27:06.798 JupyterHub web:1869] 403 POST /hub/api/users/testuser/activity (172.31.27.80): Missing or invalid credentials.
[W 2024-03-18 07:27:06.798 JupyterHub log:191] 403 POST /hub/api/users/testuser/activity (@172.31.27.80) 1.77ms
[D 2024-03-18 07:27:07.120 JupyterHub log:191] 200 GET /hub/health (@172.31.31.225) 0.57ms
[D 2024-03-18 07:27:09.120 JupyterHub log:191] 200 GET /hub/health (@172.31.31.225) 0.58ms
[W 2024-03-18 07:27:09.829 JupyterHub web:1869] 403 POST /hub/api/users/testuser/activity (172.31.27.80): Missing or invalid credentials.
[W 2024-03-18 07:27:09.831 JupyterHub log:191] 403 POST /hub/api/users/testuser/activity (@172.31.27.80) 3.62ms
[W 2024-03-18 07:27:10.612 JupyterHub web:1869] 403 POST /hub/api/users/testuser/activity (172.31.27.80): Missing or invalid credentials.
[W 2024-03-18 07:27:10.614 JupyterHub log:191] 403 POST /hub/api/users/testuser/activity (@172.31.27.80) 3.12ms
[D 2024-03-18 07:27:11.120 JupyterHub log:191] 200 GET /hub/health (@172.31.31.225) 0.59ms
[D 2024-03-18 07:27:13.119 JupyterHub log:191] 200 GET /hub/health (@172.31.31.225) 0.60ms
[D 2024-03-18 07:27:14.039 JupyterHub reflector:374] events watcher timeout
[D 2024-03-18 07:27:14.039 JupyterHub reflector:289] Connecting events watcher
[D 2024-03-18 07:27:14.633 JupyterHub reflector:374] pods watcher timeout
[D 2024-03-18 07:27:14.633 JupyterHub reflector:289] Connecting pods watcher
[D 2024-03-18 07:27:15.120 JupyterHub log:191] 200 GET /hub/health (@172.31.31.225) 1.05ms
[D 2024-03-18 07:27:15.121 JupyterHub log:191] 200 GET /hub/health (@172.31.31.225) 1.14ms
[D 2024-03-18 07:27:16.931 JupyterHub proxy:880] Proxy: Fetching GET http://proxy-api:8001/api/routes
[D 2024-03-18 07:27:16.941 JupyterHub proxy:392] Checking routes
[D 2024-03-18 07:27:17.120 JupyterHub log:191] 200 GET /hub/health (@172.31.31.225) 0.55ms
[I 2024-03-18 07:27:17.941 JupyterHub log:191] 302 GET / -> /hub/ (@::ffff:172.31.40.86) 0.61ms
[I 2024-03-18 07:27:17.975 JupyterHub log:191] 302 GET / -> /hub/ (@::ffff:172.31.29.53) 0.53ms

[E 2024-03-18 07:27:18.752 JupyterHub generic:142] The claim_groups_key cognito:groups does not exist in the user token
[D 2024-03-18 07:27:18.754 JupyterHub roles:281] Assigning default role to User testuser
[D 2024-03-18 07:27:18.761 JupyterHub base:587] Setting cookie jupyterhub-session-id: {'httponly': True, 'path': '/'}
[D 2024-03-18 07:27:18.761 JupyterHub base:591] Setting cookie for testuser: jupyterhub-hub-login
[D 2024-03-18 07:27:18.761 JupyterHub base:587] Setting cookie jupyterhub-hub-login: {'httponly': True, 'path': '/hub/'}
[I 2024-03-18 07:27:18.761 JupyterHub base:837] User logged in: testuser
[I 2024-03-18 07:27:18.762 JupyterHub log:191] 302 GET /hub/oauth_callback?code=[secret]&state=[secret] -> /hub/ (@::ffff:172.31.40.86) 331.40ms
[I 2024-03-18 07:27:18.911 JupyterHub log:191] 302 GET /hub/ -> /user/testuser/ (testuser@::ffff:172.31.40.86) 1.96ms
[W 2024-03-18 07:27:18.993 JupyterHub web:1869] 403 POST /hub/api/users/testuser/activity (172.31.27.80): Missing or invalid credentials.
[W 2024-03-18 07:27:18.994 JupyterHub log:191] 403 POST /hub/api/users/testuser/activity (@172.31.27.80) 2.12ms
[D 2024-03-18 07:27:19.120 JupyterHub log:191] 200 GET /hub/health (@172.31.31.225) 0.58ms
[W 2024-03-18 07:27:19.211 JupyterHub log:191] 403 GET /hub/api/user (@172.31.27.80) 1.50ms
[D 2024-03-18 07:27:19.352 JupyterHub provider:420] Validating client id jupyterhub-user-testuser
[D 2024-03-18 07:27:19.353 oauthlib.oauth2.rfc6749.grant_types.authorization_code authorization_code:362] Validating redirection uri /user/testuser/oauth_callback for client jupyterhub-user-testuser.
[D 2024-03-18 07:27:19.353 oauthlib.oauth2.rfc6749.grant_types.base base:231] Using provided redirect_uri /user/testuser/oauth_callback
[D 2024-03-18 07:27:19.353 JupyterHub provider:495] validate_redirect_uri: client_id=jupyterhub-user-testuser, redirect_uri=/user/testuser/oauth_callback
[D 2024-03-18 07:27:19.355 oauthlib.oauth2.rfc6749.grant_types.base base:172] Validating access to scopes ['access:servers!server=testuser/', 'read:users:name!user', 'read:users:groups!user'] for client 'jupyterhub-user-testuser' (<OAuthClient(identifier='jupyterhub-user-testuser')>).
[D 2024-03-18 07:27:19.357 JupyterHub provider:622] Allowing request for scope(s) for jupyterhub-user-testuser:  read:users:groups!user,access:servers!server=testuser/,read:users:name!user
[D 2024-03-18 07:27:19.358 JupyterHub auth:320] Skipping oauth confirmation for <User(testuser 1/1 running)> accessing Server at /user/testuser/
[D 2024-03-18 07:27:19.358 oauthlib.oauth2.rfc6749.endpoints.authorization authorization:98] Dispatching response_type code request to <oauthlib.oauth2.rfc6749.grant_types.authorization_code.AuthorizationCodeGrant object at 0x7f8043cc2350>.
[D 2024-03-18 07:27:19.359 JupyterHub provider:420] Validating client id jupyterhub-user-testuser
[D 2024-03-18 07:27:19.359 oauthlib.oauth2.rfc6749.grant_types.authorization_code authorization_code:362] Validating redirection uri /user/testuser/oauth_callback for client jupyterhub-user-testuser.
[D 2024-03-18 07:27:19.359 oauthlib.oauth2.rfc6749.grant_types.base base:231] Using provided redirect_uri /user/testuser/oauth_callback
[D 2024-03-18 07:27:19.359 JupyterHub provider:495] validate_redirect_uri: client_id=jupyterhub-user-testuser, redirect_uri=/user/testuser/oauth_callback
[D 2024-03-18 07:27:19.360 oauthlib.oauth2.rfc6749.grant_types.base base:172] Validating access to scopes {'access:servers!server=testuser/', 'read:users:name!user', 'read:users:groups!user'} for client 'jupyterhub-user-testuser' (<OAuthClient(identifier='jupyterhub-user-testuser')>).
[D 2024-03-18 07:27:19.361 JupyterHub provider:622] Allowing request for scope(s) for jupyterhub-user-testuser:  read:users:groups!user,access:servers!server=testuser/,read:users:name!user
[D 2024-03-18 07:27:19.361 oauthlib.oauth2.rfc6749.grant_types.authorization_code authorization_code:245] Pre resource owner authorization validation ok for <oauthlib.Request SANITIZED>.
[D 2024-03-18 07:27:19.361 oauthlib.oauth2.rfc6749.grant_types.authorization_code authorization_code:170] Created authorization code grant {'code': 'FjZB0gjqtnbqE22xaWkJqWqYVRE0LY', 'state': 'eyJ1dWlkIjogIjAxOTdiZTUzZGE5ZTQwNzI4MThmODMwNWU4N2E2OGVkIiwgIm5leHRfdXJsIjogIi91c2VyL2JhbnNvbi9sYWI_In0'} for request <oauthlib.Request SANITIZED>.
[D 2024-03-18 07:27:19.363 oauthlib.oauth2.rfc6749.grant_types.authorization_code authorization_code:277] Saving grant {'code': 'FjZB0gjqtnbqE22xaWkJqWqYVRE0LY', 'state': 'eyJ1dWlkIjogIjAxOTdiZTUzZGE5ZTQwNzI4MThmODMwNWU4N2E2OGVkIiwgIm5leHRfdXJsIjogIi91c2VyL2JhbnNvbi9sYWI_In0'} for <oauthlib.Request SANITIZED>.
[D 2024-03-18 07:27:19.363 JupyterHub provider:246] Saving authorization code jupyterhub-user-testuser, FjZ..., (), {}
[I 2024-03-18 07:27:19.372 JupyterHub log:191] 302 GET /hub/api/oauth2/authorize?client_id=jupyterhub-user-testuser&redirect_uri=%2Fuser%2Ftestuser%2Foauth_callback&response_type=code&state=[secret] -> /user/testuser/oauth_callback?code=[secret]&state=[secret] (testuser@::ffff:172.31.40.86) 21.90ms
[D 2024-03-18 07:27:19.516 oauthlib.oauth2.rfc6749.endpoints.token token:112] Dispatching grant_type authorization_code request to <oauthlib.oauth2.rfc6749.grant_types.authorization_code.AuthorizationCodeGrant object at 0x7f8043cc2350>.
[D 2024-03-18 07:27:19.516 JupyterHub provider:57] authenticate_client <oauthlib.Request SANITIZED>
[D 2024-03-18 07:27:19.520 oauthlib.oauth2.rfc6749.grant_types.authorization_code authorization_code:533] Using provided redirect_uri /user/testuser/oauth_callback
[D 2024-03-18 07:27:19.520 JupyterHub provider:116] confirm_redirect_uri: client_id=jupyterhub-user-testuser, redirect_uri=/user/testuser/oauth_callback
[D 2024-03-18 07:27:19.520 oauthlib.oauth2.rfc6749.grant_types.authorization_code authorization_code:301] Token request validation ok for <oauthlib.Request SANITIZED>.
[D 2024-03-18 07:27:19.520 JupyterHub provider:344] Saving bearer token {'access_token': 'REDACTED', 'expires_in': 1209600, 'token_type': 'Bearer', 'scope': 'read:users:groups!user access:servers!server=testuser/ read:users:name!user', 'refresh_token': 'REDACTED'}
[D 2024-03-18 07:27:19.528 JupyterHub provider:204] Deleting oauth code FjZ... for jupyterhub-user-testuser
[I 2024-03-18 07:27:19.536 JupyterHub log:191] 200 POST /hub/api/oauth2/token (@172.31.27.80) 22.23ms
[D 2024-03-18 07:27:19.540 JupyterHub base:299] Recording first activity for <APIToken('fdol...', user='testuser', client_id='jupyterhub-user-testuser')>
[I 2024-03-18 07:27:19.547 JupyterHub log:191] 200 GET /hub/api/user (testuser@172.31.27.80) 9.12ms

GenericOAuthenticator.claim_groups_key can be a callable:

Is there any chance you could define a callable to debug the value of user_info? Something like the following (not tested so might need some adjustments):

hub:
  extraConfig:
    debuginfo.py: |
      def debug_claim_groups_key(user_info):
          print(user_info)
          return set(user_info["cognito:groups"])
      c.GenericOAuthenticator.claim_groups_key = debug_claim_groups_key

Thanks @manics
I applied the debug function and I can see that indeed cognito:groups is not in the token JupyterHub receives.

{‘sub’: ‘32582b67-7587-46c5-9afe-0620fc39018f’, ‘email_verified’: ‘true’, ‘preferred_username’: ‘user1’, ‘email’: ‘user1@myhost.com’, ‘username’: ‘32582b67-7587-46c5-9afe-0620fc39018f’}

Hmm do you know if JupyterHub is making a different type of auth call?
I had assumed JupyterHub uses what Cognito calls a USER_PASSWORD_AUTH call but perhaps it is different

I’m not familiar with the AWS Cognito settings. What happens if you use the default?

If you search this forum for cognito:groups it sounds like others have got this to work.

1 Like

There is no real “default” setup for aws cognito. Looking at what other users have done I have made the same setup… openid, profile, aws.cognito.signin.user.admin scope and Authorization code grant OAuth grant type.
I can see cognito:groups in the Id Token and Access Token returned when using aws cli to simulate an OAuth login. However in JupyterHub if I setup debugging like you showed me above, I see only a subset of properties in the user_info dict passed into debug_claim_groups_key. This makes me suspect that there is a bug or configuration problem with JupyterHub OAuthenticator rather than a Cognito config issue