????
Current Path : C:/opt/pgsql/pgAdmin 4/python/Lib/site-packages/flask_security/ |
Current File : C:/opt/pgsql/pgAdmin 4/python/Lib/site-packages/flask_security/oauth_glue.py |
""" flask_security.oauth_glue ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Class and methods to glue our login path with authlib for to support 'social' auth. :copyright: (c) 2022-2024 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from __future__ import annotations import typing as t try: # noinspection PyUnresolvedReferences from authlib.integrations.flask_client import OAuth # noinspection PyUnresolvedReferences from authlib.integrations.base_client.errors import ( OAuthError, ) except ImportError: # pragma: no cover pass from flask import abort, after_this_request, redirect, request, session from flask_login import current_user from .decorators import unauth_csrf from .oauth_provider import ( OauthCbType, FsOAuthProvider, GoogleFsOauthProvider, GitHubFsOauthProvider, ) from .proxies import _security from .utils import ( config_value as cv, do_flash, login_user, get_message, get_post_action_redirect, get_url, is_user_authenticated, json_error_response, slash_url_suffix, url_for_security, view_commit, ) if t.TYPE_CHECKING: # pragma: no cover import flask from flask.typing import ResponseValue @unauth_csrf() def oauthstart(name: str) -> ResponseValue: """View to start an oauth authentication. Name is a pre-registered oauth provider. TODO: remember me? """ assert _security.oauthglue if is_user_authenticated(current_user): # Just redirect current_user to POST_LOGIN_VIEW. # For json - return an error. # This endpoint is POST only. # This does NOT use get_post_login_redirect() so that it doesn't look at # 'next' - which can cause infinite redirect loops # (see test_common::test_authenticated_loop) if _security._want_json(request): payload = json_error_response( errors=get_message("ANONYMOUS_USER_REQUIRED")[0] ) return _security._render_json(payload, 400, None, None) else: return redirect(get_url(cv("POST_LOGIN_VIEW"))) # we never want to return here or to the redirect location. # Some providers match on entire redirect url - so we don't want to # store next there. Use session. session.pop("fs_oauth_next", None) if request.args.get("next"): session["fs_oauth_next"] = request.args.get("next") return _security.oauthglue.get_redirect(name) def oauthresponse(name: str) -> ResponseValue: """ Callback from oauth provider - response is provider specific Since this is a callback from oauth provider - there is no form, We may have stored the original 'next' in the session N.B. all responses MUST be redirects. """ assert _security.oauthglue authlib_provider = _security.oauthglue.authlib_provider(name) oauth_provider = _security.oauthglue.providers.get(name) if not authlib_provider or not oauth_provider: # this should only happen with purposeful bad API call abort(404) # TODO - redirect... to where? # This parses the Flask Request try: token = authlib_provider.authorize_access_token() except OAuthError as e: """One known way this can happen is if the session cookie 'samesite' is set to 'strict' and e.g. the first time the user goes to github and has to authorize this app and then redirect - the session cookie isn't sent - and by default that is where the state is kept. """ return oauth_provider.oauth_response_failure(e) field_name, value = oauth_provider.fetch_identity_cb( _security.oauthglue.oauth_app, token ) user = _security.datastore.find_user(**{field_name: value}) if user: after_this_request(view_commit) next_loc = session.pop("fs_oauth_next", None) response = _security.two_factor_plugins.tf_enter( user, False, "oauth", next_loc=next_loc ) if response: return response # two factor not required - login user login_user(user) if cv("REDIRECT_BEHAVIOR") == "spa": return redirect( get_url( cv("POST_OAUTH_LOGIN_VIEW"), qparams=user.get_redirect_qparams() ) ) redirect_url = get_post_action_redirect( "SECURITY_POST_LOGIN_VIEW", dict(next=next_loc) ) return redirect(redirect_url) # Seems ok to show identity - the only identity it could be is the callers # so seems no way this can be used to enumerate registered users. m, c = get_message("IDENTITY_NOT_REGISTERED", id=value) if cv("REDIRECT_BEHAVIOR") == "spa": return redirect(get_url(cv("LOGIN_ERROR_VIEW"), qparams={c: m})) do_flash(m, c) # TODO: should redirect to where we came from? return redirect(url_for_security("login")) class OAuthGlue: """ Provide the necessary glue between the Flask-Security login process and authlib oauth client code. There are some builtin providers which can be used or not - configured via :py:data:`SECURITY_OAUTH_BUILTIN_PROVIDERS`. Any other provider can be registered using :py:meth:`register_provider_ext`. See `Flask OAuth Client <https://docs.authlib.org/en/latest/client/flask.html>`_ .. versionadded:: 5.1.0 .. versionchanged:: 5.4.0 Added register_provider_ext which allows applications more control to manage new providers (such as extended error handling). """ def __init__(self, app: flask.Flask, oauthapp: OAuth | None = None): if not oauthapp: oauthapp = OAuth(app) self.oauth = oauthapp self.providers: dict[str, FsOAuthProvider] = dict() if cv("OAUTH_BUILTIN_PROVIDERS", app=app): for provider in cv("OAUTH_BUILTIN_PROVIDERS", app=app): if provider == "github": self.register_provider_ext(GitHubFsOauthProvider("github")) elif provider == "google": self.register_provider_ext(GoogleFsOauthProvider("google")) def _create_blueprint(self, app: flask.Flask, bp: flask.Blueprint) -> None: # Routes for each type of oauth provider start_url = cv("OAUTH_START_URL", app=app) response_url = cv("OAUTH_RESPONSE_URL", app=app) bp.route( start_url + slash_url_suffix(start_url, "<name>"), methods=["POST"], endpoint="oauthstart", )(oauthstart) bp.route( response_url + slash_url_suffix(response_url, "<name>"), methods=["GET"], endpoint="oauthresponse", )(oauthresponse) def get_redirect(self, name: str, **values: t.Any) -> ResponseValue: authlib_provider = self.authlib_provider(name) if not authlib_provider: return abort(404) start_uri = url_for_security( "oauthresponse", name=name, _external=True, **values ) redirect_url = authlib_provider.authorize_redirect(start_uri) return redirect_url @property def provider_names(self): return self.providers.keys() @property def oauth_app(self): return self.oauth def authlib_provider(self, name): return getattr(self.oauth, name, None) def register_provider( self, name: str, registration_info: dict[str, t.Any] | None, fetch_identity_cb: OauthCbType, ) -> None: """Add a provider to the list. :param name: Name of provider. This is used as part of the :py:data:`SECURITY_OAUTH_START_URL`. :param registration_info: Sent directly to authlib. Set this to None if you already have registered the provider directly with OAuth. :param fetch_identity_cb: This callback is called when the oauth redirect happens. It must take the response from the provider and return a tuple of <user_model_field_name, value> - which will be used to look up the user in the datastore. The provider can be registered with OAuth here or already be done by the application. If you register directly with OAuth make sure to use the same `name`. .. deprecated:: 5.4.0 Use :py:meth:`register_provider_ext` instead. """ pcls = FsOAuthProvider( name, registration_info=registration_info, fetch_identity_cb=fetch_identity_cb, ) self.register_provider_ext(pcls) def register_provider_ext(self, provider: FsOAuthProvider) -> None: """Register a provider via an instance of subclass. This is the new way - to provide more control for applications The authlib provider can be registered here (by calling Oauth) or already be done by the application. If you register directly with OAuth make sure to use the same `name` when instantiating the class. """ self.providers[provider.name] = provider if not self.authlib_provider(provider.name): self.oauth.register(provider.name, **provider.authlib_config())