Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added single log out for auth_saml #27

Closed
wants to merge 13 commits into from

Conversation

TracyWR
Copy link
Collaborator

@TracyWR TracyWR commented Jul 1, 2021

Added single log out for auth_saml

Learning

Describe the research stage

Links to blog posts, patterns, libraries or addons used to solve this problem

Copy link
Owner

@busykoala busykoala left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is very much work in progress. I could look into adding sessions in a easy way (most likely starlettes session middleware will do the job) to provide the userdata throughout the app. Were you able to look at testing the already newly introduced endpoints and the other things I mentioned?

fastapi_opa/auth/auth_saml.py Outdated Show resolved Hide resolved
fastapi_opa/auth/auth_saml.py Outdated Show resolved Hide resolved
fastapi_opa/auth/auth_saml.py Outdated Show resolved Hide resolved
fastapi_opa/auth/auth_saml.py Outdated Show resolved Hide resolved
fastapi_opa/auth/auth_saml.py Outdated Show resolved Hide resolved
tests/test_oidc_auth.py Outdated Show resolved Hide resolved
# sso_built_url = auth.login()
# request.session['AuthNRequestID'] = auth.get_last_request_id()
# return redirect(sso_built_url)
elif 'sso2' in request.query_params:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be tested.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a comment in the test. I don't quite agree that the test brings any value since it just repeats what's done in sso endpoint. In order to show the difference in a test you would also need to test the added attrs on calling with the sso2 param, while you just send a different url into the single sign on method (and there you can send anything, so not really an additional case in my eyes).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The add attrs are outside of the sso function, will think about how to test this.

fastapi_opa/auth/auth_saml.py Show resolved Hide resolved
@busykoala busykoala added enhancement New feature or request feature labels Jul 3, 2021
@busykoala busykoala linked an issue Jul 3, 2021 that may be closed by this pull request
@TracyWR
Copy link
Collaborator Author

TracyWR commented Jul 3, 2021 via email

fastapi_opa/auth/auth_saml.py Outdated Show resolved Hide resolved
tests/test_oidc_auth.py Outdated Show resolved Hide resolved
Copy link
Owner

@busykoala busykoala left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked at how to make use of sessions. I checked out different approaches but ended up with the starlette implementation.

from starlette.middleware.sessions import SessionMiddleware
...

app = FastAPI()
app.add_middleware(OPAMiddleware, config=opa_config)
app.add_middleware(SessionMiddleware, secret_key="very-secret", max_age=3600)

Within the app the session keys can easily be gotten/set by just changing the value for a key:

request.session["user_session"] = "{'some': 'dict', 'as': 'string'}"
request.session.get("user_session")

I think this will be a good solution since it does not require additional dependencies, is very easy to handle in the opa middleware and doesn't require a interface change.

@TracyWR TracyWR force-pushed the tw_saml_opa branch 2 times, most recently from 876c481 to 8004b91 Compare July 4, 2021 10:00
@TracyWR TracyWR marked this pull request as draft July 5, 2021 12:34
fastapi_opa/auth/auth_saml.py Outdated Show resolved Hide resolved
fastapi_opa/auth/auth_saml.py Outdated Show resolved Hide resolved
fastapi_opa/auth/auth_saml.py Show resolved Hide resolved
fastapi_opa/auth/auth_saml.py Outdated Show resolved Hide resolved
tests/test_saml_auth.py Show resolved Hide resolved
fastapi_opa/auth/auth_saml.py Outdated Show resolved Hide resolved
fastapi_opa/auth/auth_saml.py Outdated Show resolved Hide resolved
tests/test_saml_auth.py Show resolved Hide resolved
tests/test_saml_auth.py Outdated Show resolved Hide resolved
tests/test_saml_auth.py Outdated Show resolved Hide resolved
fastapi_opa/auth/auth_saml.py Outdated Show resolved Hide resolved
# sso_built_url = auth.login()
# request.session['AuthNRequestID'] = auth.get_last_request_id()
# return redirect(sso_built_url)
elif 'sso2' in request.query_params:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a comment in the test. I don't quite agree that the test brings any value since it just repeats what's done in sso endpoint. In order to show the difference in a test you would also need to test the added attrs on calling with the sso2 param, while you just send a different url into the single sign on method (and there you can send anything, so not really an additional case in my eyes).

@TracyWR TracyWR marked this pull request as ready for review July 8, 2021 12:20
@TracyWR
Copy link
Collaborator Author

TracyWR commented Jul 8, 2021

every single log out will redirect to using sls, so need sls method to finish single log out.
image

current sls method does not work, error is:

OneLogin_Saml2_Error: SAML LogoutRequest/LogoutResponse not found. Only supported HTTP_REDIRECT Binding

Traceback
\venv\lib\site-packages\onelogin\saml2\auth.py, line 203, in process_slo ‒

  1.             return self.redirect_to(self.get_slo_response_url(), parameters)
    
  2.     else:
    
  3.         self.__errors.append('invalid_binding')
    
  4.         raise OneLogin_Saml2_Error(
    
  5.             'SAML LogoutRequest/LogoutResponse not found. Only supported HTTP_REDIRECT Binding',
    
  6.             OneLogin_Saml2_Error.SAML_LOGOUTMESSAGE_NOT_FOUND
    
  7.         )
    

@TracyWR TracyWR force-pushed the tw_saml_opa branch 4 times, most recently from a2d2215 to dab4ef7 Compare July 10, 2021 13:21
Copy link
Owner

@busykoala busykoala left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not yet setup the complete environment, but I created a little POC to show how the session indeed should be working just fine.

Let's define an in-package app like this:

# fastapi_opa/main.py
from typing import Dict

from fastapi import FastAPI
from starlette.middleware.sessions import SessionMiddleware
from starlette.requests import Request

from fastapi_opa import OPAConfig
from fastapi_opa import OPAMiddleware
from fastapi_opa.auth.auth_saml import SAMLAuthentication
from fastapi_opa.auth.auth_saml import SAMLConfig



opa_host = "http://localhost:8181"
# In this example we use OIDC authentication flow (using Keycloak)
saml_config = SAMLConfig(settings_directory="./tests/test_data/saml")
saml_auth = SAMLAuthentication(saml_config)

opa_config = OPAConfig(authentication=saml_auth, opa_host=opa_host)

app = FastAPI()
app.add_middleware(OPAMiddleware, config=opa_config)
app.add_middleware(SessionMiddleware, secret_key="secret", max_age=24*60*60)


@app.get("/")
async def root(request: Request) -> Dict:
    return {
        "msg": request.session.get("foo"),
    }

And now change the opa middleware into a simple example of using the session:

# fastapi_opa.opa.opa_middleware.py
import asyncio
import json
import logging
from json.decoder import JSONDecodeError

import requests
from fastapi.responses import JSONResponse
from starlette.requests import Request
from starlette.responses import RedirectResponse
from starlette.types import ASGIApp
from starlette.types import Receive
from starlette.types import Scope
from starlette.types import Send

from fastapi_opa.auth.exceptions import AuthenticationException
from fastapi_opa.opa.opa_config import OPAConfig

logger = logging.getLogger(__name__)


class OPAMiddleware:
    def __init__(self, app: ASGIApp, config: OPAConfig) -> None:
        self.config = config
        self.app = app

    async def __call__(
        self, scope: Scope, receive: Receive, send: Send
    ) -> None:
        request = Request(scope, receive, send)

        if "bar" in request.query_params:
            return await self.app(scope, receive, send)

        request.session["foo"] = "some data"
        return await RedirectResponse("?bar").__call__(
            scope, receive, send
        )
# end of file (for the POC)

Now additionally add uvicorn (just to run the example). If you start your app running poetry run uvicorn fastapi_opa.main:app and visit / a session is set with the key foo and the data some-data. Then the client is redirected to ?bar. On hitting ?bar the client will actually see the root route of fastapi, which checks what's in request.session.get("foo") and displays it in it's json msg. This also works within the middleware (just try printing it or so...).

The same works within the saml auth injectable. Feel free to add the session middleware for the test client since this will soon become a requirement for OIDC as well.

errors = saml_settings.validate_metadata(metadata)
status_code = 200
if len(errors) != 0:
metadata = ', '.join(errors)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about the flow specs here, or how you are planning to use this method (right now it is not used at all). But exposing the errors is a security issue usually.

assert response.status_code == 307


async def async_return(result):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this method used?


elif 'sls' in request.query_params:
logger.debug('--sls--')
return await self.single_log_out_from_IdP(request)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rename according to pep8 (self.single_log_out_from_idp).

@@ -39,18 +62,47 @@ async def init_saml_auth(self, request_args: Dict) -> OneLogin_Saml2_Auth:
)

@staticmethod
async def single_sign_on(auth: OneLogin_Saml2_Auth) -> RedirectResponse:
redirect_url = auth.login()
async def single_log_out_from_IdP(request: Request) -> \
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If not explicitly required by black, please do now use \ here but wrap with parenthesis.

}

async def get_metadata(self, request: Request):
saml_settings = OneLogin_Saml2_Settings(custom_base_path=self.custom_folder,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the type checker I think this should be self.custom_folder.as_posix() instead.

@busykoala
Copy link
Owner

every single log out will redirect to using sls, so need sls method to finish single log out.
image

current sls method does not work, error is:

OneLogin_Saml2_Error: SAML LogoutRequest/LogoutResponse not found. Only supported HTTP_REDIRECT Binding

Traceback
\venv\lib\site-packages\onelogin\saml2\auth.py, line 203, in process_slo ‒

1. ```
               return self.redirect_to(self.get_slo_response_url(), parameters)
   ```

2. ```
       else:
   ```

3. ```
           self.__errors.append('invalid_binding')
   ```

4. ```
           raise OneLogin_Saml2_Error(
   ```

5. ```
               'SAML LogoutRequest/LogoutResponse not found. Only supported HTTP_REDIRECT Binding',
   ```

6. ```
               OneLogin_Saml2_Error.SAML_LOGOUTMESSAGE_NOT_FOUND
   ```

7. ```
           )
   ```

This might be related (SAML-Toolkits/python-saml#146) but I was not yet able to dig into the issue much.

@TracyWR
Copy link
Collaborator Author

TracyWR commented Jul 14, 2021

every single log out will redirect to using sls, so need sls method to finish single log out.
image

current sls method does not work, error is:

OneLogin_Saml2_Error: SAML LogoutRequest/LogoutResponse not found. Only supported HTTP_REDIRECT Binding

Traceback
\venv\lib\site-packages\onelogin\saml2\auth.py, line 203, in process_slo ‒

1. ```
               return self.redirect_to(self.get_slo_response_url(), parameters)
   ```

2. ```
       else:
   ```

3. ```
           self.__errors.append('invalid_binding')
   ```

4. ```
           raise OneLogin_Saml2_Error(
   ```

5. ```
               'SAML LogoutRequest/LogoutResponse not found. Only supported HTTP_REDIRECT Binding',
   ```

6. ```
               OneLogin_Saml2_Error.SAML_LOGOUTMESSAGE_NOT_FOUND
   ```

7. ```
           )
   ```

I have changed sls method, it works now

@TracyWR TracyWR closed this Jul 14, 2021
@TracyWR
Copy link
Collaborator Author

TracyWR commented Jul 14, 2021

every single log out will redirect to using sls, so need sls method to finish single log out.
image
current sls method does not work, error is:
OneLogin_Saml2_Error: SAML LogoutRequest/LogoutResponse not found. Only supported HTTP_REDIRECT Binding
Traceback
\venv\lib\site-packages\onelogin\saml2\auth.py, line 203, in process_slo ‒

1. ```
               return self.redirect_to(self.get_slo_response_url(), parameters)
  1.     else:
    
  2.         self.__errors.append('invalid_binding')
    
  3.         raise OneLogin_Saml2_Error(
    
  4.             'SAML LogoutRequest/LogoutResponse not found. Only supported HTTP_REDIRECT Binding',
    
  5.             OneLogin_Saml2_Error.SAML_LOGOUTMESSAGE_NOT_FOUND
    
  6.         )
    

This might be related (onelogin/python-saml#146) but I was not yet able to dig into the issue much.

I have changed sls method, now it works

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants