codekingpro/portable-devtools
114k
1from datetime import datetime2from datetime import timedelta3 4from flask import abort5from flask import current_app6from flask import flash7from flask import g8from flask import has_app_context9from flask import redirect10from flask import request11from flask import session12 13from .config import AUTH_HEADER_NAME14from .config import COOKIE_DURATION15from .config import COOKIE_HTTPONLY16from .config import COOKIE_NAME17from .config import COOKIE_SAMESITE18from .config import COOKIE_SECURE19from .config import ID_ATTRIBUTE20from .config import LOGIN_MESSAGE21from .config import LOGIN_MESSAGE_CATEGORY22from .config import REFRESH_MESSAGE23from .config import REFRESH_MESSAGE_CATEGORY24from .config import SESSION_KEYS25from .config import USE_SESSION_FOR_NEXT26from .mixins import AnonymousUserMixin27from .signals import session_protected28from .signals import user_accessed29from .signals import user_loaded_from_cookie30from .signals import user_loaded_from_request31from .signals import user_needs_refresh32from .signals import user_unauthorized33from .utils import _create_identifier34from .utils import _user_context_processor35from .utils import decode_cookie36from .utils import encode_cookie37from .utils import expand_login_view38from .utils import login_url as make_login_url39from .utils import make_next_param40 41 42class LoginManager:43 """This object is used to hold the settings used for logging in. Instances44 of :class:`LoginManager` are *not* bound to specific apps, so you can45 create one in the main body of your code and then bind it to your46 app in a factory function.47 """48 49 def __init__(self, app=None, add_context_processor=True):50 #: A class or factory function that produces an anonymous user, which51 #: is used when no one is logged in.52 self.anonymous_user = AnonymousUserMixin53 54 #: The name of the view to redirect to when the user needs to log in.55 #: (This can be an absolute URL as well, if your authentication56 #: machinery is external to your application.)57 self.login_view = None58 59 #: Names of views to redirect to when the user needs to log in,60 #: per blueprint. If the key value is set to None the value of61 #: :attr:`login_view` will be used instead.62 self.blueprint_login_views = {}63 64 #: The message to flash when a user is redirected to the login page.65 self.login_message = LOGIN_MESSAGE66 67 #: The message category to flash when a user is redirected to the login68 #: page.69 self.login_message_category = LOGIN_MESSAGE_CATEGORY70 71 #: The name of the view to redirect to when the user needs to72 #: reauthenticate.73 self.refresh_view = None74 75 #: The message to flash when a user is redirected to the 'needs76 #: refresh' page.77 self.needs_refresh_message = REFRESH_MESSAGE78 79 #: The message category to flash when a user is redirected to the80 #: 'needs refresh' page.81 self.needs_refresh_message_category = REFRESH_MESSAGE_CATEGORY82 83 #: The mode to use session protection in. This can be either84 #: ``'basic'`` (the default) or ``'strong'``, or ``None`` to disable85 #: it.86 self.session_protection = "basic"87 88 #: If present, used to translate flash messages ``self.login_message``89 #: and ``self.needs_refresh_message``90 self.localize_callback = None91 92 self.unauthorized_callback = None93 94 self.needs_refresh_callback = None95 96 self.id_attribute = ID_ATTRIBUTE97 98 self._user_callback = None99 100 self._header_callback = None101 102 self._request_callback = None103 104 self._session_identifier_generator = _create_identifier105 106 if app is not None:107 self.init_app(app, add_context_processor)108 109 def setup_app(self, app, add_context_processor=True): # pragma: no cover110 """111 This method has been deprecated. Please use112 :meth:`LoginManager.init_app` instead.113 """114 import warnings115 116 warnings.warn(117 "'setup_app' is deprecated and will be removed in"118 " Flask-Login 0.7. Use 'init_app' instead.",119 DeprecationWarning,120 stacklevel=2,121 )122 self.init_app(app, add_context_processor)123 124 def init_app(self, app, add_context_processor=True):125 """126 Configures an application. This registers an `after_request` call, and127 attaches this `LoginManager` to it as `app.login_manager`.128 129 :param app: The :class:`flask.Flask` object to configure.130 :type app: :class:`flask.Flask`131 :param add_context_processor: Whether to add a context processor to132 the app that adds a `current_user` variable to the template.133 Defaults to ``True``.134 :type add_context_processor: bool135 """136 app.login_manager = self137 app.after_request(self._update_remember_cookie)138 139 if add_context_processor:140 app.context_processor(_user_context_processor)141 142 def unauthorized(self):143 """144 This is called when the user is required to log in. If you register a145 callback with :meth:`LoginManager.unauthorized_handler`, then it will146 be called. Otherwise, it will take the following actions:147 148 - Flash :attr:`LoginManager.login_message` to the user.149 150 - If the app is using blueprints find the login view for151 the current blueprint using `blueprint_login_views`. If the app152 is not using blueprints or the login view for the current153 blueprint is not specified use the value of `login_view`.154 155 - Redirect the user to the login view. (The page they were156 attempting to access will be passed in the ``next`` query157 string variable, so you can redirect there if present instead158 of the homepage. Alternatively, it will be added to the session159 as ``next`` if USE_SESSION_FOR_NEXT is set.)160 161 If :attr:`LoginManager.login_view` is not defined, then it will simply162 raise a HTTP 401 (Unauthorized) error instead.163 164 This should be returned from a view or before/after_request function,165 otherwise the redirect will have no effect.166 """167 user_unauthorized.send(current_app._get_current_object())168 169 if self.unauthorized_callback:170 return self.unauthorized_callback()171 172 if request.blueprint in self.blueprint_login_views:173 login_view = self.blueprint_login_views[request.blueprint]174 else:175 login_view = self.login_view176 177 if not login_view:178 abort(401)179 180 if self.login_message:181 if self.localize_callback is not None:182 flash(183 self.localize_callback(self.login_message),184 category=self.login_message_category,185 )186 else:187 flash(self.login_message, category=self.login_message_category)188 189 config = current_app.config190 if config.get("USE_SESSION_FOR_NEXT", USE_SESSION_FOR_NEXT):191 login_url = expand_login_view(login_view)192 session["_id"] = self._session_identifier_generator()193 session["next"] = make_next_param(login_url, request.url)194 redirect_url = make_login_url(login_view)195 else:196 redirect_url = make_login_url(login_view, next_url=request.url)197 198 return redirect(redirect_url)199 200 def user_loader(self, callback):201 """202 This sets the callback for reloading a user from the session. The203 function you set should take a user ID (a ``str``) and return a204 user object, or ``None`` if the user does not exist.205 206 :param callback: The callback for retrieving a user object.207 :type callback: callable208 """209 self._user_callback = callback210 return self.user_callback211 212 @property213 def user_callback(self):214 """Gets the user_loader callback set by user_loader decorator."""215 return self._user_callback216 217 def request_loader(self, callback):218 """219 This sets the callback for loading a user from a Flask request.220 The function you set should take Flask request object and221 return a user object, or `None` if the user does not exist.222 223 :param callback: The callback for retrieving a user object.224 :type callback: callable225 """226 self._request_callback = callback227 return self.request_callback228 229 @property230 def request_callback(self):231 """Gets the request_loader callback set by request_loader decorator."""232 return self._request_callback233 234 def unauthorized_handler(self, callback):235 """236 This will set the callback for the `unauthorized` method, which among237 other things is used by `login_required`. It takes no arguments, and238 should return a response to be sent to the user instead of their239 normal view.240 241 :param callback: The callback for unauthorized users.242 :type callback: callable243 """244 self.unauthorized_callback = callback245 return callback246 247 def needs_refresh_handler(self, callback):248 """249 This will set the callback for the `needs_refresh` method, which among250 other things is used by `fresh_login_required`. It takes no arguments,251 and should return a response to be sent to the user instead of their252 normal view.253 254 :param callback: The callback for unauthorized users.255 :type callback: callable256 """257 self.needs_refresh_callback = callback258 return callback259 260 def needs_refresh(self):261 """262 This is called when the user is logged in, but they need to be263 reauthenticated because their session is stale. If you register a264 callback with `needs_refresh_handler`, then it will be called.265 Otherwise, it will take the following actions:266 267 - Flash :attr:`LoginManager.needs_refresh_message` to the user.268 269 - Redirect the user to :attr:`LoginManager.refresh_view`. (The page270 they were attempting to access will be passed in the ``next``271 query string variable, so you can redirect there if present272 instead of the homepage.)273 274 If :attr:`LoginManager.refresh_view` is not defined, then it will275 simply raise a HTTP 401 (Unauthorized) error instead.276 277 This should be returned from a view or before/after_request function,278 otherwise the redirect will have no effect.279 """280 user_needs_refresh.send(current_app._get_current_object())281 282 if self.needs_refresh_callback:283 return self.needs_refresh_callback()284 285 if not self.refresh_view:286 abort(401)287 288 if self.needs_refresh_message:289 if self.localize_callback is not None:290 flash(291 self.localize_callback(self.needs_refresh_message),292 category=self.needs_refresh_message_category,293 )294 else:295 flash(296 self.needs_refresh_message,297 category=self.needs_refresh_message_category,298 )299 300 config = current_app.config301 if config.get("USE_SESSION_FOR_NEXT", USE_SESSION_FOR_NEXT):302 login_url = expand_login_view(self.refresh_view)303 session["_id"] = self._session_identifier_generator()304 session["next"] = make_next_param(login_url, request.url)305 redirect_url = make_login_url(self.refresh_view)306 else:307 login_url = self.refresh_view308 redirect_url = make_login_url(login_url, next_url=request.url)309 310 return redirect(redirect_url)311 312 def header_loader(self, callback):313 """314 This function has been deprecated. Please use315 :meth:`LoginManager.request_loader` instead.316 317 This sets the callback for loading a user from a header value.318 The function you set should take an authentication token and319 return a user object, or `None` if the user does not exist.320 321 :param callback: The callback for retrieving a user object.322 :type callback: callable323 """324 import warnings325 326 warnings.warn(327 "'header_loader' is deprecated and will be removed in"328 " Flask-Login 0.7. Use 'request_loader' instead.",329 DeprecationWarning,330 stacklevel=2,331 )332 self._header_callback = callback333 return callback334 335 def _update_request_context_with_user(self, user=None):336 """Store the given user as ctx.user."""337 338 if user is None:339 user = self.anonymous_user()340 341 g._login_user = user342 343 def _load_user(self):344 """Loads user from session or remember_me cookie as applicable"""345 346 if self._user_callback is None and self._request_callback is None:347 raise Exception(348 "Missing user_loader or request_loader. Refer to "349 "http://flask-login.readthedocs.io/#how-it-works "350 "for more info."351 )352 353 user_accessed.send(current_app._get_current_object())354 355 # Check SESSION_PROTECTION356 if self._session_protection_failed():357 return self._update_request_context_with_user()358 359 user = None360 361 # Load user from Flask Session362 user_id = session.get("_user_id")363 if user_id is not None and self._user_callback is not None:364 user = self._user_callback(user_id)365 366 # Load user from Remember Me Cookie or Request Loader367 if user is None:368 config = current_app.config369 cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)370 header_name = config.get("AUTH_HEADER_NAME", AUTH_HEADER_NAME)371 has_cookie = (372 cookie_name in request.cookies and session.get("_remember") != "clear"373 )374 if has_cookie:375 cookie = request.cookies[cookie_name]376 user = self._load_user_from_remember_cookie(cookie)377 elif self._request_callback:378 user = self._load_user_from_request(request)379 elif header_name in request.headers:380 header = request.headers[header_name]381 user = self._load_user_from_header(header)382 383 return self._update_request_context_with_user(user)384 385 def _session_protection_failed(self):386 sess = session._get_current_object()387 ident = self._session_identifier_generator()388 389 app = current_app._get_current_object()390 mode = app.config.get("SESSION_PROTECTION", self.session_protection)391 392 if not mode or mode not in ["basic", "strong"]:393 return False394 395 # if the sess is empty, it's an anonymous user or just logged out396 # so we can skip this397 if sess and ident != sess.get("_id", None):398 if mode == "basic" or sess.permanent:399 if sess.get("_fresh") is not False:400 sess["_fresh"] = False401 session_protected.send(app)402 return False403 elif mode == "strong":404 for k in SESSION_KEYS:405 sess.pop(k, None)406 407 sess["_remember"] = "clear"408 session_protected.send(app)409 return True410 411 return False412 413 def _load_user_from_remember_cookie(self, cookie):414 user_id = decode_cookie(cookie)415 if user_id is not None:416 session["_user_id"] = user_id417 session["_fresh"] = False418 user = None419 if self._user_callback:420 user = self._user_callback(user_id)421 if user is not None:422 app = current_app._get_current_object()423 user_loaded_from_cookie.send(app, user=user)424 return user425 return None426 427 def _load_user_from_header(self, header):428 if self._header_callback:429 user = self._header_callback(header)430 if user is not None:431 app = current_app._get_current_object()432 433 from .signals import _user_loaded_from_header434 435 _user_loaded_from_header.send(app, user=user)436 return user437 return None438 439 def _load_user_from_request(self, request):440 if self._request_callback:441 user = self._request_callback(request)442 if user is not None:443 app = current_app._get_current_object()444 user_loaded_from_request.send(app, user=user)445 return user446 return None447 448 def _update_remember_cookie(self, response):449 # Don't modify the session unless there's something to do.450 if "_remember" not in session and current_app.config.get(451 "REMEMBER_COOKIE_REFRESH_EACH_REQUEST"452 ):453 session["_remember"] = "set"454 455 if "_remember" in session:456 operation = session.pop("_remember", None)457 458 if operation == "set" and "_user_id" in session:459 self._set_cookie(response)460 elif operation == "clear":461 self._clear_cookie(response)462 463 return response464 465 def _set_cookie(self, response):466 # cookie settings467 config = current_app.config468 cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)469 domain = config.get("REMEMBER_COOKIE_DOMAIN")470 path = config.get("REMEMBER_COOKIE_PATH", "/")471 472 secure = config.get("REMEMBER_COOKIE_SECURE", COOKIE_SECURE)473 httponly = config.get("REMEMBER_COOKIE_HTTPONLY", COOKIE_HTTPONLY)474 samesite = config.get("REMEMBER_COOKIE_SAMESITE", COOKIE_SAMESITE)475 476 if "_remember_seconds" in session:477 duration = timedelta(seconds=session["_remember_seconds"])478 else:479 duration = config.get("REMEMBER_COOKIE_DURATION", COOKIE_DURATION)480 481 # prepare data482 data = encode_cookie(str(session["_user_id"]))483 484 if isinstance(duration, int):485 duration = timedelta(seconds=duration)486 487 try:488 expires = datetime.utcnow() + duration489 except TypeError as e:490 raise Exception(491 "REMEMBER_COOKIE_DURATION must be a datetime.timedelta,"492 f" instead got: {duration}"493 ) from e494 495 # actually set it496 response.set_cookie(497 cookie_name,498 value=data,499 expires=expires,500 domain=domain,501 path=path,502 secure=secure,503 httponly=httponly,504 samesite=samesite,505 )506 507 def _clear_cookie(self, response):508 config = current_app.config509 cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)510 domain = config.get("REMEMBER_COOKIE_DOMAIN")511 path = config.get("REMEMBER_COOKIE_PATH", "/")512 response.delete_cookie(cookie_name, domain=domain, path=path)513 514 @property515 def _login_disabled(self):516 """Legacy property, use app.config['LOGIN_DISABLED'] instead."""517 import warnings518 519 warnings.warn(520 "'_login_disabled' is deprecated and will be removed in"521 " Flask-Login 0.7. Use 'LOGIN_DISABLED' in 'app.config'"522 " instead.",523 DeprecationWarning,524 stacklevel=2,525 )526 527 if has_app_context():528 return current_app.config.get("LOGIN_DISABLED", False)529 return False530 531 @_login_disabled.setter532 def _login_disabled(self, newvalue):533 """Legacy property setter, use app.config['LOGIN_DISABLED'] instead."""534 import warnings535 536 warnings.warn(537 "'_login_disabled' is deprecated and will be removed in"538 " Flask-Login 0.7. Use 'LOGIN_DISABLED' in 'app.config'"539 " instead.",540 DeprecationWarning,541 stacklevel=2,542 )543 current_app.config["LOGIN_DISABLED"] = newvalue544 