login_manager.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. from datetime import datetime
  2. from datetime import timedelta
  3. from flask import abort
  4. from flask import current_app
  5. from flask import flash
  6. from flask import g
  7. from flask import has_app_context
  8. from flask import redirect
  9. from flask import request
  10. from flask import session
  11. from .config import AUTH_HEADER_NAME
  12. from .config import COOKIE_DURATION
  13. from .config import COOKIE_HTTPONLY
  14. from .config import COOKIE_NAME
  15. from .config import COOKIE_SAMESITE
  16. from .config import COOKIE_SECURE
  17. from .config import ID_ATTRIBUTE
  18. from .config import LOGIN_MESSAGE
  19. from .config import LOGIN_MESSAGE_CATEGORY
  20. from .config import REFRESH_MESSAGE
  21. from .config import REFRESH_MESSAGE_CATEGORY
  22. from .config import SESSION_KEYS
  23. from .config import USE_SESSION_FOR_NEXT
  24. from .mixins import AnonymousUserMixin
  25. from .signals import session_protected
  26. from .signals import user_accessed
  27. from .signals import user_loaded_from_cookie
  28. from .signals import user_loaded_from_request
  29. from .signals import user_needs_refresh
  30. from .signals import user_unauthorized
  31. from .utils import _create_identifier
  32. from .utils import _user_context_processor
  33. from .utils import decode_cookie
  34. from .utils import encode_cookie
  35. from .utils import expand_login_view
  36. from .utils import login_url as make_login_url
  37. from .utils import make_next_param
  38. class LoginManager:
  39. """This object is used to hold the settings used for logging in. Instances
  40. of :class:`LoginManager` are *not* bound to specific apps, so you can
  41. create one in the main body of your code and then bind it to your
  42. app in a factory function.
  43. """
  44. def __init__(self, app=None, add_context_processor=True):
  45. #: A class or factory function that produces an anonymous user, which
  46. #: is used when no one is logged in.
  47. self.anonymous_user = AnonymousUserMixin
  48. #: The name of the view to redirect to when the user needs to log in.
  49. #: (This can be an absolute URL as well, if your authentication
  50. #: machinery is external to your application.)
  51. self.login_view = None
  52. #: Names of views to redirect to when the user needs to log in,
  53. #: per blueprint. If the key value is set to None the value of
  54. #: :attr:`login_view` will be used instead.
  55. self.blueprint_login_views = {}
  56. #: The message to flash when a user is redirected to the login page.
  57. self.login_message = LOGIN_MESSAGE
  58. #: The message category to flash when a user is redirected to the login
  59. #: page.
  60. self.login_message_category = LOGIN_MESSAGE_CATEGORY
  61. #: The name of the view to redirect to when the user needs to
  62. #: reauthenticate.
  63. self.refresh_view = None
  64. #: The message to flash when a user is redirected to the 'needs
  65. #: refresh' page.
  66. self.needs_refresh_message = REFRESH_MESSAGE
  67. #: The message category to flash when a user is redirected to the
  68. #: 'needs refresh' page.
  69. self.needs_refresh_message_category = REFRESH_MESSAGE_CATEGORY
  70. #: The mode to use session protection in. This can be either
  71. #: ``'basic'`` (the default) or ``'strong'``, or ``None`` to disable
  72. #: it.
  73. self.session_protection = "basic"
  74. #: If present, used to translate flash messages ``self.login_message``
  75. #: and ``self.needs_refresh_message``
  76. self.localize_callback = None
  77. self.unauthorized_callback = None
  78. self.needs_refresh_callback = None
  79. self.id_attribute = ID_ATTRIBUTE
  80. self._user_callback = None
  81. self._header_callback = None
  82. self._request_callback = None
  83. self._session_identifier_generator = _create_identifier
  84. if app is not None:
  85. self.init_app(app, add_context_processor)
  86. def setup_app(self, app, add_context_processor=True): # pragma: no cover
  87. """
  88. This method has been deprecated. Please use
  89. :meth:`LoginManager.init_app` instead.
  90. """
  91. import warnings
  92. warnings.warn(
  93. "'setup_app' is deprecated and will be removed in"
  94. " Flask-Login 0.7. Use 'init_app' instead.",
  95. DeprecationWarning,
  96. stacklevel=2,
  97. )
  98. self.init_app(app, add_context_processor)
  99. def init_app(self, app, add_context_processor=True):
  100. """
  101. Configures an application. This registers an `after_request` call, and
  102. attaches this `LoginManager` to it as `app.login_manager`.
  103. :param app: The :class:`flask.Flask` object to configure.
  104. :type app: :class:`flask.Flask`
  105. :param add_context_processor: Whether to add a context processor to
  106. the app that adds a `current_user` variable to the template.
  107. Defaults to ``True``.
  108. :type add_context_processor: bool
  109. """
  110. app.login_manager = self
  111. app.after_request(self._update_remember_cookie)
  112. if add_context_processor:
  113. app.context_processor(_user_context_processor)
  114. def unauthorized(self):
  115. """
  116. This is called when the user is required to log in. If you register a
  117. callback with :meth:`LoginManager.unauthorized_handler`, then it will
  118. be called. Otherwise, it will take the following actions:
  119. - Flash :attr:`LoginManager.login_message` to the user.
  120. - If the app is using blueprints find the login view for
  121. the current blueprint using `blueprint_login_views`. If the app
  122. is not using blueprints or the login view for the current
  123. blueprint is not specified use the value of `login_view`.
  124. - Redirect the user to the login view. (The page they were
  125. attempting to access will be passed in the ``next`` query
  126. string variable, so you can redirect there if present instead
  127. of the homepage. Alternatively, it will be added to the session
  128. as ``next`` if USE_SESSION_FOR_NEXT is set.)
  129. If :attr:`LoginManager.login_view` is not defined, then it will simply
  130. raise a HTTP 401 (Unauthorized) error instead.
  131. This should be returned from a view or before/after_request function,
  132. otherwise the redirect will have no effect.
  133. """
  134. user_unauthorized.send(current_app._get_current_object())
  135. if self.unauthorized_callback:
  136. return self.unauthorized_callback()
  137. if request.blueprint in self.blueprint_login_views:
  138. login_view = self.blueprint_login_views[request.blueprint]
  139. else:
  140. login_view = self.login_view
  141. if not login_view:
  142. abort(401)
  143. if self.login_message:
  144. if self.localize_callback is not None:
  145. flash(
  146. self.localize_callback(self.login_message),
  147. category=self.login_message_category,
  148. )
  149. else:
  150. flash(self.login_message, category=self.login_message_category)
  151. config = current_app.config
  152. if config.get("USE_SESSION_FOR_NEXT", USE_SESSION_FOR_NEXT):
  153. login_url = expand_login_view(login_view)
  154. session["_id"] = self._session_identifier_generator()
  155. session["next"] = make_next_param(login_url, request.url)
  156. redirect_url = make_login_url(login_view)
  157. else:
  158. redirect_url = make_login_url(login_view, next_url=request.url)
  159. return redirect(redirect_url)
  160. def user_loader(self, callback):
  161. """
  162. This sets the callback for reloading a user from the session. The
  163. function you set should take a user ID (a ``str``) and return a
  164. user object, or ``None`` if the user does not exist.
  165. :param callback: The callback for retrieving a user object.
  166. :type callback: callable
  167. """
  168. self._user_callback = callback
  169. return self.user_callback
  170. @property
  171. def user_callback(self):
  172. """Gets the user_loader callback set by user_loader decorator."""
  173. return self._user_callback
  174. def request_loader(self, callback):
  175. """
  176. This sets the callback for loading a user from a Flask request.
  177. The function you set should take Flask request object and
  178. return a user object, or `None` if the user does not exist.
  179. :param callback: The callback for retrieving a user object.
  180. :type callback: callable
  181. """
  182. self._request_callback = callback
  183. return self.request_callback
  184. @property
  185. def request_callback(self):
  186. """Gets the request_loader callback set by request_loader decorator."""
  187. return self._request_callback
  188. def unauthorized_handler(self, callback):
  189. """
  190. This will set the callback for the `unauthorized` method, which among
  191. other things is used by `login_required`. It takes no arguments, and
  192. should return a response to be sent to the user instead of their
  193. normal view.
  194. :param callback: The callback for unauthorized users.
  195. :type callback: callable
  196. """
  197. self.unauthorized_callback = callback
  198. return callback
  199. def needs_refresh_handler(self, callback):
  200. """
  201. This will set the callback for the `needs_refresh` method, which among
  202. other things is used by `fresh_login_required`. It takes no arguments,
  203. and should return a response to be sent to the user instead of their
  204. normal view.
  205. :param callback: The callback for unauthorized users.
  206. :type callback: callable
  207. """
  208. self.needs_refresh_callback = callback
  209. return callback
  210. def needs_refresh(self):
  211. """
  212. This is called when the user is logged in, but they need to be
  213. reauthenticated because their session is stale. If you register a
  214. callback with `needs_refresh_handler`, then it will be called.
  215. Otherwise, it will take the following actions:
  216. - Flash :attr:`LoginManager.needs_refresh_message` to the user.
  217. - Redirect the user to :attr:`LoginManager.refresh_view`. (The page
  218. they were attempting to access will be passed in the ``next``
  219. query string variable, so you can redirect there if present
  220. instead of the homepage.)
  221. If :attr:`LoginManager.refresh_view` is not defined, then it will
  222. simply raise a HTTP 401 (Unauthorized) error instead.
  223. This should be returned from a view or before/after_request function,
  224. otherwise the redirect will have no effect.
  225. """
  226. user_needs_refresh.send(current_app._get_current_object())
  227. if self.needs_refresh_callback:
  228. return self.needs_refresh_callback()
  229. if not self.refresh_view:
  230. abort(401)
  231. if self.needs_refresh_message:
  232. if self.localize_callback is not None:
  233. flash(
  234. self.localize_callback(self.needs_refresh_message),
  235. category=self.needs_refresh_message_category,
  236. )
  237. else:
  238. flash(
  239. self.needs_refresh_message,
  240. category=self.needs_refresh_message_category,
  241. )
  242. config = current_app.config
  243. if config.get("USE_SESSION_FOR_NEXT", USE_SESSION_FOR_NEXT):
  244. login_url = expand_login_view(self.refresh_view)
  245. session["_id"] = self._session_identifier_generator()
  246. session["next"] = make_next_param(login_url, request.url)
  247. redirect_url = make_login_url(self.refresh_view)
  248. else:
  249. login_url = self.refresh_view
  250. redirect_url = make_login_url(login_url, next_url=request.url)
  251. return redirect(redirect_url)
  252. def header_loader(self, callback):
  253. """
  254. This function has been deprecated. Please use
  255. :meth:`LoginManager.request_loader` instead.
  256. This sets the callback for loading a user from a header value.
  257. The function you set should take an authentication token and
  258. return a user object, or `None` if the user does not exist.
  259. :param callback: The callback for retrieving a user object.
  260. :type callback: callable
  261. """
  262. import warnings
  263. warnings.warn(
  264. "'header_loader' is deprecated and will be removed in"
  265. " Flask-Login 0.7. Use 'request_loader' instead.",
  266. DeprecationWarning,
  267. stacklevel=2,
  268. )
  269. self._header_callback = callback
  270. return callback
  271. def _update_request_context_with_user(self, user=None):
  272. """Store the given user as ctx.user."""
  273. if user is None:
  274. user = self.anonymous_user()
  275. g._login_user = user
  276. def _load_user(self):
  277. """Loads user from session or remember_me cookie as applicable"""
  278. if self._user_callback is None and self._request_callback is None:
  279. raise Exception(
  280. "Missing user_loader or request_loader. Refer to "
  281. "http://flask-login.readthedocs.io/#how-it-works "
  282. "for more info."
  283. )
  284. user_accessed.send(current_app._get_current_object())
  285. # Check SESSION_PROTECTION
  286. if self._session_protection_failed():
  287. return self._update_request_context_with_user()
  288. user = None
  289. # Load user from Flask Session
  290. user_id = session.get("_user_id")
  291. if user_id is not None and self._user_callback is not None:
  292. user = self._user_callback(user_id)
  293. # Load user from Remember Me Cookie or Request Loader
  294. if user is None:
  295. config = current_app.config
  296. cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
  297. header_name = config.get("AUTH_HEADER_NAME", AUTH_HEADER_NAME)
  298. has_cookie = (
  299. cookie_name in request.cookies and session.get("_remember") != "clear"
  300. )
  301. if has_cookie:
  302. cookie = request.cookies[cookie_name]
  303. user = self._load_user_from_remember_cookie(cookie)
  304. elif self._request_callback:
  305. user = self._load_user_from_request(request)
  306. elif header_name in request.headers:
  307. header = request.headers[header_name]
  308. user = self._load_user_from_header(header)
  309. return self._update_request_context_with_user(user)
  310. def _session_protection_failed(self):
  311. sess = session._get_current_object()
  312. ident = self._session_identifier_generator()
  313. app = current_app._get_current_object()
  314. mode = app.config.get("SESSION_PROTECTION", self.session_protection)
  315. if not mode or mode not in ["basic", "strong"]:
  316. return False
  317. # if the sess is empty, it's an anonymous user or just logged out
  318. # so we can skip this
  319. if sess and ident != sess.get("_id", None):
  320. if mode == "basic" or sess.permanent:
  321. if sess.get("_fresh") is not False:
  322. sess["_fresh"] = False
  323. session_protected.send(app)
  324. return False
  325. elif mode == "strong":
  326. for k in SESSION_KEYS:
  327. sess.pop(k, None)
  328. sess["_remember"] = "clear"
  329. session_protected.send(app)
  330. return True
  331. return False
  332. def _load_user_from_remember_cookie(self, cookie):
  333. user_id = decode_cookie(cookie)
  334. if user_id is not None:
  335. session["_user_id"] = user_id
  336. session["_fresh"] = False
  337. user = None
  338. if self._user_callback:
  339. user = self._user_callback(user_id)
  340. if user is not None:
  341. app = current_app._get_current_object()
  342. user_loaded_from_cookie.send(app, user=user)
  343. return user
  344. return None
  345. def _load_user_from_header(self, header):
  346. if self._header_callback:
  347. user = self._header_callback(header)
  348. if user is not None:
  349. app = current_app._get_current_object()
  350. from .signals import _user_loaded_from_header
  351. _user_loaded_from_header.send(app, user=user)
  352. return user
  353. return None
  354. def _load_user_from_request(self, request):
  355. if self._request_callback:
  356. user = self._request_callback(request)
  357. if user is not None:
  358. app = current_app._get_current_object()
  359. user_loaded_from_request.send(app, user=user)
  360. return user
  361. return None
  362. def _update_remember_cookie(self, response):
  363. # Don't modify the session unless there's something to do.
  364. if "_remember" not in session and current_app.config.get(
  365. "REMEMBER_COOKIE_REFRESH_EACH_REQUEST"
  366. ):
  367. session["_remember"] = "set"
  368. if "_remember" in session:
  369. operation = session.pop("_remember", None)
  370. if operation == "set" and "_user_id" in session:
  371. self._set_cookie(response)
  372. elif operation == "clear":
  373. self._clear_cookie(response)
  374. return response
  375. def _set_cookie(self, response):
  376. # cookie settings
  377. config = current_app.config
  378. cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
  379. domain = config.get("REMEMBER_COOKIE_DOMAIN")
  380. path = config.get("REMEMBER_COOKIE_PATH", "/")
  381. secure = config.get("REMEMBER_COOKIE_SECURE", COOKIE_SECURE)
  382. httponly = config.get("REMEMBER_COOKIE_HTTPONLY", COOKIE_HTTPONLY)
  383. samesite = config.get("REMEMBER_COOKIE_SAMESITE", COOKIE_SAMESITE)
  384. if "_remember_seconds" in session:
  385. duration = timedelta(seconds=session["_remember_seconds"])
  386. else:
  387. duration = config.get("REMEMBER_COOKIE_DURATION", COOKIE_DURATION)
  388. # prepare data
  389. data = encode_cookie(str(session["_user_id"]))
  390. if isinstance(duration, int):
  391. duration = timedelta(seconds=duration)
  392. try:
  393. expires = datetime.utcnow() + duration
  394. except TypeError as e:
  395. raise Exception(
  396. "REMEMBER_COOKIE_DURATION must be a datetime.timedelta,"
  397. f" instead got: {duration}"
  398. ) from e
  399. # actually set it
  400. response.set_cookie(
  401. cookie_name,
  402. value=data,
  403. expires=expires,
  404. domain=domain,
  405. path=path,
  406. secure=secure,
  407. httponly=httponly,
  408. samesite=samesite,
  409. )
  410. def _clear_cookie(self, response):
  411. config = current_app.config
  412. cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
  413. domain = config.get("REMEMBER_COOKIE_DOMAIN")
  414. path = config.get("REMEMBER_COOKIE_PATH", "/")
  415. response.delete_cookie(cookie_name, domain=domain, path=path)
  416. @property
  417. def _login_disabled(self):
  418. """Legacy property, use app.config['LOGIN_DISABLED'] instead."""
  419. import warnings
  420. warnings.warn(
  421. "'_login_disabled' is deprecated and will be removed in"
  422. " Flask-Login 0.7. Use 'LOGIN_DISABLED' in 'app.config'"
  423. " instead.",
  424. DeprecationWarning,
  425. stacklevel=2,
  426. )
  427. if has_app_context():
  428. return current_app.config.get("LOGIN_DISABLED", False)
  429. return False
  430. @_login_disabled.setter
  431. def _login_disabled(self, newvalue):
  432. """Legacy property setter, use app.config['LOGIN_DISABLED'] instead."""
  433. import warnings
  434. warnings.warn(
  435. "'_login_disabled' is deprecated and will be removed in"
  436. " Flask-Login 0.7. Use 'LOGIN_DISABLED' in 'app.config'"
  437. " instead.",
  438. DeprecationWarning,
  439. stacklevel=2,
  440. )
  441. current_app.config["LOGIN_DISABLED"] = newvalue