diff --git a/ee/api/Pipfile b/ee/api/Pipfile index 1279cb550..dd3f2ac95 100644 --- a/ee/api/Pipfile +++ b/ee/api/Pipfile @@ -15,16 +15,16 @@ fastapi = "==0.104.1" gunicorn = "==21.2.0" python-decouple = "==3.8" apscheduler = "==3.10.4" +python3-saml = "==1.16.0" python-multipart = "==0.0.6" redis = "==5.0.1" -python3-saml = "==1.16.0" azure-storage-blob = "==12.19.0" +psycopg = {extras = ["binary", "pool"], version = "==3.1.14"} uvicorn = {extras = ["standard"], version = "==0.23.2"} pydantic = {extras = ["email"], version = "==2.3.0"} clickhouse-driver = {extras = ["lz4"], version = "==0.2.6"} -psycopg = {extras = ["binary", "pool"], version = "==3.1.12"} [dev-packages] [requires] -python_version = "3.11" +python_version = "3.12" diff --git a/ee/api/chalicelib/utils/SAML2_helper.py b/ee/api/chalicelib/utils/SAML2_helper.py index 6ef8ae942..2878f6df1 100644 --- a/ee/api/chalicelib/utils/SAML2_helper.py +++ b/ee/api/chalicelib/utils/SAML2_helper.py @@ -10,17 +10,18 @@ from starlette.datastructures import FormData if config("ENABLE_SSO", cast=bool, default=True): from onelogin.saml2.auth import OneLogin_Saml2_Auth +API_PREFIX = "/api" SAML2 = { "strict": config("saml_strict", cast=bool, default=True), "debug": config("saml_debug", cast=bool, default=True), "sp": { - "entityId": config("SITE_URL") + "/api/sso/saml2/metadata/", + "entityId": config("SITE_URL") + API_PREFIX + "/sso/saml2/metadata/", "assertionConsumerService": { - "url": config("SITE_URL") + "/api/sso/saml2/acs/", + "url": config("SITE_URL") + API_PREFIX + "/sso/saml2/acs/", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" }, "singleLogoutService": { - "url": config("SITE_URL") + "/api/sso/saml2/sls/", + "url": config("SITE_URL") + API_PREFIX + "/sso/saml2/sls/", "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", @@ -110,8 +111,8 @@ async def prepare_request(request: Request): # add / to /acs if not path.endswith("/"): path = path + '/' - if not path.startswith("/api"): - path = "/api" + path + if len(API_PREFIX) > 0 and not path.startswith(API_PREFIX): + path = API_PREFIX + path return { 'https': 'on' if proto == 'https' else 'off', @@ -136,7 +137,13 @@ def get_saml2_provider(): config("idp_name", default="saml2")) > 0 else None -def get_landing_URL(jwt): +def get_landing_URL(jwt, redirect_to_link2=False): + if redirect_to_link2: + if len(config("sso_landing_override", default="")) == 0: + logging.warning("SSO trying to redirect to custom URL, but sso_landing_override env var is empty") + else: + return config("sso_landing_override") + "?jwt=%s" % jwt + return config("SITE_URL") + config("sso_landing", default="/login?jwt=%s") % jwt diff --git a/ee/api/development.md b/ee/api/development.md index d980a24d7..55d869c3f 100644 --- a/ee/api/development.md +++ b/ee/api/development.md @@ -16,12 +16,15 @@ mkdir .venv # Installing dependencies (pipenv will detect the .venv folder and use it as a target) pipenv install -r requirements.txt [--skip-lock] - # These commands must bu used everytime you make changes to FOSS. # To clean the unused files before getting new ones bash clean.sh # To copy commun files from FOSS bash prepare-dev.sh + +# In case of an issue with python3-saml installation for MacOS, +# please follow these instructions: +https://github.com/xmlsec/python-xmlsec/issues/254#issuecomment-1726249435 ``` ### Building and deploying locally diff --git a/ee/api/routers/saml.py b/ee/api/routers/saml.py index b7a5cc97c..ae7d3171e 100644 --- a/ee/api/routers/saml.py +++ b/ee/api/routers/saml.py @@ -1,9 +1,11 @@ +import json +import logging + from fastapi import HTTPException, Request, Response, status from chalicelib.utils import SAML2_helper from chalicelib.utils.SAML2_helper import prepare_request, init_saml_auth from routers.base import get_routers -import logging logger = logging.getLogger(__name__) @@ -18,11 +20,11 @@ from starlette.responses import RedirectResponse @public_app.get("/sso/saml2", tags=["saml2"]) @public_app.get("/sso/saml2/", tags=["saml2"]) -async def start_sso(request: Request): +async def start_sso(request: Request, iFrame: bool = False): request.path = '' req = await prepare_request(request=request) auth = init_saml_auth(req) - sso_built_url = auth.login() + sso_built_url = auth.login(return_to=json.dumps({'iFrame': iFrame})) return RedirectResponse(url=sso_built_url) @@ -33,6 +35,8 @@ async def process_sso_assertion(request: Request): session = req["cookie"]["session"] auth = init_saml_auth(req) + redirect_to_link2 = json.loads(req.get("post_data", {}) \ + .get('RelayState', '{}')).get("iFrame") request_id = None if 'AuthNRequestID' in session: request_id = session['AuthNRequestID'] @@ -111,7 +115,7 @@ async def process_sso_assertion(request: Request): refresh_token_max_age = jwt["refreshTokenMaxAge"] response = Response( status_code=status.HTTP_302_FOUND, - headers={'Location': SAML2_helper.get_landing_URL(jwt["jwt"])}) + headers={'Location': SAML2_helper.get_landing_URL(jwt["jwt"], redirect_to_link2=redirect_to_link2)}) response.set_cookie(key="refreshToken", value=refresh_token, path="/api/refresh", max_age=refresh_token_max_age, secure=True, httponly=True) return response @@ -124,6 +128,8 @@ async def process_sso_assertion_tk(tenantKey: str, request: Request): session = req["cookie"]["session"] auth = init_saml_auth(req) + redirect_to_link2 = json.loads(req.get("post_data", {}) \ + .get('RelayState', '{}')).get("iFrame") request_id = None if 'AuthNRequestID' in session: request_id = session['AuthNRequestID'] @@ -194,9 +200,14 @@ async def process_sso_assertion_tk(tenantKey: str, request: Request): jwt = users.authenticate_sso(email=email, internal_id=internal_id, exp=expiration) if jwt is None: return {"errors": ["null JWT"]} - return Response( + refresh_token = jwt["refreshToken"] + refresh_token_max_age = jwt["refreshTokenMaxAge"] + response = Response( status_code=status.HTTP_302_FOUND, - headers={'Location': SAML2_helper.get_landing_URL(jwt)}) + headers={'Location': SAML2_helper.get_landing_URL(jwt["jwt"], redirect_to_link2=redirect_to_link2)}) + response.set_cookie(key="refreshToken", value=refresh_token, path="/api/refresh", + max_age=refresh_token_max_age, secure=True, httponly=True) + return response @public_app.get('/sso/saml2/sls', tags=["saml2"])