feature(chalice): support multi SSO redirect
This commit is contained in:
parent
4e190cc00d
commit
19470cd41f
4 changed files with 37 additions and 16 deletions
|
|
@ -15,16 +15,16 @@ fastapi = "==0.104.1"
|
||||||
gunicorn = "==21.2.0"
|
gunicorn = "==21.2.0"
|
||||||
python-decouple = "==3.8"
|
python-decouple = "==3.8"
|
||||||
apscheduler = "==3.10.4"
|
apscheduler = "==3.10.4"
|
||||||
|
python3-saml = "==1.16.0"
|
||||||
python-multipart = "==0.0.6"
|
python-multipart = "==0.0.6"
|
||||||
redis = "==5.0.1"
|
redis = "==5.0.1"
|
||||||
python3-saml = "==1.16.0"
|
|
||||||
azure-storage-blob = "==12.19.0"
|
azure-storage-blob = "==12.19.0"
|
||||||
|
psycopg = {extras = ["binary", "pool"], version = "==3.1.14"}
|
||||||
uvicorn = {extras = ["standard"], version = "==0.23.2"}
|
uvicorn = {extras = ["standard"], version = "==0.23.2"}
|
||||||
pydantic = {extras = ["email"], version = "==2.3.0"}
|
pydantic = {extras = ["email"], version = "==2.3.0"}
|
||||||
clickhouse-driver = {extras = ["lz4"], version = "==0.2.6"}
|
clickhouse-driver = {extras = ["lz4"], version = "==0.2.6"}
|
||||||
psycopg = {extras = ["binary", "pool"], version = "==3.1.12"}
|
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
python_version = "3.11"
|
python_version = "3.12"
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,18 @@ from starlette.datastructures import FormData
|
||||||
if config("ENABLE_SSO", cast=bool, default=True):
|
if config("ENABLE_SSO", cast=bool, default=True):
|
||||||
from onelogin.saml2.auth import OneLogin_Saml2_Auth
|
from onelogin.saml2.auth import OneLogin_Saml2_Auth
|
||||||
|
|
||||||
|
API_PREFIX = "/api"
|
||||||
SAML2 = {
|
SAML2 = {
|
||||||
"strict": config("saml_strict", cast=bool, default=True),
|
"strict": config("saml_strict", cast=bool, default=True),
|
||||||
"debug": config("saml_debug", cast=bool, default=True),
|
"debug": config("saml_debug", cast=bool, default=True),
|
||||||
"sp": {
|
"sp": {
|
||||||
"entityId": config("SITE_URL") + "/api/sso/saml2/metadata/",
|
"entityId": config("SITE_URL") + API_PREFIX + "/sso/saml2/metadata/",
|
||||||
"assertionConsumerService": {
|
"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"
|
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
||||||
},
|
},
|
||||||
"singleLogoutService": {
|
"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"
|
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
||||||
},
|
},
|
||||||
"NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
"NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
||||||
|
|
@ -110,8 +111,8 @@ async def prepare_request(request: Request):
|
||||||
# add / to /acs
|
# add / to /acs
|
||||||
if not path.endswith("/"):
|
if not path.endswith("/"):
|
||||||
path = path + '/'
|
path = path + '/'
|
||||||
if not path.startswith("/api"):
|
if len(API_PREFIX) > 0 and not path.startswith(API_PREFIX):
|
||||||
path = "/api" + path
|
path = API_PREFIX + path
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'https': 'on' if proto == 'https' else 'off',
|
'https': 'on' if proto == 'https' else 'off',
|
||||||
|
|
@ -136,7 +137,13 @@ def get_saml2_provider():
|
||||||
config("idp_name", default="saml2")) > 0 else None
|
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
|
return config("SITE_URL") + config("sso_landing", default="/login?jwt=%s") % jwt
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,15 @@ mkdir .venv
|
||||||
|
|
||||||
# Installing dependencies (pipenv will detect the .venv folder and use it as a target)
|
# Installing dependencies (pipenv will detect the .venv folder and use it as a target)
|
||||||
pipenv install -r requirements.txt [--skip-lock]
|
pipenv install -r requirements.txt [--skip-lock]
|
||||||
|
|
||||||
# These commands must bu used everytime you make changes to FOSS.
|
# These commands must bu used everytime you make changes to FOSS.
|
||||||
# To clean the unused files before getting new ones
|
# To clean the unused files before getting new ones
|
||||||
bash clean.sh
|
bash clean.sh
|
||||||
# To copy commun files from FOSS
|
# To copy commun files from FOSS
|
||||||
bash prepare-dev.sh
|
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
|
### Building and deploying locally
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
from fastapi import HTTPException, Request, Response, status
|
from fastapi import HTTPException, Request, Response, status
|
||||||
|
|
||||||
from chalicelib.utils import SAML2_helper
|
from chalicelib.utils import SAML2_helper
|
||||||
from chalicelib.utils.SAML2_helper import prepare_request, init_saml_auth
|
from chalicelib.utils.SAML2_helper import prepare_request, init_saml_auth
|
||||||
from routers.base import get_routers
|
from routers.base import get_routers
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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"])
|
||||||
@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 = ''
|
request.path = ''
|
||||||
req = await prepare_request(request=request)
|
req = await prepare_request(request=request)
|
||||||
auth = init_saml_auth(req)
|
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)
|
return RedirectResponse(url=sso_built_url)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -33,6 +35,8 @@ async def process_sso_assertion(request: Request):
|
||||||
session = req["cookie"]["session"]
|
session = req["cookie"]["session"]
|
||||||
auth = init_saml_auth(req)
|
auth = init_saml_auth(req)
|
||||||
|
|
||||||
|
redirect_to_link2 = json.loads(req.get("post_data", {}) \
|
||||||
|
.get('RelayState', '{}')).get("iFrame")
|
||||||
request_id = None
|
request_id = None
|
||||||
if 'AuthNRequestID' in session:
|
if 'AuthNRequestID' in session:
|
||||||
request_id = session['AuthNRequestID']
|
request_id = session['AuthNRequestID']
|
||||||
|
|
@ -111,7 +115,7 @@ async def process_sso_assertion(request: Request):
|
||||||
refresh_token_max_age = jwt["refreshTokenMaxAge"]
|
refresh_token_max_age = jwt["refreshTokenMaxAge"]
|
||||||
response = Response(
|
response = Response(
|
||||||
status_code=status.HTTP_302_FOUND,
|
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",
|
response.set_cookie(key="refreshToken", value=refresh_token, path="/api/refresh",
|
||||||
max_age=refresh_token_max_age, secure=True, httponly=True)
|
max_age=refresh_token_max_age, secure=True, httponly=True)
|
||||||
return response
|
return response
|
||||||
|
|
@ -124,6 +128,8 @@ async def process_sso_assertion_tk(tenantKey: str, request: Request):
|
||||||
session = req["cookie"]["session"]
|
session = req["cookie"]["session"]
|
||||||
auth = init_saml_auth(req)
|
auth = init_saml_auth(req)
|
||||||
|
|
||||||
|
redirect_to_link2 = json.loads(req.get("post_data", {}) \
|
||||||
|
.get('RelayState', '{}')).get("iFrame")
|
||||||
request_id = None
|
request_id = None
|
||||||
if 'AuthNRequestID' in session:
|
if 'AuthNRequestID' in session:
|
||||||
request_id = session['AuthNRequestID']
|
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)
|
jwt = users.authenticate_sso(email=email, internal_id=internal_id, exp=expiration)
|
||||||
if jwt is None:
|
if jwt is None:
|
||||||
return {"errors": ["null JWT"]}
|
return {"errors": ["null JWT"]}
|
||||||
return Response(
|
refresh_token = jwt["refreshToken"]
|
||||||
|
refresh_token_max_age = jwt["refreshTokenMaxAge"]
|
||||||
|
response = Response(
|
||||||
status_code=status.HTTP_302_FOUND,
|
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"])
|
@public_app.get('/sso/saml2/sls', tags=["saml2"])
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue