login_manager.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. # -*- coding: utf-8 -*-
  2. '''
  3. flask_login.login_manager
  4. -------------------------
  5. The LoginManager class.
  6. '''
  7. import warnings
  8. from datetime import datetime
  9. from flask import (_request_ctx_stack, abort, current_app, flash, redirect,
  10. request, session)
  11. from ._compat import text_type
  12. from .config import (COOKIE_NAME, COOKIE_DURATION, COOKIE_SECURE,
  13. COOKIE_HTTPONLY, LOGIN_MESSAGE, LOGIN_MESSAGE_CATEGORY,
  14. REFRESH_MESSAGE, REFRESH_MESSAGE_CATEGORY, ID_ATTRIBUTE,
  15. AUTH_HEADER_NAME, SESSION_KEYS)
  16. from .mixins import AnonymousUserMixin
  17. from .signals import (user_loaded_from_cookie, user_loaded_from_header,
  18. user_loaded_from_request, user_unauthorized,
  19. user_needs_refresh, user_accessed, session_protected)
  20. from .utils import (_get_user, login_url, _create_identifier,
  21. _user_context_processor, encode_cookie, decode_cookie)
  22. class LoginManager(object):
  23. '''
  24. This object is used to hold the settings used for logging in. Instances of
  25. :class:`LoginManager` are *not* bound to specific apps, so you can create
  26. one in the main body of your code and then bind it to your
  27. app in a factory function.
  28. '''
  29. def __init__(self, app=None, add_context_processor=True):
  30. #: A class or factory function that produces an anonymous user, which
  31. #: is used when no one is logged in.
  32. self.anonymous_user = AnonymousUserMixin
  33. #: The name of the view to redirect to when the user needs to log in.
  34. #: (This can be an absolute URL as well, if your authentication
  35. #: machinery is external to your application.)
  36. self.login_view = None
  37. #: Names of views to redirect to when the user needs to log in,
  38. #: per blueprint. If the key value is set to None the value of
  39. #: :attr:`login_view` will be used instead.
  40. self.blueprint_login_views = {}
  41. #: The message to flash when a user is redirected to the login page.
  42. self.login_message = LOGIN_MESSAGE
  43. #: The message category to flash when a user is redirected to the login
  44. #: page.
  45. self.login_message_category = LOGIN_MESSAGE_CATEGORY
  46. #: The name of the view to redirect to when the user needs to
  47. #: reauthenticate.
  48. self.refresh_view = None
  49. #: The message to flash when a user is redirected to the 'needs
  50. #: refresh' page.
  51. self.needs_refresh_message = REFRESH_MESSAGE
  52. #: The message category to flash when a user is redirected to the
  53. #: 'needs refresh' page.
  54. self.needs_refresh_message_category = REFRESH_MESSAGE_CATEGORY
  55. #: The mode to use session protection in. This can be either
  56. #: ``'basic'`` (the default) or ``'strong'``, or ``None`` to disable
  57. #: it.
  58. self.session_protection = 'basic'
  59. #: If present, used to translate flash messages ``self.login_message``
  60. #: and ``self.needs_refresh_message``
  61. self.localize_callback = None
  62. self.user_callback = None
  63. self.unauthorized_callback = None
  64. self.needs_refresh_callback = None
  65. self.id_attribute = ID_ATTRIBUTE
  66. self.header_callback = None
  67. self.request_callback = None
  68. if app is not None:
  69. self.init_app(app, add_context_processor)
  70. def setup_app(self, app, add_context_processor=True): # pragma: no cover
  71. '''
  72. This method has been deprecated. Please use
  73. :meth:`LoginManager.init_app` instead.
  74. '''
  75. warnings.warn('Warning setup_app is deprecated. Please use init_app.',
  76. DeprecationWarning)
  77. self.init_app(app, add_context_processor)
  78. def init_app(self, app, add_context_processor=True):
  79. '''
  80. Configures an application. This registers an `after_request` call, and
  81. attaches this `LoginManager` to it as `app.login_manager`.
  82. :param app: The :class:`flask.Flask` object to configure.
  83. :type app: :class:`flask.Flask`
  84. :param add_context_processor: Whether to add a context processor to
  85. the app that adds a `current_user` variable to the template.
  86. Defaults to ``True``.
  87. :type add_context_processor: bool
  88. '''
  89. app.login_manager = self
  90. app.after_request(self._update_remember_cookie)
  91. self._login_disabled = app.config.get('LOGIN_DISABLED', False)
  92. if add_context_processor:
  93. app.context_processor(_user_context_processor)
  94. def unauthorized(self):
  95. '''
  96. This is called when the user is required to log in. If you register a
  97. callback with :meth:`LoginManager.unauthorized_handler`, then it will
  98. be called. Otherwise, it will take the following actions:
  99. - Flash :attr:`LoginManager.login_message` to the user.
  100. - If the app is using blueprints find the login view for
  101. the current blueprint using `blueprint_login_views`. If the app
  102. is not using blueprints or the login view for the current
  103. blueprint is not specified use the value of `login_view`.
  104. Redirect the user to the login view. (The page they were
  105. attempting to access will be passed in the ``next`` query
  106. string variable, so you can redirect there if present instead
  107. of the homepage.)
  108. If :attr:`LoginManager.login_view` is not defined, then it will simply
  109. raise a HTTP 401 (Unauthorized) error instead.
  110. This should be returned from a view or before/after_request function,
  111. otherwise the redirect will have no effect.
  112. '''
  113. user_unauthorized.send(current_app._get_current_object())
  114. if self.unauthorized_callback:
  115. return self.unauthorized_callback()
  116. if request.blueprint in self.blueprint_login_views:
  117. login_view = self.blueprint_login_views[request.blueprint]
  118. else:
  119. login_view = self.login_view
  120. if not login_view:
  121. abort(401)
  122. if self.login_message:
  123. if self.localize_callback is not None:
  124. flash(self.localize_callback(self.login_message),
  125. category=self.login_message_category)
  126. else:
  127. flash(self.login_message, category=self.login_message_category)
  128. return redirect(login_url(login_view, request.url))
  129. def user_loader(self, callback):
  130. '''
  131. This sets the callback for reloading a user from the session. The
  132. function you set should take a user ID (a ``unicode``) and return a
  133. user object, or ``None`` if the user does not exist.
  134. :param callback: The callback for retrieving a user object.
  135. :type callback: callable
  136. '''
  137. self.user_callback = callback
  138. return callback
  139. def header_loader(self, callback):
  140. '''
  141. This sets the callback for loading a user from a header value.
  142. The function you set should take an authentication token and
  143. return a user object, or `None` if the user does not exist.
  144. :param callback: The callback for retrieving a user object.
  145. :type callback: callable
  146. '''
  147. self.header_callback = callback
  148. return callback
  149. def request_loader(self, callback):
  150. '''
  151. This sets the callback for loading a user from a Flask request.
  152. The function you set should take Flask request object and
  153. return a user object, or `None` if the user does not exist.
  154. :param callback: The callback for retrieving a user object.
  155. :type callback: callable
  156. '''
  157. self.request_callback = callback
  158. return callback
  159. def unauthorized_handler(self, callback):
  160. '''
  161. This will set the callback for the `unauthorized` method, which among
  162. other things is used by `login_required`. It takes no arguments, and
  163. should return a response to be sent to the user instead of their
  164. normal view.
  165. :param callback: The callback for unauthorized users.
  166. :type callback: callable
  167. '''
  168. self.unauthorized_callback = callback
  169. return callback
  170. def needs_refresh_handler(self, callback):
  171. '''
  172. This will set the callback for the `needs_refresh` method, which among
  173. other things is used by `fresh_login_required`. It takes no arguments,
  174. and should return a response to be sent to the user instead of their
  175. normal view.
  176. :param callback: The callback for unauthorized users.
  177. :type callback: callable
  178. '''
  179. self.needs_refresh_callback = callback
  180. return callback
  181. def needs_refresh(self):
  182. '''
  183. This is called when the user is logged in, but they need to be
  184. reauthenticated because their session is stale. If you register a
  185. callback with `needs_refresh_handler`, then it will be called.
  186. Otherwise, it will take the following actions:
  187. - Flash :attr:`LoginManager.needs_refresh_message` to the user.
  188. - Redirect the user to :attr:`LoginManager.refresh_view`. (The page
  189. they were attempting to access will be passed in the ``next``
  190. query string variable, so you can redirect there if present
  191. instead of the homepage.)
  192. If :attr:`LoginManager.refresh_view` is not defined, then it will
  193. simply raise a HTTP 401 (Unauthorized) error instead.
  194. This should be returned from a view or before/after_request function,
  195. otherwise the redirect will have no effect.
  196. '''
  197. user_needs_refresh.send(current_app._get_current_object())
  198. if self.needs_refresh_callback:
  199. return self.needs_refresh_callback()
  200. if not self.refresh_view:
  201. abort(401)
  202. if self.localize_callback is not None:
  203. flash(self.localize_callback(self.needs_refresh_message),
  204. category=self.needs_refresh_message_category)
  205. else:
  206. flash(self.needs_refresh_message,
  207. category=self.needs_refresh_message_category)
  208. return redirect(login_url(self.refresh_view, request.url))
  209. def reload_user(self, user=None):
  210. ctx = _request_ctx_stack.top
  211. if user is None:
  212. user_id = session.get('user_id')
  213. if user_id is None:
  214. ctx.user = self.anonymous_user()
  215. else:
  216. if self.user_callback is None:
  217. raise Exception(
  218. "No user_loader has been installed for this "
  219. "LoginManager. Add one with the "
  220. "'LoginManager.user_loader' decorator.")
  221. user = self.user_callback(user_id)
  222. if user is None:
  223. ctx.user = self.anonymous_user()
  224. else:
  225. ctx.user = user
  226. else:
  227. ctx.user = user
  228. def _load_user(self):
  229. '''Loads user from session or remember_me cookie as applicable'''
  230. user_accessed.send(current_app._get_current_object())
  231. # first check SESSION_PROTECTION
  232. config = current_app.config
  233. if config.get('SESSION_PROTECTION', self.session_protection):
  234. deleted = self._session_protection()
  235. if deleted:
  236. return self.reload_user()
  237. # If a remember cookie is set, and the session is not, move the
  238. # cookie user ID to the session.
  239. #
  240. # However, the session may have been set if the user has been
  241. # logged out on this request, 'remember' would be set to clear,
  242. # so we should check for that and not restore the session.
  243. is_missing_user_id = 'user_id' not in session
  244. if is_missing_user_id:
  245. cookie_name = config.get('REMEMBER_COOKIE_NAME', COOKIE_NAME)
  246. header_name = config.get('AUTH_HEADER_NAME', AUTH_HEADER_NAME)
  247. has_cookie = (cookie_name in request.cookies and
  248. session.get('remember') != 'clear')
  249. if has_cookie:
  250. return self._load_from_cookie(request.cookies[cookie_name])
  251. elif self.request_callback:
  252. return self._load_from_request(request)
  253. elif header_name in request.headers:
  254. return self._load_from_header(request.headers[header_name])
  255. return self.reload_user()
  256. def _session_protection(self):
  257. sess = session._get_current_object()
  258. ident = _create_identifier()
  259. app = current_app._get_current_object()
  260. mode = app.config.get('SESSION_PROTECTION', self.session_protection)
  261. # if the sess is empty, it's an anonymous user or just logged out
  262. # so we can skip this
  263. if sess and ident != sess.get('_id', None):
  264. if mode == 'basic' or sess.permanent:
  265. sess['_fresh'] = False
  266. session_protected.send(app)
  267. return False
  268. elif mode == 'strong':
  269. for k in SESSION_KEYS:
  270. sess.pop(k, None)
  271. sess['remember'] = 'clear'
  272. session_protected.send(app)
  273. return True
  274. return False
  275. def _load_from_cookie(self, cookie):
  276. user_id = decode_cookie(cookie)
  277. if user_id is not None:
  278. session['user_id'] = user_id
  279. session['_fresh'] = False
  280. self.reload_user()
  281. if _request_ctx_stack.top.user is not None:
  282. app = current_app._get_current_object()
  283. user_loaded_from_cookie.send(app, user=_get_user())
  284. def _load_from_header(self, header):
  285. user = None
  286. if self.header_callback:
  287. user = self.header_callback(header)
  288. if user is not None:
  289. self.reload_user(user=user)
  290. app = current_app._get_current_object()
  291. user_loaded_from_header.send(app, user=_get_user())
  292. else:
  293. self.reload_user()
  294. def _load_from_request(self, request):
  295. user = None
  296. if self.request_callback:
  297. user = self.request_callback(request)
  298. if user is not None:
  299. self.reload_user(user=user)
  300. app = current_app._get_current_object()
  301. user_loaded_from_request.send(app, user=_get_user())
  302. else:
  303. self.reload_user()
  304. def _update_remember_cookie(self, response):
  305. # Don't modify the session unless there's something to do.
  306. if 'remember' in session:
  307. operation = session.pop('remember', None)
  308. if operation == 'set' and 'user_id' in session:
  309. self._set_cookie(response)
  310. elif operation == 'clear':
  311. self._clear_cookie(response)
  312. return response
  313. def _set_cookie(self, response):
  314. # cookie settings
  315. config = current_app.config
  316. cookie_name = config.get('REMEMBER_COOKIE_NAME', COOKIE_NAME)
  317. duration = config.get('REMEMBER_COOKIE_DURATION', COOKIE_DURATION)
  318. domain = config.get('REMEMBER_COOKIE_DOMAIN')
  319. path = config.get('REMEMBER_COOKIE_PATH', '/')
  320. secure = config.get('REMEMBER_COOKIE_SECURE', COOKIE_SECURE)
  321. httponly = config.get('REMEMBER_COOKIE_HTTPONLY', COOKIE_HTTPONLY)
  322. # prepare data
  323. data = encode_cookie(text_type(session['user_id']))
  324. try:
  325. expires = datetime.utcnow() + duration
  326. except TypeError:
  327. raise Exception('REMEMBER_COOKIE_DURATION must be a ' +
  328. 'datetime.timedelta, instead got: {0}'.format(
  329. duration))
  330. # actually set it
  331. response.set_cookie(cookie_name,
  332. value=data,
  333. expires=expires,
  334. domain=domain,
  335. path=path,
  336. secure=secure,
  337. httponly=httponly)
  338. def _clear_cookie(self, response):
  339. config = current_app.config
  340. cookie_name = config.get('REMEMBER_COOKIE_NAME', COOKIE_NAME)
  341. domain = config.get('REMEMBER_COOKIE_DOMAIN')
  342. path = config.get('REMEMBER_COOKIE_PATH', '/')
  343. response.delete_cookie(cookie_name, domain=domain, path=path)