From cb57752903bf7f7ceaa296e226a56dbc9cb034e1 Mon Sep 17 00:00:00 2001 From: John Westcott IV <32551173+john-westcott-iv@users.noreply.github.com> Date: Sun, 27 Feb 2022 07:27:25 -0500 Subject: [PATCH] Changing session cookie name and added a way for clients to know what the name is #11413 (#11679) * Changing session cookie name and added a way for clients to know what the key name is * Adding session information to docs * Fixing how awxkit gets the session id header --- awx/api/generics.py | 1 + awx/settings/defaults.py | 4 ++++ awx/sso/views.py | 1 + awxkit/awxkit/api/client.py | 15 ++++++++++++--- awxkit/awxkit/awx/utils.py | 8 ++++---- awxkit/awxkit/ws.py | 6 ++++-- docs/auth/session.md | 21 +++++++++++++-------- 7 files changed, 39 insertions(+), 17 deletions(-) diff --git a/awx/api/generics.py b/awx/api/generics.py index b10728f32a..58ed5a9801 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -99,6 +99,7 @@ class LoggedLoginView(auth_views.LoginView): current_user = smart_text(JSONRenderer().render(current_user.data)) current_user = urllib.parse.quote('%s' % current_user, '') ret.set_cookie('current_user', current_user, secure=settings.SESSION_COOKIE_SECURE or None) + ret.setdefault('X-API-Session-Cookie-Name', getattr(settings, 'SESSION_COOKIE_NAME', 'awx_sessionid')) return ret else: diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 7a41615b10..9d0078916d 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -252,6 +252,10 @@ SESSION_COOKIE_SECURE = True # Note: This setting may be overridden by database settings. SESSION_COOKIE_AGE = 1800 +# Name of the cookie that contains the session information. +# Note: Changing this value may require changes to any clients. +SESSION_COOKIE_NAME = 'awx_sessionid' + # Maximum number of per-user valid, concurrent sessions. # -1 is unlimited # Note: This setting may be overridden by database settings. diff --git a/awx/sso/views.py b/awx/sso/views.py index 35f81b26a4..2f3a448af9 100644 --- a/awx/sso/views.py +++ b/awx/sso/views.py @@ -46,6 +46,7 @@ class CompleteView(BaseRedirectView): current_user = smart_text(JSONRenderer().render(current_user.data)) current_user = urllib.parse.quote('%s' % current_user, '') response.set_cookie('current_user', current_user, secure=settings.SESSION_COOKIE_SECURE or None) + response.setdefault('X-API-Session-Cookie-Name', getattr(settings, 'SESSION_COOKIE_NAME', 'awx_sessionid')) return response diff --git a/awxkit/awxkit/api/client.py b/awxkit/awxkit/api/client.py index 1cea4a61c2..04b399e079 100644 --- a/awxkit/awxkit/api/client.py +++ b/awxkit/awxkit/api/client.py @@ -33,6 +33,10 @@ class Connection(object): def __init__(self, server, verify=False): self.server = server self.verify = verify + # Note: We use the old sessionid here incase someone is trying to connect to an older AWX version + # There is a check below so that if AWX returns an X-API-Session-Cookie-Name we will grab it and + # connect with the new session cookie name. + self.session_cookie_name = 'sessionid' if not self.verify: requests.packages.urllib3.disable_warnings() @@ -49,8 +53,13 @@ class Connection(object): _next = kwargs.get('next') if _next: headers = self.session.headers.copy() - self.post('/api/login/', headers=headers, data=dict(username=username, password=password, next=_next)) - self.session_id = self.session.cookies.get('sessionid') + response = self.post('/api/login/', headers=headers, data=dict(username=username, password=password, next=_next)) + # The login causes a redirect so we need to search the history of the request to find the header + for historical_response in response.history: + if 'X-API-Session-Cookie-Name' in historical_response.headers: + self.session_cookie_name = historical_response.headers.get('X-API-Session-Cookie-Name') + + self.session_id = self.session.cookies.get(self.session_cookie_name, None) self.uses_session_cookie = True else: self.session.auth = (username, password) @@ -61,7 +70,7 @@ class Connection(object): def logout(self): if self.uses_session_cookie: - self.session.cookies.pop('sessionid', None) + self.session.cookies.pop(self.session_cookie_name, None) else: self.session.auth = None diff --git a/awxkit/awxkit/awx/utils.py b/awxkit/awxkit/awx/utils.py index 6fc3a18480..df61f0b7a0 100644 --- a/awxkit/awxkit/awx/utils.py +++ b/awxkit/awxkit/awx/utils.py @@ -95,12 +95,12 @@ def as_user(v, username, password=None): # requests doesn't provide interface for retrieving # domain segregated cookies other than iterating. for cookie in connection.session.cookies: - if cookie.name == 'sessionid': + if cookie.name == connection.session_cookie_name: session_id = cookie.value domain = cookie.domain break if session_id: - del connection.session.cookies['sessionid'] + del connection.session.cookies[connection.session_cookie_name] if access_token: kwargs = dict(token=access_token) else: @@ -114,9 +114,9 @@ def as_user(v, username, password=None): if config.use_sessions: if access_token: connection.session.auth = None - del connection.session.cookies['sessionid'] + del connection.session.cookies[connection.session_cookie_name] if session_id: - connection.session.cookies.set('sessionid', session_id, domain=domain) + connection.session.cookies.set(connection.session_cookie_name, session_id, domain=domain) else: connection.session.auth = previous_auth diff --git a/awxkit/awxkit/ws.py b/awxkit/awxkit/ws.py index d56fccf719..b2b51fefba 100644 --- a/awxkit/awxkit/ws.py +++ b/awxkit/awxkit/ws.py @@ -51,7 +51,9 @@ class WSClient(object): # Subscription group types - def __init__(self, token=None, hostname='', port=443, secure=True, session_id=None, csrftoken=None, add_received_time=False): + def __init__( + self, token=None, hostname='', port=443, secure=True, session_id=None, csrftoken=None, add_received_time=False, session_cookie_name='awx_sessionid' + ): # delay this import, because this is an optional dependency import websocket @@ -78,7 +80,7 @@ class WSClient(object): if self.token is not None: auth_cookie = 'token="{0.token}";'.format(self) elif self.session_id is not None: - auth_cookie = 'sessionid="{0.session_id}"'.format(self) + auth_cookie = '{1}="{0.session_id}"'.format(self, session_cookie_name) if self.csrftoken: auth_cookie += ';csrftoken={0.csrftoken}'.format(self) else: diff --git a/docs/auth/session.md b/docs/auth/session.md index f5a3d3888f..df1248ae3f 100644 --- a/docs/auth/session.md +++ b/docs/auth/session.md @@ -6,9 +6,9 @@ Session authentication is a safer way of utilizing HTTP(S) cookies. Theoreticall `Cookie` header, but this method is vulnerable to cookie hijacks, where crackers can see and steal user information from the cookie payload. -Session authentication, on the other hand, sets a single `session_id` cookie. The `session_id` -is *a random string which will be mapped to user authentication informations by server*. Crackers who -hijack cookies will only get the `session_id` itself, which does not imply any critical user info, is valid only for +Session authentication, on the other hand, sets a single `awx_sessionid` cookie. The `awx_sessionid` +is *a random string which will be mapped to user authentication information by the server*. Crackers who +hijack cookies will only get the `awx_sessionid` itself, which does not imply any critical user info, is valid only for a limited time, and can be revoked at any time. > Note: The CSRF token will by default allow HTTP. To increase security, the `CSRF_COOKIE_SECURE` setting should @@ -34,22 +34,27 @@ be provided in the form: * `next`: The path of the redirect destination, in API browser `"/api/"` is used. * `csrfmiddlewaretoken`: The CSRF token, usually populated by using Django template `{% csrf_token %}`. -The `session_id` is provided as a return `Set-Cookie` header. Here is a typical one: +The `awx_session_id` is provided as a return `Set-Cookie` header. Here is a typical one: ``` -Set-Cookie: sessionid=lwan8l5ynhrqvps280rg5upp7n3yp6ds; expires=Tue, 21-Nov-2017 16:33:13 GMT; httponly; Max-Age=1209600; Path=/ +Set-Cookie: awx_sessionid=lwan8l5ynhrqvps280rg5upp7n3yp6ds; expires=Tue, 21-Nov-2017 16:33:13 GMT; httponly; Max-Age=1209600; Path=/ ``` + +In addition, when the `awx_sessionid` a header called `X-API-Session-Cookie-Name` this header will only be displayed once on a successful logging and denotes the name of the session cookie name. By default this is `awx_sessionid` but can be changed (see below). + Any client should follow the standard rules of [cookie protocol](https://tools.ietf.org/html/rfc6265) to -parse that header to obtain information about the session, such as session cookie name (`session_id`), +parse that header to obtain information about the session, such as session cookie name (`awx_sessionid`), session cookie value, expiration date, duration, etc. +The name of the cookie is configurable by Tower Configuration setting `SESSION_COOKIE_NAME` under the category `authentication`. It is a string. The default session cookie name is `awx_sessionid`. + The duration of the cookie is configurable by Tower Configuration setting `SESSION_COOKIE_AGE` under category `authentication`. It is an integer denoting the number of seconds the session cookie should live. The default session cookie age is two weeks. -After a valid session is acquired, a client should provide the `session_id` as a cookie for subsequent requests +After a valid session is acquired, a client should provide the `awx_sessionid` as a cookie for subsequent requests in order to be authenticated. For example: ``` -Cookie: sessionid=lwan8l5ynhrqvps280rg5upp7n3yp6ds; ... +Cookie: awx_sessionid=lwan8l5ynhrqvps280rg5upp7n3yp6ds; ... ``` User should use the `/api/logout/` endpoint to log out. In the API browser, a logged-in user can do that by