diff --git a/CHANGES b/CHANGES index b39a2001..2f47eab1 100644 --- a/CHANGES +++ b/CHANGES @@ -42,6 +42,7 @@ Relase date to be decided, codename to be chosen. pluggable (class based) views. - Fixed an issue where the test client if used with the with statement did not trigger the execution of the teardown handlers. +- Added finer control over the session cookie parameters. Version 0.7.3 ------------- diff --git a/docs/config.rst b/docs/config.rst index df31aba0..1ed004d2 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -70,6 +70,20 @@ The following configuration values are used internally by Flask: very risky). ``SECRET_KEY`` the secret key ``SESSION_COOKIE_NAME`` the name of the session cookie +``SESSION_COOKIE_DOMAIN`` the domain for the session cookie. If + this is not set, the cookie will be + valid for all subdomains of + ``SERVER_NAME``. +``SESSION_COOKIE_PATH`` the path for the session cookie. If + this is not set the cookie will be valid + for all of ``APPLICATION_ROOT`` or if + that is not set for ``'/'``. +``SESSION_COOKIE_HTTPONLY`` controls if the cookie should be set + with the httponly flag. Defaults to + `True`. +``SESSION_COOKIE_SECURE`` controls if the cookie should be set + with the secure flag. Defaults to + `False`. ``PERMANENT_SESSION_LIFETIME`` the lifetime of a permanent session as :class:`datetime.timedelta` object. ``USE_X_SENDFILE`` enable/disable x-sendfile @@ -142,7 +156,9 @@ The following configuration values are used internally by Flask: .. versionadded:: 0.8 ``TRAP_BAD_REQUEST_ERRORS``, ``TRAP_HTTP_EXCEPTIONS``, - ``APPLICATION_ROOT`` + ``APPLICATION_ROOT``, ``SESSION_COOKIE_DOMAIN``, + ``SESSION_COOKIE_PATH``, ``SESSION_COOKIE_HTTPONLY``, + ``SESSION_COOKIE_SECURE`` Configuring from Files ---------------------- diff --git a/flask/app.py b/flask/app.py index 609ab952..3c479df5 100644 --- a/flask/app.py +++ b/flask/app.py @@ -231,12 +231,16 @@ class Flask(_PackageBoundObject): 'PROPAGATE_EXCEPTIONS': None, 'PRESERVE_CONTEXT_ON_EXCEPTION': None, 'SECRET_KEY': None, - 'SESSION_COOKIE_NAME': 'session', 'PERMANENT_SESSION_LIFETIME': timedelta(days=31), 'USE_X_SENDFILE': False, 'LOGGER_NAME': None, 'SERVER_NAME': None, 'APPLICATION_ROOT': None, + 'SESSION_COOKIE_NAME': 'session', + 'SESSION_COOKIE_DOMAIN': None, + 'SESSION_COOKIE_PATH': None, + 'SESSION_COOKIE_HTTPONLY': True, + 'SESSION_COOKIE_SECURE': False, 'MAX_CONTENT_LENGTH': None, 'TRAP_BAD_REQUEST_ERRORS': False, 'TRAP_HTTP_EXCEPTIONS': False diff --git a/flask/sessions.py b/flask/sessions.py index fda84a25..8a9fae51 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -123,16 +123,33 @@ class SessionInterface(object): """Helpful helper method that returns the cookie domain that should be used for the session cookie if session cookies are used. """ + if app.config['SESSION_COOKIE_DOMAIN'] is not None: + return app.config['SESSION_COOKIE_DOMAIN'] if app.config['SERVER_NAME'] is not None: # chop of the port which is usually not supported by browsers return '.' + app.config['SERVER_NAME'].rsplit(':', 1)[0] def get_cookie_path(self, app): """Returns the path for which the cookie should be valid. The - default implementation uses the value from the ``APPLICATION_ROOT`` - configuration variable or uses ``/`` if it's `None`. + default implementation uses the value from the SESSION_COOKIE_PATH`` + config var if it's set, and falls back to ``APPLICATION_ROOT`` or + uses ``/`` if it's `None`. """ - return app.config['APPLICATION_ROOT'] or '/' + return app.config['SESSION_COOKIE_PATH'] or \ + app.config['APPLICATION_ROOT'] or '/' + + def get_cookie_httponly(self, app): + """Returns True if the session cookie should be httponly. This + currently just returns the value of the ``SESSION_COOKIE_HTTPONLY`` + config var. + """ + return app.config['SESSION_COOKIE_HTTPONLY'] + + def get_cookie_secure(self, app): + """Returns True if the cookie should be secure. This currently + just returns the value of the ``SESSION_COOKIE_SECURE`` setting. + """ + return app.config['SESSION_COOKIE_SECURE'] def get_expiration_time(self, app, session): """A helper method that returns an expiration date for the session @@ -177,9 +194,12 @@ class SecureCookieSessionInterface(SessionInterface): expires = self.get_expiration_time(app, session) domain = self.get_cookie_domain(app) path = self.get_cookie_path(app) + httponly = self.get_cookie_httponly(app) + secure = self.get_cookie_secure(app) if session.modified and not session: response.delete_cookie(app.session_cookie_name, path=path, domain=domain) else: session.save_cookie(response, app.session_cookie_name, path=path, - expires=expires, httponly=True, domain=domain) + expires=expires, httponly=httponly, + secure=secure, domain=domain) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 947d0073..33975b99 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -207,6 +207,28 @@ class BasicFunctionalityTestCase(FlaskTestCase): rv = app.test_client().get('/', 'http://example.com:8080/') self.assert_('path=/bar' in rv.headers['set-cookie'].lower()) + def test_session_using_session_settings(self): + app = flask.Flask(__name__) + app.config.update( + SECRET_KEY='foo', + SERVER_NAME='www.example.com:8080', + APPLICATION_ROOT='/test', + SESSION_COOKIE_DOMAIN='.example.com', + SESSION_COOKIE_HTTPONLY=False, + SESSION_COOKIE_SECURE=True, + SESSION_COOKIE_PATH='/' + ) + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'Hello World' + rv = app.test_client().get('/', 'http://www.example.com:8080/test/') + cookie = rv.headers['set-cookie'].lower() + self.assert_('domain=.example.com' in cookie) + self.assert_('path=/;' in cookie) + self.assert_('secure' in cookie) + self.assert_('httponly' not in cookie) + def test_missing_session(self): app = flask.Flask(__name__) def expect_exception(f, *args, **kwargs):