This is an automated email from the ASF dual-hosted git repository.

yasithdev pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airavata-portals.git


The following commit(s) were added to refs/heads/main by this push:
     new 5da3e569c feat(portal): de-Thrift the auth login path + browser 
session-token bridge (Track D, D5) (#179)
5da3e569c is described below

commit 5da3e569c8b1bbf368ecdfdc32d902a27ab1cf5d
Author: Yasith Jayawardana <[email protected]>
AuthorDate: Mon Jun 8 21:26:59 2026 -0400

    feat(portal): de-Thrift the auth login path + browser session-token bridge 
(Track D, D5) (#179)
    
    The browser session login authenticated against Keycloak fine but then
    hung: login(request, user) -> user_logged_in -> signals.initialize_user_
    profile made a Thrift user_profile_client_pool.doesUserExist call, and
    gateway_groups_middleware made Thrift getGatewayGroups + group_manager
    calls -- both time out because the legacy Thrift server read-times-out.
    
    - signals.initialize_user_profile: use request.airavata.iam.does_user_
      exist; drop the Thrift initializeUserProfile (the gRPC backend
      provisions the profile server-side; admins are emailed for new complete
      profiles). Wrapped so a failure never blocks login.
    - middleware.set_admin_group_attributes / gateway_groups_middleware: use
      request.airavata.compute.get_gateway_groups() (admins_group_id /
      read_only_admins_group_id) and request.airavata.sharing.gm_get_all_
      groups_user_belongs() instead of the Thrift client + profile_service.
    - KeycloakTokenAuthentication: when there is no Bearer header, fall back to
      the session-stored ACCESS_TOKEN, so the existing browser session
      authenticates against the token-only API without a frontend change
      (interim until the frontend sends the token as a Bearer header).
    
    Verified end to end: cookie-jar login POST -> 302 to the dashboard in
    ~0.08s (was hanging), and a subsequent /api/projects/ call carrying only
    the session cookie -> 200 via the session-token bridge.
---
 .../django_airavata/apps/auth/middleware.py        | 21 +++++++------
 .../django_airavata/apps/auth/signals.py           | 36 +++++++++++++---------
 .../apps/auth/token_authentication.py              | 15 +++++++--
 3 files changed, 44 insertions(+), 28 deletions(-)

diff --git a/airavata-django-portal/django_airavata/apps/auth/middleware.py 
b/airavata-django-portal/django_airavata/apps/auth/middleware.py
index 53569325a..319e5fa63 100644
--- a/airavata-django-portal/django_airavata/apps/auth/middleware.py
+++ b/airavata-django-portal/django_airavata/apps/auth/middleware.py
@@ -1,5 +1,4 @@
 """Django Airavata Auth Middleware."""
-import copy
 import logging
 
 from django.conf import settings
@@ -31,16 +30,21 @@ def authz_token_middleware(get_response):
     return middleware
 
 
+def _gateway_groups_dict(request):
+    """Fetch the gateway's admin group ids via the gRPC compute facade."""
+    gg = request.airavata.compute.get_gateway_groups()
+    return {'adminsGroupId': gg.admins_group_id,
+            'readOnlyAdminsGroupId': gg.read_only_admins_group_id}
+
+
 def set_admin_group_attributes(request, gateway_groups=None):
     """Set is_gateway_admin and is_read_only_gateway_admin request attrs."""
     if gateway_groups is None:
-        gateway_groups = 
request.airavata_client.getGatewayGroups(request.authz_token)
-        gateway_groups = copy.deepcopy(gateway_groups.__dict__)
+        gateway_groups = _gateway_groups_dict(request)
     admins_group_id = gateway_groups['adminsGroupId']
     read_only_admins_group_id = gateway_groups['readOnlyAdminsGroupId']
-    group_manager_client = request.profile_service['group_manager']
-    group_memberships = group_manager_client.getAllGroupsUserBelongs(
-        request.authz_token, request.user.username + "@" + settings.GATEWAY_ID)
+    group_memberships = 
request.airavata.sharing.gm_get_all_groups_user_belongs(
+        request.user.username + "@" + settings.GATEWAY_ID)
     group_ids = [group.id for group in group_memberships]
     request.is_gateway_admin = admins_group_id in group_ids
     request.is_read_only_gateway_admin = read_only_admins_group_id in group_ids
@@ -63,10 +67,7 @@ def gateway_groups_middleware(get_response):
             # Load the GatewayGroups and check if user is in the Admins and/or
             # Read Only Admins groups
             if not request.session.get('GATEWAY_GROUPS'):
-                gateway_groups = request.airavata_client.getGatewayGroups(
-                    request.authz_token)
-                gateway_groups_dict = copy.deepcopy(gateway_groups.__dict__)
-                request.session['GATEWAY_GROUPS'] = gateway_groups_dict
+                request.session['GATEWAY_GROUPS'] = 
_gateway_groups_dict(request)
             set_admin_group_attributes(request, 
gateway_groups=request.session.get("GATEWAY_GROUPS"))
             # Gateway Admins are made 'superuser' in Django so they can edit
             # pages in the CMS
diff --git a/airavata-django-portal/django_airavata/apps/auth/signals.py 
b/airavata-django-portal/django_airavata/apps/auth/signals.py
index 466e20bff..96d180d69 100644
--- a/airavata-django-portal/django_airavata/apps/auth/signals.py
+++ b/airavata-django-portal/django_airavata/apps/auth/signals.py
@@ -7,7 +7,6 @@ from django.shortcuts import reverse
 from django.template import Context
 
 from django_airavata.apps.api.signals import user_added_to_group
-from django_airavata.utils import user_profile_client_pool
 
 from . import models, utils
 
@@ -38,20 +37,27 @@ def initialize_user_profile(sender, request, user, 
**kwargs):
     # have an Airavata user profile (See IAMAdminServices.enableUser). The
     # following is necessary for users coming from federated login who don't
     # need to verify their email.
-    if request.authz_token is not None:
-        if not user_profile_client_pool.doesUserExist(request.authz_token,
-                                                      user.username,
-                                                      settings.GATEWAY_ID):
-            if user.user_profile.is_complete:
-                
user_profile_client_pool.initializeUserProfile(request.authz_token)
-                log.info("initialized user profile for 
{}".format(user.username))
-                # Since user profile created, inform admins of new user
-                utils.send_new_user_email(
-                    request, user.username, user.email, user.first_name, 
user.last_name)
-                log.info("sent new user email for user 
{}".format(user.username))
-            else:
-                log.info(f"user profile not complete for {user.username}, "
-                         "skipping initializing Airavata user profile")
+    if request.authz_token is None:
+        return
+    try:
+        exists = request.airavata.iam.does_user_exist(
+            user.username, settings.GATEWAY_ID)
+    except Exception:
+        log.warning("Could not check Airavata user existence for %s",
+                    user.username, exc_info=True)
+        return
+    if not exists:
+        if user.user_profile.is_complete:
+            # New federated-login user with a complete profile. Inform admins;
+            # the Airavata user profile is provisioned server-side on first
+            # authenticated request.
+            utils.send_new_user_email(
+                request, user.username, user.email, user.first_name,
+                user.last_name)
+            log.info("sent new user email for user {}".format(user.username))
+        else:
+            log.info(f"user profile not complete for {user.username}, "
+                     "skipping initializing Airavata user profile")
 
     else:
         log.warning(f"Logged in user {user.username} has no access token")
diff --git 
a/airavata-django-portal/django_airavata/apps/auth/token_authentication.py 
b/airavata-django-portal/django_airavata/apps/auth/token_authentication.py
index cb5811be7..a78313d7d 100644
--- a/airavata-django-portal/django_airavata/apps/auth/token_authentication.py
+++ b/airavata-django-portal/django_airavata/apps/auth/token_authentication.py
@@ -65,9 +65,18 @@ class 
KeycloakTokenAuthentication(authentication.BaseAuthentication):
 
     def authenticate(self, request):
         header = request.META.get('HTTP_AUTHORIZATION', '')
-        if not header.startswith('Bearer '):
-            return None
-        token = header[len('Bearer '):].strip()
+        if header.startswith('Bearer '):
+            token = header[len('Bearer '):].strip()
+        else:
+            # Browser bridge: the session login flow stores the Keycloak access
+            # token in the session; use it when no Authorization header is sent
+            # so the existing browser session authenticates against the
+            # token-only API. (Final state: the frontend sends the token as a
+            # Bearer header and the session login is removed.)
+            session = getattr(request, 'session', None)
+            token = session.get('ACCESS_TOKEN') if session is not None else 
None
+            if not token:
+                return None
         try:
             signing_key = _jwks().get_signing_key_from_jwt(token)
             claims = jwt.decode(

Reply via email to