mattupstate/flask-security — pull request #352 by fuhrysteve (2014-12-28 13:37:29+00:00)
Title: X-Forwarded-For can contain multiple IP addresses

A common server configuration for setting up an nginx proxy for flask is:

```
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
```

In some cases, folks have issues logging in due to `$proxy_add_x_forwarded_for` containing multiple IPs. The workaround I've had is to set `X-Forwarded-For` to `$remote_addr`, and that actually works, however it seems that this is not quite proper, according to the docs.

My proposal is to prefer `X-Real-IP` if it is available, otherwise default to whatever the first IP in `X-Forwarded-For` is.

From the nginx docs: http://nginx.org/en/docs/http/ngx_http_proxy_module.html

```
$proxy_add_x_forwarded_for
the “X-Forwarded-For” client request header field with the $remote_addr variable
appended to it, separated by a comma. If the “X-Forwarded-For” field is not present
in the client request header, the $proxy_add_x_forwarded_for variable is equal to
the $remote_addr variable.
```

Here's a stack trace illustrating the issue (some indent added for clarity):

```python
DataError: (raised as a result of Query-invoked autoflush; consider using a
session.no_autoflush block if this flush is occurring prematurely) (DataError)
invalid input syntax for type inet: "172.21.5.123, 189.254.205.210"
LINE 1: ...-27T00:00:25.154208'::timestamp, current_login_ip='172.21.5....
^
'UPDATE user_account
SET
last_login_at=%(last_login_at)s,
current_login_at=%(current_login_at)s,
current_login_ip=%(current_login_ip)s,
login_count=%(login_count)s, modifieddate=now()
WHERE
user_account.user_id = %(user_account_user_id)s' {
'current_login_at': datetime.datetime(2014, 12, 27, 0, 0, 25, 154208),
'login_count': 14,
'user_account_user_id': 3,
'current_login_ip': '172.21.5.123, 189.254.205.210',
'last_login_at': datetime.datetime(
2014, 12, 26, 21, 21, 21, 456305,
tzinfo=psycopg2.tz.FixedOffsetTimezone(offset=0, name=None))
}

Stacktrace (most recent call last):

File "flask/app.py", line 1817, in wsgi_app
response = self.full_dispatch_request()
File "flask/app.py", line 1477, in full_dispatch_request
rv = self.handle_user_exception(e)
File "flask/app.py", line 1381, in handle_user_exception
reraise(exc_type, exc_value, tb)
File "flask/_compat.py", line 33, in reraise
raise value
File "flask/app.py", line 1475, in full_dispatch_request
rv = self.dispatch_request()
File "flask/app.py", line 1461, in dispatch_request
return self.view_functions[rule.endpoint](**req.view_args)
File "flask_security/decorators.py", line 205, in wrapper
return f(*args, **kwargs)
File "flask_security/views.py", line 76, in login
login_user(form.user, remember=form.remember.data)
File "flask_security/utils.py", line 82, in login_user
identity=Identity(user.id))
File "blinker/base.py", line 267, in send
for receiver in self.receivers_for(sender)]
File "blinker/base.py", line 267, in
for receiver in self.receivers_for(sender)]
File "site-packages/flask_principal.py", line 469, in _on_identity_changed
self.set_identity(identity)
File "site-packages/flask_principal.py", line 418, in set_identity
self._set_thread_identity(identity)
File "site-packages/flask_principal.py", line 463, in _set_thread_identity
identity=identity)
File "blinker/base.py", line 267, in send
for receiver in self.receivers_for(sender)]
File "blinker/base.py", line 267, in
for receiver in self.receivers_for(sender)]
File "flask_security/core.py", line 214, in _on_identity_loaded
for role in current_user.roles:
File "werkzeug/local.py", line 338, in __getattr__
return getattr(self._get_current_object(), name)
File "sqlalchemy/orm/attributes.py", line 239, in __get__
return self.impl.get(instance_state(instance), dict_)
File "sqlalchemy/orm/attributes.py", line 591, in get
value = self.callable_(state, passive)
File "sqlalchemy/orm/strategies.py", line 534, in _load_for_state
return self._emit_lazyload(session, state, ident_key, passive)
File "", line 1, in
File "sqlalchemy/orm/strategies.py", line 603, in _emit_lazyload
result = q.all()
File "sqlalchemy/orm/query.py", line 2320, in all
return list(self)
File "sqlalchemy/orm/query.py", line 2437, in __iter__
self.session._autoflush()
File "sqlalchemy/orm/session.py", line 1208, in _autoflush
util.raise_from_cause(e)
File "sqlalchemy/util/compat.py", line 188, in raise_from_cause
reraise(type(exception), exception, tb=exc_tb, cause=exc_value)
File "sqlalchemy/util/compat.py", line 182, in reraise
raise value
File "sqlalchemy/orm/session.py", line 1198, in _autoflush
self.flush()
File "sqlalchemy/orm/session.py", line 1919, in flush
self._flush(objects)
File "sqlalchemy/orm/session.py", line 2037, in _flush
transaction.rollback(_capture_exception=True)
File "sqlalchemy/util/langhelpers.py", line 60, in __exit__
compat.reraise(exc_type, exc_value, exc_tb)
File "sqlalchemy/util/compat.py", line 182, in reraise
raise value
File "sqlalchemy/orm/session.py", line 2001, in _flush
flush_context.execute()
File "sqlalchemy/orm/unitofwork.py", line 372, in execute
rec.execute(self)
File "sqlalchemy/orm/unitofwork.py", line 526, in execute
uow
File "sqlalchemy/orm/persistence.py", line 60, in save_obj
mapper, table, update)
File "sqlalchemy/orm/persistence.py", line 518, in _emit_update_statements
execute(statement, params)
File "sqlalchemy/engine/base.py", line 729, in execute
return meth(self, multiparams, params)
File "sqlalchemy/sql/elements.py", line 322, in _execute_on_connection
return connection._execute_clauseelement(self, multiparams, params)
File "sqlalchemy/engine/base.py", line 826, in _execute_clauseelement
compiled_sql, distilled_params
File "sqlalchemy/engine/base.py", line 958, in _execute_context
context)
File "sqlalchemy/engine/base.py", line 1159, in _handle_dbapi_exception
exc_info
File "sqlalchemy/util/compat.py", line 188, in raise_from_cause
reraise(type(exception), exception, tb=exc_tb, cause=exc_value)
File "sqlalchemy/util/compat.py", line 181, in reraise
raise value.with_traceback(tb)
File "sqlalchemy/engine/base.py", line 951, in _execute_context
context)
File "sqlalchemy/engine/default.py", line 436, in do_execute
cursor.execute(statement, parameters)
```