Skip to content

Commit bed1ffa

Browse files
committed
feat(config): add EOSC EU Node AAI SSO login method (#750)
Add support for EOSC EU Node AAI as a new SSO login provider by importing and configuring the EOSC AAI OAuth provider from `invenio-oauthclient`. The implementation introduces `EOSC_*` environment variables for consumer credentials and endpoint URLs and configures the remote REST app with proper OAuth URLs and scopes. A custom `invenio-oauthclient` branch is installed to provide the necessary EOSC AAI support without having to upgrade the full Invenio stack.
1 parent 5ae23c1 commit bed1ffa

File tree

5 files changed

+79
-13
lines changed

5 files changed

+79
-13
lines changed

Dockerfile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# This file is part of REANA.
2-
# Copyright (C) 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024 CERN.
2+
# Copyright (C) 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026 CERN.
33
#
44
# REANA is free software; you can redistribute it and/or modify it
55
# under the terms of the MIT License; see LICENSE file for more details.
@@ -65,7 +65,12 @@ RUN if test -e modules/reana-commons; then \
6565
fi \
6666
fi
6767

68+
# Install custom invenio-oauthclient branch
69+
# hadolint ignore=DL3013
70+
RUN pip install --no-cache-dir --force-reinstall --no-deps "git+https://github.com/tiborsimko/invenio-oauthclient.git@reana-een-aai"
71+
6872
# A quick fix to allow eduGAIN and social login users that wouldn't otherwise match Invenio username rules
73+
# hadolint ignore=DL3059
6974
RUN sed -i 's|^username_regex = re.compile\(.*\)$|username_regex = re.compile("^\\S+$")|g' /usr/local/lib/python3.12/dist-packages/invenio_userprofiles/validators.py
7075

7176
# Check for any broken Python dependencies

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (C) 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025 CERN.
3+
Copyright (C) 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026 CERN.
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy of
66
this software and associated documentation files (the "Software"), to deal in

reana_server/config.py

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- coding: utf-8 -*-
22
#
33
# This file is part of REANA.
4-
# Copyright (C) 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025 CERN.
4+
# Copyright (C) 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026 CERN.
55
#
66
# REANA is free software; you can redistribute it and/or modify it
77
# under the terms of the MIT License; see LICENSE file for more details.
@@ -17,7 +17,7 @@
1717
from distutils.util import strtobool
1818
from limits.util import parse
1919
from invenio_app.config import APP_DEFAULT_SECURE_HEADERS
20-
from invenio_oauthclient.contrib import cern_openid
20+
from invenio_oauthclient.contrib import cern_openid, eosc_aai
2121
from invenio_oauthclient.contrib.keycloak import KeycloakSettingsHelper
2222
from reana_commons.config import REANA_INFRASTRUCTURE_COMPONENTS_HOSTNAMES
2323
from reana_commons.job_utils import kubernetes_memory_to_bytes
@@ -52,6 +52,26 @@
5252
"https://auth.cern.ch/auth/realms/cern/protocol/openid-connect/userinfo",
5353
)
5454

55+
REANA_SSO_EOSC_CONSUMER_KEY = os.getenv("EOSC_CONSUMER_KEY", "CHANGE_ME")
56+
REANA_SSO_EOSC_CONSUMER_SECRET = os.getenv("EOSC_CONSUMER_SECRET", "CHANGE_ME")
57+
REANA_SSO_EOSC_BASE_URL = os.getenv(
58+
"EOSC_BASE_URL", "https://proxy.testing.eosc-federation.eu"
59+
)
60+
REANA_SSO_EOSC_REQUIRED_ENTITLEMENT = os.getenv("EOSC_REQUIRED_ENTITLEMENT", "")
61+
"""Required EOSC AAI entitlement for login. If set, users must have this entitlement to access REANA."""
62+
REANA_SSO_EOSC_TOKEN_URL = os.getenv(
63+
"EOSC_TOKEN_URL",
64+
f"{REANA_SSO_EOSC_BASE_URL}/OIDC/token",
65+
)
66+
REANA_SSO_EOSC_AUTH_URL = os.getenv(
67+
"EOSC_AUTH_URL",
68+
f"{REANA_SSO_EOSC_BASE_URL}/OIDC/authorization",
69+
)
70+
REANA_SSO_EOSC_USERINFO_URL = os.getenv(
71+
"EOSC_USERINFO_URL",
72+
f"{REANA_SSO_EOSC_BASE_URL}/OIDC/userinfo",
73+
)
74+
5575
# Load Login Providers Configuration and Secrets as JSON from environment variables
5676
REANA_SSO_LOGIN_PROVIDERS = json.loads(os.getenv("LOGIN_PROVIDERS_CONFIGS", "[]"))
5777
REANA_SSO_LOGIN_PROVIDERS_SECRETS = json.loads(
@@ -372,14 +392,14 @@ def _get_rate_limit(env_variable: str, default: str) -> str:
372392
OAUTHCLIENT_REST_REMOTE_APPS["keycloak"] = KEYCLOAK_REST_APP
373393

374394
# CERN SSO configuration
375-
OAUTH_REMOTE_REST_APP = copy.deepcopy(cern_openid.REMOTE_REST_APP)
376-
OAUTH_REMOTE_REST_APP.update(
395+
CERN_REMOTE_REST_APP = copy.deepcopy(cern_openid.REMOTE_REST_APP)
396+
CERN_REMOTE_REST_APP.update(
377397
{
378398
"authorized_redirect_url": OAUTH_REDIRECT_URL,
379399
"error_redirect_url": OAUTH_REDIRECT_URL,
380400
}
381401
)
382-
OAUTH_REMOTE_REST_APP["params"].update(
402+
CERN_REMOTE_REST_APP["params"].update(
383403
dict(
384404
request_token_params={"scope": "openid"},
385405
base_url=REANA_SSO_CERN_BASE_URL,
@@ -394,8 +414,34 @@ def _get_rate_limit(env_variable: str, default: str) -> str:
394414
consumer_secret=REANA_SSO_CERN_CONSUMER_SECRET,
395415
)
396416

397-
OAUTHCLIENT_REMOTE_APPS["cern_openid"] = OAUTH_REMOTE_REST_APP
398-
OAUTHCLIENT_REST_REMOTE_APPS["cern_openid"] = OAUTH_REMOTE_REST_APP
417+
OAUTHCLIENT_REMOTE_APPS["cern_openid"] = CERN_REMOTE_REST_APP
418+
OAUTHCLIENT_REST_REMOTE_APPS["cern_openid"] = CERN_REMOTE_REST_APP
419+
420+
# EOSC SSO configuration
421+
EOSC_REMOTE_REST_APP = copy.deepcopy(eosc_aai.REMOTE_REST_APP)
422+
EOSC_REMOTE_REST_APP.update(
423+
{
424+
"authorized_redirect_url": OAUTH_REDIRECT_URL,
425+
"error_redirect_url": OAUTH_REDIRECT_URL,
426+
}
427+
)
428+
EOSC_REMOTE_REST_APP["params"].update(
429+
dict(
430+
request_token_params={"scope": "openid profile email entitlements"},
431+
base_url=REANA_SSO_EOSC_BASE_URL,
432+
access_token_url=REANA_SSO_EOSC_TOKEN_URL,
433+
authorize_url=REANA_SSO_EOSC_AUTH_URL,
434+
)
435+
)
436+
OAUTHCLIENT_EOSC_AAI_USERINFO_URL = REANA_SSO_EOSC_USERINFO_URL
437+
438+
EOSC_AAI_APP_CREDENTIALS = dict(
439+
consumer_key=REANA_SSO_EOSC_CONSUMER_KEY,
440+
consumer_secret=REANA_SSO_EOSC_CONSUMER_SECRET,
441+
)
442+
443+
OAUTHCLIENT_REMOTE_APPS["eosc_aai"] = EOSC_REMOTE_REST_APP
444+
OAUTHCLIENT_REST_REMOTE_APPS["eosc_aai"] = EOSC_REMOTE_REST_APP
399445

400446
SECURITY_PASSWORD_SALT = "security-password-salt"
401447

reana_server/utils.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- coding: utf-8 -*-
22
#
33
# This file is part of REANA.
4-
# Copyright (C) 2018, 2019, 2020, 2021, 2022, 2023, 2024 CERN.
4+
# Copyright (C) 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026 CERN.
55
#
66
# REANA is free software; you can redistribute it and/or modify it
77
# under the terms of the MIT License; see LICENSE file for more details.
@@ -24,6 +24,7 @@
2424
import requests
2525
import yaml
2626
from flask import url_for
27+
from invenio_oauthclient.errors import OAuthClientUnAuthorized
2728
from jinja2 import Environment, PackageLoader, select_autoescape
2829
from marshmallow.exceptions import ValidationError
2930
from marshmallow.validate import Email
@@ -67,6 +68,7 @@
6768
from reana_server.config import (
6869
ADMIN_USER_ID,
6970
REANA_HOSTNAME,
71+
REANA_SSO_EOSC_REQUIRED_ENTITLEMENT,
7072
REANA_USER_EMAIL_CONFIRMATION,
7173
REANA_WORKFLOW_SCHEDULING_POLICY,
7274
REANA_WORKFLOW_SCHEDULING_POLICIES,
@@ -368,10 +370,23 @@ def _import_users(users_csv_file):
368370

369371

370372
def _create_and_associate_oauth_user(sender, account_info, **kwargs):
371-
logging.info(f"account_info: {account_info}")
372373
user_email = account_info["user"]["email"]
373374
user_fullname = account_info["user"]["profile"]["full_name"]
374-
username = account_info["user"]["profile"]["username"]
375+
if "username" in account_info["user"]["profile"]:
376+
username = account_info["user"]["profile"]["username"]
377+
else:
378+
username = user_email # external_id
379+
if "entitlements" in kwargs.get("response", {}):
380+
entitlements = kwargs["response"]["entitlements"]
381+
if REANA_SSO_EOSC_REQUIRED_ENTITLEMENT:
382+
if REANA_SSO_EOSC_REQUIRED_ENTITLEMENT not in entitlements:
383+
logging.warning(
384+
f"User {user_email} does not have the required EOSC entitlement "
385+
f"'{REANA_SSO_EOSC_REQUIRED_ENTITLEMENT}'. Login denied."
386+
)
387+
raise OAuthClientUnAuthorized(
388+
"Access denied. You do not have the required entitlement to use this REANA instance."
389+
)
375390
return _create_and_associate_reana_user(user_email, user_fullname, username)
376391

377392

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
# From auth bundle
7777
"invenio-accounts>=2.0.0",
7878
"invenio-oauth2server>=1.3.0",
79-
"invenio-oauthclient>=1.5.0",
79+
"invenio-oauthclient @ git+https://github.com/tiborsimko/invenio-oauthclient.git@reana-een-aai",
8080
"invenio-userprofiles>=1.2.0",
8181
"invenio-userprofiles>=1.2.0",
8282
"invenio-theme>=1.4.7",

0 commit comments

Comments
 (0)