From a36fafbab8c8b953d6e5ab6f2e2f104879366d7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Hru=C5=A1ka?= Date: Mon, 10 Jul 2023 16:47:51 +0200 Subject: [PATCH 01/10] Remove faulty reference --- seacatauth/authn/handler.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/seacatauth/authn/handler.py b/seacatauth/authn/handler.py index 858a114c..c15709cf 100644 --- a/seacatauth/authn/handler.py +++ b/seacatauth/authn/handler.py @@ -315,9 +315,6 @@ async def logout(self, request): else: set_cookie(self.App, response, impersonator_session) - if self.BatmanService is not None: - response.del_cookie(self.BatmanService.CookieName) - return response async def smslogin(self, request): From 893afa3e18c1270d8bad4df033c9bae2650e4bdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Hru=C5=A1ka?= Date: Mon, 10 Jul 2023 17:00:46 +0200 Subject: [PATCH 02/10] Add POST endpoint for Batman introspection (to be consistent with cookie and oauth introspection) --- seacatauth/batman/handler.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/seacatauth/batman/handler.py b/seacatauth/batman/handler.py index 7e1aae2d..112c3518 100644 --- a/seacatauth/batman/handler.py +++ b/seacatauth/batman/handler.py @@ -25,9 +25,11 @@ def __init__(self, app, batman_svc): web_app = app.WebContainer.WebApp web_app.router.add_put("/batman/nginx", self.batman_nginx) + web_app.router.add_post("/batman/nginx", self.batman_nginx) # Public endpoints web_app_public.router.add_put("/batman/nginx", self.batman_nginx) + web_app_public.router.add_post("/batman/nginx", self.batman_nginx) async def batman_nginx(self, request): From 3701b682ee41d95b1f48c11a7acf5c76b04f90d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Hru=C5=A1ka?= Date: Mon, 10 Jul 2023 17:27:11 +0200 Subject: [PATCH 03/10] Fix cookie lookup --- seacatauth/batman/handler.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/seacatauth/batman/handler.py b/seacatauth/batman/handler.py index 112c3518..ea762d41 100644 --- a/seacatauth/batman/handler.py +++ b/seacatauth/batman/handler.py @@ -39,11 +39,12 @@ async def batman_nginx(self, request): **Internal endpoint for Nginx auth_request.** """ cookie_service = self.BatmanService.App.get_service("seacatauth.CookieService") - cookie_value = cookie_service.get_session_cookie_value(request, request.query.get("client_id")) - if cookie_value is None: - return aiohttp.web.HTTPUnauthorized() - session = await cookie_service.get_session_by_session_cookie_value(cookie_value) + client_id = request.query.get("client_id") + if client_id is None: + raise ValueError("No 'client_id' parameter specified in Batman introspection query.") + + session = await cookie_service.get_session_by_request_cookie(request, request.query.get("client_id")) if session is None or session.Batman is None: return aiohttp.web.HTTPUnauthorized() From c750d3cd444e049151f1fe57176571e441db5122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Hru=C5=A1ka?= Date: Mon, 10 Jul 2023 17:27:31 +0200 Subject: [PATCH 04/10] Fix log message --- seacatauth/cookie/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seacatauth/cookie/handler.py b/seacatauth/cookie/handler.py index e77ffcc5..b4f18a15 100644 --- a/seacatauth/cookie/handler.py +++ b/seacatauth/cookie/handler.py @@ -147,7 +147,7 @@ async def nginx(self, request): """ client_id = request.query.get("client_id") if client_id is None: - raise ValueError("No 'client_id' parameter specified in anonymous introspection query.") + raise ValueError("No 'client_id' parameter specified in cookie introspection query.") # TODO: Also check query for scope and validate it From b764d01880fa729a6d10525aeaeab58ea87ee8ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Hru=C5=A1ka?= Date: Mon, 10 Jul 2023 18:06:24 +0200 Subject: [PATCH 05/10] Two types of encrypted session fields --- seacatauth/session/adapter.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/seacatauth/session/adapter.py b/seacatauth/session/adapter.py index d710437f..b54a38b2 100644 --- a/seacatauth/session/adapter.py +++ b/seacatauth/session/adapter.py @@ -144,19 +144,25 @@ class Batman: _prefix = "ba" Token = "ba_t" - # Fields that are stored encrypted - SensitiveFields = frozenset([ - FN.OAuth2.IdToken, + # Session identifiers are stored encrypted + # They are used as session lookup keys and need special encryption treatment for that + EncryptedIdentifierFields = frozenset([ FN.OAuth2.AccessToken, FN.OAuth2.RefreshToken, FN.Cookie.Id, + ]) + + # Other sensitive fields (not used as lookup keys) + # They use regular encryption provided by asab.storage + EncryptedAttributes = frozenset([ FN.Batman.Token, + FN.OAuth2.IdToken, ]) EncryptedPrefix = b"$aescbc$" def __init__(self, session_svc, session_dict): - self._decrypt_sensitive_fields(session_dict, session_svc) + self._decrypt_encrypted_identifiers(session_dict, session_svc) self.Session = self._deserialize_session_data(session_dict) self.Id = self.Session.Id @@ -263,9 +269,9 @@ def rest_get(self): session_dict = self.serialize() return rest_get(session_dict) - def _decrypt_sensitive_fields(self, session_dict, session_svc): + def _decrypt_encrypted_identifiers(self, session_dict, session_svc): # Decrypt sensitive fields - for field in self.SensitiveFields: + for field in self.EncryptedIdentifierFields: # BACK COMPAT: Handle nested dictionaries obj = session_dict keys = field.split(".") From 98e9ee272ee4be703305ff264e54ab60a484f601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Hru=C5=A1ka?= Date: Mon, 10 Jul 2023 18:08:42 +0200 Subject: [PATCH 06/10] Apply asab.storage encryption where due. Refactor Sessionservice.get_by() - only one key-value pair is needed. --- seacatauth/authn/m2m.py | 2 +- seacatauth/cookie/service.py | 2 +- seacatauth/openidconnect/service.py | 2 +- seacatauth/openidconnect/session.py | 1 - seacatauth/session/service.py | 31 ++++++++++++++--------------- 5 files changed, 18 insertions(+), 20 deletions(-) diff --git a/seacatauth/authn/m2m.py b/seacatauth/authn/m2m.py index da98f9a0..d3fc3e83 100644 --- a/seacatauth/authn/m2m.py +++ b/seacatauth/authn/m2m.py @@ -70,7 +70,7 @@ async def authenticate_request(self, request, client_id): # Find session object try: - session = await self.SessionService.get_by({SessionAdapter.FN.Credentials.Id: credentials_id}) + session = await self.SessionService.get_by(SessionAdapter.FN.Credentials.Id, credentials_id) except KeyError: session = None diff --git a/seacatauth/cookie/service.py b/seacatauth/cookie/service.py index 130c8963..8b22d262 100644 --- a/seacatauth/cookie/service.py +++ b/seacatauth/cookie/service.py @@ -108,7 +108,7 @@ async def get_session_by_session_cookie_value(self, cookie_value): Get session by cookie value. """ try: - session = await self.SessionService.get_by({SessionAdapter.FN.Cookie.Id: cookie_value}) + session = await self.SessionService.get_by(SessionAdapter.FN.Cookie.Id, cookie_value) except KeyError: L.info("Session not found.", struct_data={"sci": cookie_value}) return None diff --git a/seacatauth/openidconnect/service.py b/seacatauth/openidconnect/service.py index 708eb627..c544b892 100644 --- a/seacatauth/openidconnect/service.py +++ b/seacatauth/openidconnect/service.py @@ -201,7 +201,7 @@ async def get_session_by_access_token(self, token_value): # Locate the session try: - session = await self.SessionService.get_by({SessionAdapter.FN.OAuth2.AccessToken: access_token}) + session = await self.SessionService.get_by(SessionAdapter.FN.OAuth2.AccessToken, access_token) except KeyError: L.info("Session not found by access token: {}".format(access_token)) return None diff --git a/seacatauth/openidconnect/session.py b/seacatauth/openidconnect/session.py index f819a33d..2a9d70c0 100644 --- a/seacatauth/openidconnect/session.py +++ b/seacatauth/openidconnect/session.py @@ -18,4 +18,3 @@ def oauth2_session_builder(oauth2_data): if scope is None or "cookie" not in scope: yield (SessionAdapter.FN.OAuth2.AccessToken, secrets.token_bytes(token_length)) yield (SessionAdapter.FN.OAuth2.RefreshToken, secrets.token_bytes(token_length)) - yield (SessionAdapter.FN.OAuth2.IdToken, None) diff --git a/seacatauth/session/service.py b/seacatauth/session/service.py index f14087dc..7c624ca9 100644 --- a/seacatauth/session/service.py +++ b/seacatauth/session/service.py @@ -205,9 +205,11 @@ async def create_session( session_builders = list() for session_builder in session_builders: for key, value in session_builder: - if key in SessionAdapter.SensitiveFields and value is not None: + if key in SessionAdapter.EncryptedIdentifierFields and value is not None: value = SessionAdapter.EncryptedPrefix + self.aes_encrypt(value) - upsertor.set(key, value) + upsertor.set(key, value) + else: + upsertor.set(key, value, encrypt=(key in SessionAdapter.EncryptedAttributes)) session_id = await upsertor.execute(event_type=EventTypes.SESSION_CREATED) @@ -234,30 +236,26 @@ async def update_session(self, session_id: str, session_builders: list): for session_builder in session_builders: for key, value in session_builder: - upsertor.set(key, value) + upsertor.set(key, value, encrypt=(key in SessionAdapter.EncryptedAttributes)) await upsertor.execute(event_type=EventTypes.SESSION_UPDATED) return await self.get(session_id) - async def get_by(self, criteria: dict): + async def get_by(self, key: str, value): # Encrypt sensitive fields - query_filter = {} - for key, value in criteria.items(): - if key in SessionAdapter.SensitiveFields: - query_filter[key] = SessionAdapter.EncryptedPrefix + self.aes_encrypt(value) - else: - query_filter[key] = value + if key in SessionAdapter.EncryptedIdentifierFields: + value = SessionAdapter.EncryptedPrefix + self.aes_encrypt(value) - collection = self.StorageService.Database[self.SessionCollection] - session_dict = await collection.find_one(query_filter) + session_dict = await self.StorageService.get_by( + self.SessionCollection, key, value, decrypt=SessionAdapter.EncryptedAttributes) if session_dict is None: - raise exceptions.SessionNotFoundError("Session not found in database.", query=criteria) + raise exceptions.SessionNotFoundError("Session not found in database.", query={key: value}) # Do not return expired sessions if session_dict[SessionAdapter.FN.Session.Expiration] < datetime.datetime.now(datetime.timezone.utc): - raise exceptions.SessionNotFoundError("Session expired.", query=criteria) + raise exceptions.SessionNotFoundError("Session expired.", query={key: value}) try: session = SessionAdapter(self, session_dict) @@ -265,7 +263,7 @@ async def get_by(self, criteria: dict): L.error("Failed to create SessionAdapter from database object", struct_data={ "sid": session_dict.get("_id"), }) - raise exceptions.SessionNotFoundError("Session not found in database.", query=criteria) from e + raise exceptions.SessionNotFoundError("Session not found in database.", query={key: value}) from e return session @@ -273,7 +271,8 @@ async def get_by(self, criteria: dict): async def get(self, session_id): if isinstance(session_id, str): session_id = bson.ObjectId(session_id) - session_dict = await self.StorageService.get(self.SessionCollection, session_id) + session_dict = await self.StorageService.get( + self.SessionCollection, session_id, decrypt=SessionAdapter.EncryptedAttributes) # Do not return expired sessions if session_dict[SessionAdapter.FN.Session.Expiration] < datetime.datetime.now(datetime.timezone.utc): From 68559c442fd4d7eb6c39870fef88c0dd18ef7e21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Hru=C5=A1ka?= Date: Tue, 11 Jul 2023 12:32:57 +0200 Subject: [PATCH 07/10] Louder logging when session deserialization fails. --- seacatauth/session/service.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/seacatauth/session/service.py b/seacatauth/session/service.py index 7c624ca9..59842570 100644 --- a/seacatauth/session/service.py +++ b/seacatauth/session/service.py @@ -260,10 +260,10 @@ async def get_by(self, key: str, value): try: session = SessionAdapter(self, session_dict) except Exception as e: - L.error("Failed to create SessionAdapter from database object", struct_data={ + L.exception("Failed to create SessionAdapter from database object.", struct_data={ "sid": session_dict.get("_id"), }) - raise exceptions.SessionNotFoundError("Session not found in database.", query={key: value}) from e + raise exceptions.SessionNotFoundError("Session deserialization failed.", query={key: value}) from e return session @@ -281,10 +281,10 @@ async def get(self, session_id): try: session = SessionAdapter(self, session_dict) except Exception as e: - L.exception("Failed to create SessionAdapter from database object", struct_data={ + L.exception("Failed to create SessionAdapter from database object.", struct_data={ "sid": session_dict.get("_id"), }) - raise exceptions.SessionNotFoundError("Session not found in database.", session_id=session_id) from e + raise exceptions.SessionNotFoundError("Session deserialization failed.", session_id=session_id) from e return session From de8f0304b7dcc17280c87309fd6fb82236f09590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Hru=C5=A1ka?= Date: Tue, 11 Jul 2023 12:50:31 +0200 Subject: [PATCH 08/10] ELK docs about cookie entrypoint --- docs/integrations/elk.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/docs/integrations/elk.md b/docs/integrations/elk.md index 69fcb6a6..918fad35 100644 --- a/docs/integrations/elk.md +++ b/docs/integrations/elk.md @@ -47,7 +47,11 @@ password=elasticpassword ### Client configuration Use Seacat Auth client API (or Seacat Admin UI) to register Kibana as a client. -In our case, we can send the following request: +The request body must include a human-readable `client_name`, `redirect_uris` array containing the URL of Kibana web UI +and `cookie_entry_uri` for your hostname (we define this location in the Nginx configuration below.). +We also recommend to set `redirect_uri_validation_method` to `prefix_match` if you want to allow immediate redirections +to Kibana subpaths. +In our case, we can send the following request (Remember to use your actual hostnames instead of `example.com`!): ``` POST /client @@ -56,7 +60,8 @@ POST /client "redirect_uri_validation_method": "prefix_match", "redirect_uris": [ "https://example.com/kibana" - ] + ], + "cookie_entry_uri": "https://example.com/seacat_auth/cookie" } ``` @@ -98,7 +103,7 @@ location /kibana/ { proxy_set_header Authorization $auth_header; # In the case when introspection detects invalid authorization, redirect to OAuth authorize endpoint - # !! Use your client's actual client_id !! + # !! Use your client's actual client_id and your site's actual hostname !! error_page 401 https://example.com/auth/api/openidconnect/authorize?response_type=code&scope=cookie%20batman&client_id=RZhlE-D4yuJxoKitYVL4dg&redirect_uri=https://example.com$request_uri; # Headers required by Kibana @@ -136,12 +141,13 @@ location = /_kibana_introspection { } ``` -#### Client cookie entry point +#### Cookie entry point -Must be located on the same hostname as the protected client location. +Must be located on the same hostname as the protected client location. +There should be one cookie entry point exposed per hostname, shared by all cookie-based clients on that hostname. ```nginx -location = /auth/api/cookie/kibana { +location = /seacat_auth/cookie { # Seacat Auth cookie entry upstream proxy_method POST; proxy_pass http://seacat_auth_api/cookie/entry; @@ -149,6 +155,6 @@ location = /auth/api/cookie/kibana { # Transfer the OAuth authorization code from query to request body # !! Use your client's actual client_id !! proxy_set_header Content-Type "application/x-www-form-urlencoded"; - proxy_set_body "client_id=RZhlE-D4yuJxoKitYVL4dg&grant_type=authorization_code&code=$arg_code"; + proxy_set_body $args; } ``` \ No newline at end of file From 213889470a953b8979f78ccf1fa7208445d111fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Hru=C5=A1ka?= Date: Tue, 11 Jul 2023 14:45:15 +0200 Subject: [PATCH 09/10] Update CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a6ec098..5b5cc6db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,14 @@ ## Release candidate +### Breaking changes +- Old Batman sessions are invalidated (#230, PLUM Sprint 230630) + ### Fix - Root session must be as long as its longest subsession (#228, PLUM Sprint 230630) - Webauthn `user_name` can be either email address or phone number (#229, PLUM Sprint 230630) +- Batman token uses native ASAB Storage encryption (#230, PLUM Sprint 230630) + --- From 2f34360480132860072fad85c2d74224b1cc5e57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Hru=C5=A1ka?= Date: Tue, 11 Jul 2023 14:49:56 +0200 Subject: [PATCH 10/10] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b5cc6db..8b2bf234 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ - Webauthn `user_name` can be either email address or phone number (#229, PLUM Sprint 230630) - Batman token uses native ASAB Storage encryption (#230, PLUM Sprint 230630) +### Features +- Added alternative POST endpoint for Batman introspection (#230, PLUM Sprint 230630) ---