Skip to content
This repository has been archived by the owner on Dec 18, 2024. It is now read-only.

Commit

Permalink
Merge pull request #392 from communitiesuk/bau/remove-connexion
Browse files Browse the repository at this point in the history
Remove connexion
  • Loading branch information
samuelhwilliams authored Dec 13, 2024
2 parents 3adb765 + a7ad073 commit b380cf3
Show file tree
Hide file tree
Showing 18 changed files with 201 additions and 507 deletions.
4 changes: 2 additions & 2 deletions api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from api.magic_links.routes import MagicLinksView # noqa
from api.session.auth_session import AuthSessionView # noqa
from api.sso.routes import SsoView # noqa
from api.session.auth_session import AuthSessionUserView, AuthSessionSignOutView # noqa
from api.sso.routes import SsoLoginView, SsoLogoutView, SsoGraphCallView, SsoGetTokenView # noqa
13 changes: 9 additions & 4 deletions api/magic_links/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@
from flask.views import MethodView
from fsd_utils.authentication.decorators import login_requested

from api.session.auth_session import AuthSessionView
from api.session.auth_session import AuthSessionBase
from common.blueprints import Blueprint
from config import Config
from models.account import AccountMethods
from models.magic_link import MagicLinkMethods

api_magic_link_bp = Blueprint("api_magic_links", __name__)


class MagicLinksView(MagicLinkMethods, MethodView):
@login_requested
def use(self, link_id: str):
def get(self, link_id: str):
"""
GET /magic-links/{link_id} endpoint
If the link_id matches a valid link key, this then:
Expand All @@ -28,7 +31,6 @@ def use(self, link_id: str):
:param link_id: String short key for the link
:return: 302 Redirect / 404 Error
"""

fund_short_name = request.args.get("fund")
round_short_name = request.args.get("round")

Expand Down Expand Up @@ -62,7 +64,7 @@ def use(self, link_id: str):

# Check link is not expired
if link.get("exp") > int(datetime.now().timestamp()):
return AuthSessionView.create_session_and_redirect(
return AuthSessionBase.create_session_and_redirect(
account=account,
is_via_magic_link=True,
redirect_url=link.get("redirectUrl"),
Expand Down Expand Up @@ -105,3 +107,6 @@ def use(self, link_id: str):
round=round_short_name,
)
)


api_magic_link_bp.add_url_rule("/magic-links/<link_id>", view_func=MagicLinksView.as_view("use"))
86 changes: 49 additions & 37 deletions api/session/auth_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
from typing import TYPE_CHECKING

import jwt
from flask import abort, current_app, make_response, redirect, request, session, url_for
from flask import current_app, make_response, redirect, request, session, url_for
from flask.views import MethodView
from fsd_utils import clear_sentry

from api.responses import error_response
from api.session.exceptions import SessionCreateError
from common.blueprints import Blueprint
from config import Config
from models.magic_link import MagicLinkMethods
from security.utils import create_token, decode_with_options, validate_token
Expand All @@ -16,28 +17,11 @@
from models.account import Account as Account


class AuthSessionView(MethodView):
"""
Views for session related operations
"""
api_sessions_bp = Blueprint("api_sessions", __name__)

@staticmethod
def user():
"""
GET /sessions/user endpoint
Shows the user details of the current user session
or an error if no authenticated user session found
:return: 200 user details json or 404 error
"""
token = request.cookies.get(Config.FSD_USER_TOKEN_COOKIE_NAME)
if token:
try:
valid_token = validate_token(token)
return make_response(valid_token), 200
except jwt.PyJWTError:
error_response(404, "Session token expired or invalid")
error_response(404, "No session token found")

class AuthSessionBase:
@staticmethod
def clear_session(return_app=None, return_path=None):
"""
Clears the user session (signing them out)
Expand Down Expand Up @@ -90,7 +74,9 @@ def clear_session(return_app=None, return_path=None):
)
else:
current_app.logger.warning("{return_app} not listed as a safe app.", extra=dict(return_app=return_app))
abort(400, "Unknown return app.")
resp = make_response({"detail": "Unknown return app."}, 400)
resp.headers["Content-Type"] = "application/json"
return resp

# Clear the cookie and redirect to signed out page
signed_out_url = url_for(
Expand All @@ -110,21 +96,6 @@ def clear_session(return_app=None, return_path=None):
)
return response

# Deprecation warning (Use clear_session_post instead)
@staticmethod
def clear_session_get():
"""GET /sessions/sign-out endpoint"""
return_app = request.args.get("return_app")
return_path = request.args.get("return_path")
return AuthSessionView.clear_session(return_app, return_path)

@staticmethod
def clear_session_post():
"""POST /sessions/sign-out endpoint"""
return_app = request.form.get("return_app")
return_path = request.form.get("return_path")
return AuthSessionView.clear_session(return_app, return_path)

@classmethod
def create_session_and_redirect(
cls,
Expand Down Expand Up @@ -233,3 +204,44 @@ def create_session_details_with_fund_and_round(
session_details.update({"token": create_token(session_details)})
session.update(session_details)
return session_details


class AuthSessionSignOutView(AuthSessionBase, MethodView):
"""
Views for session related operations
"""

# Deprecation warning (Use clear_session_post instead)
def get(self):
"""GET /sessions/sign-out endpoint"""
return_app = request.args.get("return_app")
return_path = request.args.get("return_path")
return self.clear_session(return_app, return_path)

def post(self):
"""POST /sessions/sign-out endpoint"""
return_app = request.form.get("return_app")
return_path = request.form.get("return_path")
return self.clear_session(return_app, return_path)


class AuthSessionUserView(AuthSessionBase, MethodView):
def get(self):
"""
GET /sessions/user endpoint
Shows the user details of the current user session
or an error if no authenticated user session found
:return: 200 user details json or 404 error
"""
token = request.cookies.get(Config.FSD_USER_TOKEN_COOKIE_NAME)
if token:
try:
valid_token = validate_token(token)
return make_response(valid_token), 200
except jwt.PyJWTError:
error_response(404, "Session token expired or invalid")
error_response(404, "No session token found")


api_sessions_bp.add_url_rule("/sessions/user", view_func=AuthSessionUserView.as_view("user"))
api_sessions_bp.add_url_rule("/sessions/sign-out", view_func=AuthSessionSignOutView.as_view("sign_out"))
145 changes: 82 additions & 63 deletions api/sso/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,56 @@
from flask.views import MethodView
from fsd_utils import clear_sentry

from api.session.auth_session import AuthSessionView
from api.session.auth_session import AuthSessionBase
from common.blueprints import Blueprint
from config import Config
from models.account import AccountMethods

api_sso_bp = Blueprint("api_sso", __name__, url_prefix="/sso")

class SsoView(MethodView):
def login(self):
"""
GET /sso/login endpoint
Redirects to the Azure AD auth uri
:return: 302 redirect to Microsoft Login
"""
session["flow"] = self.build_auth_code_flow(scopes=Config.MS_GRAPH_PERMISSIONS_SCOPE)

if return_app := request.args.get("return_app"):
session["return_app"] = return_app
session["return_path"] = request.args.get("return_path")
current_app.logger.debug(
"Setting return app to {return_app} for this session", extra=dict(return_app=return_app)
)
class SsoBase:
@staticmethod
def _load_cache():
cache = msal.SerializableTokenCache()
if session.get("token_cache"):
cache.deserialize(session["token_cache"])
return cache

return redirect(session["flow"]["auth_uri"]), 302
@staticmethod
def _save_cache(cache):
if cache.has_state_changed:
session["token_cache"] = cache.serialize()

def logout(post_logout_redirect_uri=None):
@staticmethod
def _build_msal_app(cache=None, authority=None):
return msal.ConfidentialClientApplication(
Config.AZURE_AD_CLIENT_ID,
authority=authority or Config.AZURE_AD_AUTHORITY,
client_credential=Config.AZURE_AD_CLIENT_SECRET,
token_cache=cache,
)

@staticmethod
def _get_origin_from_url(url):
parsed_uri = urlparse(url)
return f"{parsed_uri.scheme}://{parsed_uri.netloc}"

def build_auth_code_flow(self, authority=None, scopes=None):
return self._build_msal_app(authority=authority).initiate_auth_code_flow(
scopes or [], redirect_uri=Config.AZURE_AD_REDIRECT_URI
)

def _get_token_from_cache(self, scope=None):
cache = self._load_cache() # This web app maintains one cache per session
cca = self._build_msal_app(cache=cache)
accounts = cca.get_accounts()
if accounts: # So all account(s) belong to the current signed-in user
result = cca.acquire_token_silent(scope, account=accounts[0])
self._save_cache(cache)
return result

def logout(self, post_logout_redirect_uri=None):
"""
Clears the user session then redirects to
Azure AD logout endpoint to logout from our tenants web session too
Expand Down Expand Up @@ -57,28 +83,51 @@ def logout(post_logout_redirect_uri=None):
clear_sentry()
return response


class SsoLoginView(SsoBase, MethodView):
def get(self):
"""
GET /sso/login endpoint
Redirects to the Azure AD auth uri
:return: 302 redirect to Microsoft Login
"""
session["flow"] = self.build_auth_code_flow(scopes=Config.MS_GRAPH_PERMISSIONS_SCOPE)

if return_app := request.args.get("return_app"):
session["return_app"] = return_app
session["return_path"] = request.args.get("return_path")
current_app.logger.debug(
"Setting return app to {return_app} for this session", extra=dict(return_app=return_app)
)

return redirect(session["flow"]["auth_uri"]), 302


class SsoLogoutView(SsoBase, MethodView):
# Deprecation warning (Use logout_post instead)
def logout_get(self):
def get(self):
"""GET /sso/logout endpoint"""
post_logout_redirect_uri = request.args.get(
"post_logout_redirect_uri",
Config.SSO_POST_SIGN_OUT_URL + f"?{urlencode({'return_app': session['return_app']})}"
if "return_app" in session
else "",
)
return SsoView.logout(post_logout_redirect_uri)
return self.logout(post_logout_redirect_uri)

def logout_post(self):
def post(self):
"""POST /sso/logout endpoint"""
post_logout_redirect_uri = request.form.get(
"post_logout_redirect_uri",
Config.SSO_POST_SIGN_OUT_URL + f"?{urlencode({'return_app': session['return_app']})}"
if "return_app" in session
else "",
)
return SsoView.logout(post_logout_redirect_uri)
return self.logout(post_logout_redirect_uri)

def get_token(self):

class SsoGetTokenView(SsoBase, MethodView):
def get(self):
"""
GET /sso/get-token
The endpoint that Azure AD redirects back to
Expand Down Expand Up @@ -127,17 +176,21 @@ def get_token(self):
)
else:
current_app.logger.warning("{return_app} not listed as a safe app.", extra=dict(return_app=return_app))
abort(400, "Unknown return app.")
resp = make_response({"detail": "Unknown return app."}, 400)
resp.headers["Content-Type"] = "application/json"
return resp

# Create session token, set cookie and redirect
return AuthSessionView.create_session_and_redirect(
return AuthSessionBase.create_session_and_redirect(
account=updated_account,
redirect_url=redirect_url,
is_via_magic_link=False,
timeout_seconds=Config.FSD_ASSESSMENT_SESSION_TIMEOUT_SECONDS,
)

def graph_call(self):

class SsoGraphCallView(SsoBase, MethodView):
def get(self):
"""
GET /sso/graph-call endpoint
Shows the graph object for the current authenticated user
Expand All @@ -153,42 +206,8 @@ def graph_call(self):
).json()
return graph_data, 200

@staticmethod
def _load_cache():
cache = msal.SerializableTokenCache()
if session.get("token_cache"):
cache.deserialize(session["token_cache"])
return cache

@staticmethod
def _save_cache(cache):
if cache.has_state_changed:
session["token_cache"] = cache.serialize()

@staticmethod
def _build_msal_app(cache=None, authority=None):
return msal.ConfidentialClientApplication(
Config.AZURE_AD_CLIENT_ID,
authority=authority or Config.AZURE_AD_AUTHORITY,
client_credential=Config.AZURE_AD_CLIENT_SECRET,
token_cache=cache,
)

@staticmethod
def _get_origin_from_url(url):
parsed_uri = urlparse(url)
return f"{parsed_uri.scheme}://{parsed_uri.netloc}"

def build_auth_code_flow(self, authority=None, scopes=None):
return self._build_msal_app(authority=authority).initiate_auth_code_flow(
scopes or [], redirect_uri=Config.AZURE_AD_REDIRECT_URI
)

def _get_token_from_cache(self, scope=None):
cache = self._load_cache() # This web app maintains one cache per session
cca = self._build_msal_app(cache=cache)
accounts = cca.get_accounts()
if accounts: # So all account(s) belong to the current signed-in user
result = cca.acquire_token_silent(scope, account=accounts[0])
self._save_cache(cache)
return result
api_sso_bp.add_url_rule("/login", view_func=SsoLoginView.as_view("login"))
api_sso_bp.add_url_rule("/logout", view_func=SsoLogoutView.as_view("logout"))
api_sso_bp.add_url_rule("/get-token", view_func=SsoGetTokenView.as_view("get_token"))
api_sso_bp.add_url_rule("/graph-call", view_func=SsoGraphCallView.as_view("graph_call"))
Loading

0 comments on commit b380cf3

Please sign in to comment.