From 6969985baa2f27876f659f4de925e4ff1927ebcb Mon Sep 17 00:00:00 2001 From: StevenK <> Date: May 14 2025 14:57:19 +0000 Subject: Update python-flask-oidc to version 2.3.1 / rev 6 via SR 1274786 https://build.opensuse.org/request/show/1274786 by user StevenK + anag_factory - Update to 2.3.1: * Important + Rebased the Flask OIDC API on Authlib. * Added + Make the client_secrets.json file optional when OIDC is disabled + Support Python 3.12 + Re-add redirect_to_auth_server() for compatibility with v1.x + Add a user model to flask.g with convenience properties + Add signals to hook into the login and logout process * Fixed + Include the root_path when redirecting to the custom callback route + Avoid redirect loops when the app is not mounted on the webserver root + Handle token expiration when there is no refresh_token or no token URL + Use the OIDC_CALLBACK_ROUTE with the ID provider when it is defined, instead of the default + Auto-renew tokens when they have expired (if possible), as version 1.x us --- diff --git a/.files b/.files index 52e0a16..338498e 100644 Binary files a/.files and b/.files differ diff --git a/.rev b/.rev index 3f9b54c..3d361b7 100644 --- a/.rev +++ b/.rev @@ -41,4 +41,41 @@ 1079106 + + c87b564de8e824811d4ce950d0a0e357 + 2.3.1 + + anag_factory + - Update to 2.3.1: + * Important + + Rebased the Flask OIDC API on Authlib. + * Added + + Make the client_secrets.json file optional when OIDC is disabled + + Support Python 3.12 + + Re-add redirect_to_auth_server() for compatibility with v1.x + + Add a user model to flask.g with convenience properties + + Add signals to hook into the login and logout process + * Fixed + + Include the root_path when redirecting to the custom callback route + + Avoid redirect loops when the app is not mounted on the webserver root + + Handle token expiration when there is no refresh_token or no token URL + + Use the OIDC_CALLBACK_ROUTE with the ID provider when it is defined, + instead of the default + + Auto-renew tokens when they have expired (if possible), as version 1.x + used to do + + Avoid a redirect loop on logout when the token is expired + + Don't crash if the client_secrets don't contain a userinfo_uri key + + Handle older versions of Werkzeug. + * Changed + + Ship the licenses files in the sdist + + Don't request the profile scope by default, as version 1.x used to do. + * Deprecated + + Configuration option OIDC_USERINFO_URL (and the userinfo_uri key in + client_secrets) +- Switch to pyproject macros. +- Add patch ignore-quoting-madness.patch: + * Ignore quoting madness that is different for each Python version. +- Drop patch authlib.patch, included upstream. + 1274786 + diff --git a/authlib.patch b/authlib.patch deleted file mode 100644 index a9157c8..0000000 --- a/authlib.patch +++ /dev/null @@ -1,1355 +0,0 @@ -Index: flask-oidc-1.4.0/LICENSE.txt -=================================================================== ---- flask-oidc-1.4.0.orig/LICENSE.txt -+++ flask-oidc-1.4.0/LICENSE.txt -@@ -1,4 +1,4 @@ --Copyright (c) 2014-2015, Jeremy Ehrhardt -+Copyright (c) 2014-2015, Erica Ehrhardt - Copyright (c) 2016, Patrick Uiterwijk - All rights reserved. - -Index: flask-oidc-1.4.0/MANIFEST.in -=================================================================== ---- flask-oidc-1.4.0.orig/MANIFEST.in -+++ flask-oidc-1.4.0/MANIFEST.in -@@ -1,9 +1,7 @@ --include README.rst LICENSE.rst CHANGES.rst -+include README.rst LICENSE.txt - recursive-include docs * - recursive-exclude docs *.pyc - recursive-exclude docs *.pyo --recursive-include example * --recursive-exclude example *.pyc --recursive-exclude example *.pyo -+recursive-include .tox * - prune docs/_build - prune docs/_themes/.git -Index: flask-oidc-1.4.0/README.rst -=================================================================== ---- flask-oidc-1.4.0.orig/README.rst -+++ flask-oidc-1.4.0/README.rst -@@ -20,7 +20,6 @@ This library should work with any standa - - It has been tested with: - --* `Google+ Login `_ - * `Ipsilon `_ - - -Index: flask-oidc-1.4.0/docs/index.rst -=================================================================== ---- flask-oidc-1.4.0.orig/docs/index.rst -+++ flask-oidc-1.4.0/docs/index.rst -@@ -33,7 +33,7 @@ How to use - To integrate Flask-OpenID into your application you need to create an - instance of the :class:`OpenID` object first:: - -- from flask.ext.oidc import OpenIDConnect -+ from flask_oidc import OpenIDConnect - oidc = OpenIDConnect(app) - - -Index: flask-oidc-1.4.0/flask_oidc/__init__.py -=================================================================== ---- flask-oidc-1.4.0.orig/flask_oidc/__init__.py -+++ flask-oidc-1.4.0/flask_oidc/__init__.py -@@ -1,4 +1,4 @@ --# Copyright (c) 2014-2015, Jeremy Ehrhardt -+# Copyright (c) 2014-2015, Erica Ehrhardt - # Copyright (c) 2016, Patrick Uiterwijk - # All rights reserved. - # -@@ -23,175 +23,97 @@ - # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - --from functools import wraps --import os --import json --from base64 import b64encode, urlsafe_b64encode, urlsafe_b64decode -+from authlib.integrations.flask_client import OAuth - import time --from copy import copy - import logging --from warnings import warn --import calendar -- --from six.moves.urllib.parse import urlencode --from flask import request, session, redirect, url_for, g, current_app --from oauth2client.client import flow_from_clientsecrets, OAuth2WebServerFlow,\ -- AccessTokenRefreshError, OAuth2Credentials --import httplib2 --from itsdangerous import JSONWebSignatureSerializer, BadSignature, \ -- TimedJSONWebSignatureSerializer, SignatureExpired -+import json -+from functools import wraps -+from flask import request, session, redirect, url_for, g, current_app, abort - --__all__ = ['OpenIDConnect', 'MemoryCredentials'] -+__all__ = ["OpenIDConnect"] - - logger = logging.getLogger(__name__) - -- - def _json_loads(content): - if not isinstance(content, str): - content = content.decode('utf-8') - return json.loads(content) - --class MemoryCredentials(dict): -- """ -- Non-persistent local credentials store. -- Use this if you only have one app server, and don't mind making everyone -- log in again after a restart. -- """ -- pass -- -- --class DummySecretsCache(object): -- """ -- oauth2client secrets cache -- """ -- def __init__(self, client_secrets): -- self.client_secrets = client_secrets -- -- def get(self, filename, namespace): -- return self.client_secrets -- -- --class ErrStr(str): -- """ -- This is a class to work around the time I made a terrible API decision. -- -- Basically, the validate_token() function returns a boolean True if all went -- right, but a string with an error message if something went wrong. -- -- The problem here is that this means that "if validate_token(...)" will -- always be True, even with an invalid token, and users had to do -- "if validate_token(...) is True:". -- -- This is counter-intuitive, so let's "fix" this by returning instances of -- this ErrStr class, which are basic strings except for their bool() results: -- they return False. -- """ -- def __nonzero__(self): -- """The py2 method for bool().""" -- return False -- -- def __bool__(self): -- """The py3 method for bool().""" -- return False -- -- --GOOGLE_ISSUERS = ['accounts.google.com', 'https://accounts.google.com'] -- -- --class OpenIDConnect(object): -- """ -- The core OpenID Connect client object. -- """ -- def __init__(self, app=None, credentials_store=None, http=None, time=None, -- urandom=None): -- self.credentials_store = credentials_store\ -- if credentials_store is not None\ -- else MemoryCredentials() -- -- if http is not None: -- warn('HTTP argument is deprecated and unused', DeprecationWarning) -- if time is not None: -- warn('time argument is deprecated and unused', DeprecationWarning) -- if urandom is not None: -- warn('urandom argument is deprecated and unused', -- DeprecationWarning) -- -- # By default, we do not have a custom callback -- self._custom_callback = None -- -- # get stuff from the app's config, which may override stuff set above -+class OpenIDConnect: -+ def __init__( -+ self, app=None, credentials_store=None, http=None, time=None, urandom=None -+ ): - if app is not None: - self.init_app(app) - - def init_app(self, app): -- """ -- Do setup that requires a Flask app. -- -- :param app: The application to initialize. -- :type app: Flask -- """ - secrets = self.load_secrets(app) - self.client_secrets = list(secrets.values())[0] -- secrets_cache = DummySecretsCache(secrets) - -- # Set some default configuration options -- app.config.setdefault('OIDC_SCOPES', ['openid', 'email']) -- app.config.setdefault('OIDC_GOOGLE_APPS_DOMAIN', None) -- app.config.setdefault('OIDC_ID_TOKEN_COOKIE_NAME', 'oidc_id_token') -- app.config.setdefault('OIDC_ID_TOKEN_COOKIE_PATH', '/') -- app.config.setdefault('OIDC_ID_TOKEN_COOKIE_TTL', 7 * 86400) # 7 days -- # should ONLY be turned off for local debugging -- app.config.setdefault('OIDC_COOKIE_SECURE', True) -- app.config.setdefault('OIDC_VALID_ISSUERS', -- (self.client_secrets.get('issuer') or -- GOOGLE_ISSUERS)) -- app.config.setdefault('OIDC_CLOCK_SKEW', 60) # 1 minute -- app.config.setdefault('OIDC_REQUIRE_VERIFIED_EMAIL', False) -- app.config.setdefault('OIDC_OPENID_REALM', None) -- app.config.setdefault('OIDC_USER_INFO_ENABLED', True) -- app.config.setdefault('OIDC_CALLBACK_ROUTE', '/oidc_callback') -- app.config.setdefault('OVERWRITE_REDIRECT_URI', False) -- # Configuration for resource servers -- app.config.setdefault('OIDC_RESOURCE_SERVER_ONLY', False) -- app.config.setdefault('OIDC_RESOURCE_CHECK_AUD', False) -- -- # We use client_secret_post, because that's what the Google -- # oauth2client library defaults to -- app.config.setdefault('OIDC_INTROSPECTION_AUTH_METHOD', 'client_secret_post') -- app.config.setdefault('OIDC_TOKEN_TYPE_HINT', 'access_token') -+ app.config.setdefault("OIDC_VALID_ISSUERS", self.client_secrets["issuer"]) -+ app.config.setdefault("OIDC_CLIENT_ID", self.client_secrets["client_id"]) -+ app.config.setdefault("OIDC_CLIENT_SECRET", self.client_secrets["client_secret"]) -+ app.config.setdefault("OIDC_USERINFO_URL", self.client_secrets["userinfo_uri"]) -+ app.config.setdefault("OIDC_INTROSPECTION_AUTH_METHOD", "client_secret_post") -+ app.config.setdefault("OIDC_CALLBACK_ROUTE", "/oidc_callback") - -+ app.config.setdefault("OIDC_SCOPES", "openid profile email") - if not 'openid' in app.config['OIDC_SCOPES']: - raise ValueError('The value "openid" must be in the OIDC_SCOPES') - -- # register callback route and cookie-setting decorator -- if not app.config['OIDC_RESOURCE_SERVER_ONLY']: -- app.route(app.config['OIDC_CALLBACK_ROUTE'])(self._oidc_callback) -- app.before_request(self._before_request) -- app.after_request(self._after_request) -- -- # Initialize oauth2client -- self.flow = flow_from_clientsecrets( -- app.config['OIDC_CLIENT_SECRETS'], -- scope=app.config['OIDC_SCOPES'], -- cache=secrets_cache) -- assert isinstance(self.flow, OAuth2WebServerFlow) -- -- # create signers using the Flask secret key -- self.extra_data_serializer = JSONWebSignatureSerializer( -- app.config['SECRET_KEY']) -- self.cookie_serializer = TimedJSONWebSignatureSerializer( -- app.config['SECRET_KEY']) -- -- try: -- self.credentials_store = app.config['OIDC_CREDENTIALS_STORE'] -- except KeyError: -- pass -+ #app.config.from_file(app.config["OIDC_CLIENT_SECRETS"], load=json.load) -+ app.config.setdefault( -+ "OIDC_SERVER_METADATA_URL", -+ f"{app.config['OIDC_VALID_ISSUERS']}/.well-known/openid-configuration", -+ ) -+ -+ self.oauth = OAuth(app) -+ self.oauth.register( -+ name="oidc", -+ server_metadata_url=app.config["OIDC_SERVER_METADATA_URL"], -+ client_kwargs={ -+ "scope": app.config["OIDC_SCOPES"], -+ "token_endpoint_auth_method": app.config["OIDC_INTROSPECTION_AUTH_METHOD"], -+ }, -+ ) -+ -+ app.route(app.config["OIDC_CALLBACK_ROUTE"])(self._oidc_callback) -+ app.before_request(self._before_request) -+ app.after_request(self._after_request) - - def load_secrets(self, app): - # Load client_secrets.json to pre-initialize some configuration -- return _json_loads(open(app.config['OIDC_CLIENT_SECRETS'], -- 'r').read()) -- -+ content = app.config['OIDC_CLIENT_SECRETS'] -+ if isinstance(content, dict): -+ return content -+ else: -+ return _json_loads(open(content, 'r').read()) -+ -+ def _before_request(self): -+ self.check_token_expiry() -+ -+ def _after_request(self, response): -+ return response -+ -+ def _oidc_callback(self): -+ try: -+ session["token"] = self.oauth.oidc.authorize_access_token() -+ g.oidc_token_info = session["token"] -+ except AttributeError: -+ raise -+ return redirect("/") -+ -+ def check_token_expiry(self): -+ try: -+ token = session.get("token") -+ if token: -+ if session.get("token")["expires_at"] - 60 < int(time.time()): -+ self.logout() -+ except Exception: -+ session.pop("token", None) -+ session.pop("userinfo", None) -+ raise -+ - @property - def user_loggedin(self): - """ -@@ -202,93 +124,7 @@ class OpenIDConnect(object): - - .. versionadded:: 1.0 - """ -- return g.oidc_id_token is not None -- -- def user_getfield(self, field, access_token=None): -- """ -- Request a single field of information about the user. -- -- :param field: The name of the field requested. -- :type field: str -- :returns: The value of the field. Depending on the type, this may be -- a string, list, dict, or something else. -- :rtype: object -- -- .. versionadded:: 1.0 -- """ -- info = self.user_getinfo([field], access_token) -- return info.get(field) -- -- def user_getinfo(self, fields, access_token=None): -- """ -- Request multiple fields of information about the user. -- -- :param fields: The names of the fields requested. -- :type fields: list -- :returns: The values of the current user for the fields requested. -- The keys are the field names, values are the values of the -- fields as indicated by the OpenID Provider. Note that fields -- that were not provided by the Provider are absent. -- :rtype: dict -- :raises Exception: If the user was not authenticated. Check this with -- user_loggedin. -- -- .. versionadded:: 1.0 -- """ -- if g.oidc_id_token is None and access_token is None: -- raise Exception('User was not authenticated') -- info = {} -- all_info = None -- for field in fields: -- if access_token is None and field in g.oidc_id_token: -- info[field] = g.oidc_id_token[field] -- elif current_app.config['OIDC_USER_INFO_ENABLED']: -- # This was not in the id_token. Let's get user information -- if all_info is None: -- all_info = self._retrieve_userinfo(access_token) -- if all_info is None: -- # To make sure we don't retry for every field -- all_info = {} -- if field in all_info: -- info[field] = all_info[field] -- else: -- # We didn't get this information -- pass -- return info -- -- def get_access_token(self): -- """Method to return the current requests' access_token. -- -- :returns: Access token or None -- :rtype: str -- -- .. versionadded:: 1.2 -- """ -- try: -- credentials = OAuth2Credentials.from_json( -- self.credentials_store[g.oidc_id_token['sub']]) -- return credentials.access_token -- except KeyError: -- logger.debug("Expired ID token, credentials missing", -- exc_info=True) -- return None -- -- def get_refresh_token(self): -- """Method to return the current requests' refresh_token. -- -- :returns: Access token or None -- :rtype: str -- -- .. versionadded:: 1.2 -- """ -- try: -- credentials = OAuth2Credentials.from_json( -- self.credentials_store[g.oidc_id_token['sub']]) -- return credentials.refresh_token -- except KeyError: -- logger.debug("Expired ID token, credentials missing", -- exc_info=True) -- return None -+ return session.get("token") is not None - - def _retrieve_userinfo(self, access_token=None): - """ -@@ -298,178 +134,21 @@ class OpenIDConnect(object): - :returns: The contents of the UserInfo endpoint. - :rtype: dict - """ -- if 'userinfo_uri' not in self.client_secrets: -- logger.debug('Userinfo uri not specified') -- raise AssertionError('UserInfo URI not specified') -- - # Cache the info from this request -- if '_oidc_userinfo' in g: -- return g._oidc_userinfo -- -- http = httplib2.Http() -- if access_token is None: -- try: -- credentials = OAuth2Credentials.from_json( -- self.credentials_store[g.oidc_id_token['sub']]) -- except KeyError: -- logger.debug("Expired ID token, credentials missing", -- exc_info=True) -- return None -- credentials.authorize(http) -- resp, content = http.request(self.client_secrets['userinfo_uri']) -+ token = session.get("token") -+ userinfo = session.get("userinfo") -+ if userinfo: -+ return userinfo - else: -- # We have been manually overriden with an access token -- resp, content = http.request( -- self.client_secrets['userinfo_uri'], -- "POST", -- body=urlencode({"access_token": access_token}), -- headers={'Content-Type': 'application/x-www-form-urlencoded'}) -- -- logger.debug('Retrieved user info: %s' % content) -- info = _json_loads(content) -- -- g._oidc_userinfo = info -- -- return info -- -- -- def get_cookie_id_token(self): -- """ -- .. deprecated:: 1.0 -- Use :func:`user_getinfo` instead. -- """ -- warn('You are using a deprecated function (get_cookie_id_token). ' -- 'Please reconsider using this', DeprecationWarning) -- return self._get_cookie_id_token() -- -- def _get_cookie_id_token(self): -- try: -- id_token_cookie = request.cookies.get(current_app.config[ -- 'OIDC_ID_TOKEN_COOKIE_NAME']) -- if not id_token_cookie: -- # Do not error if we were unable to get the cookie. -- # The user can debug this themselves. -- return None -- return self.cookie_serializer.loads(id_token_cookie) -- except SignatureExpired: -- logger.debug("Invalid ID token cookie", exc_info=True) -- return None -- -- def set_cookie_id_token(self, id_token): -- """ -- .. deprecated:: 1.0 -- """ -- warn('You are using a deprecated function (set_cookie_id_token). ' -- 'Please reconsider using this', DeprecationWarning) -- return self._set_cookie_id_token(id_token) -- -- def _set_cookie_id_token(self, id_token): -- """ -- Cooperates with @after_request to set a new ID token cookie. -- """ -- g.oidc_id_token = id_token -- g.oidc_id_token_dirty = True -- -- def _after_request(self, response): -- """ -- Set a new ID token cookie if the ID token has changed. -- """ -- # This means that if either the new or the old are False, we set -- # insecure cookies. -- # We don't define OIDC_ID_TOKEN_COOKIE_SECURE in init_app, because we -- # don't want people to find it easily. -- cookie_secure = (current_app.config['OIDC_COOKIE_SECURE'] and -- current_app.config.get('OIDC_ID_TOKEN_COOKIE_SECURE', -- True)) -- -- if getattr(g, 'oidc_id_token_dirty', False): -- if g.oidc_id_token: -- signed_id_token = self.cookie_serializer.dumps(g.oidc_id_token) -- response.set_cookie( -- current_app.config['OIDC_ID_TOKEN_COOKIE_NAME'], -- signed_id_token, -- secure=cookie_secure, -- httponly=True, -- max_age=current_app.config['OIDC_ID_TOKEN_COOKIE_TTL']) -- else: -- # This was a log out -- response.set_cookie( -- current_app.config['OIDC_ID_TOKEN_COOKIE_NAME'], -- '', -- path=current_app.config['OIDC_ID_TOKEN_COOKIE_PATH'], -- secure=cookie_secure, -- httponly=True, -- expires=0) -- return response -- -- def _before_request(self): -- g.oidc_id_token = None -- self.authenticate_or_redirect() -- -- def authenticate_or_redirect(self): -- """ -- Helper function suitable for @app.before_request and @check. -- Sets g.oidc_id_token to the ID token if the user has successfully -- authenticated, else returns a redirect object so they can go try -- to authenticate. -- -- :returns: A redirect object, or None if the user is logged in. -- :rtype: Redirect -- -- .. deprecated:: 1.0 -- Use :func:`require_login` instead. -- """ -- # the auth callback and error pages don't need user to be authenticated -- if request.endpoint in frozenset(['_oidc_callback', '_oidc_error']): -- return None -- -- # retrieve signed ID token cookie -- id_token = self._get_cookie_id_token() -- if id_token is None: -- return self.redirect_to_auth_server(request.url) -- -- # ID token expired -- # when Google is the IdP, this happens after one hour -- if time.time() >= id_token['exp']: -- # get credentials from store -- try: -- credentials = OAuth2Credentials.from_json( -- self.credentials_store[id_token['sub']]) -- except KeyError: -- logger.debug("Expired ID token, credentials missing", -- exc_info=True) -- return self.redirect_to_auth_server(request.url) -- -- # refresh and store credentials - try: -- credentials.refresh(httplib2.Http()) -- if credentials.id_token: -- id_token = credentials.id_token -- else: -- # It is not guaranteed that we will get a new ID Token on -- # refresh, so if we do not, let's just update the id token -- # expiry field and reuse the existing ID Token. -- if credentials.token_expiry is None: -- logger.debug('Expired ID token, no new expiry. Falling' -- ' back to assuming 1 hour') -- id_token['exp'] = time.time() + 3600 -- else: -- id_token['exp'] = calendar.timegm( -- credentials.token_expiry.timetuple()) -- self.credentials_store[id_token['sub']] = credentials.to_json() -- self._set_cookie_id_token(id_token) -- except AccessTokenRefreshError: -- # Can't refresh. Wipe credentials and redirect user to IdP -- # for re-authentication. -- logger.debug("Expired ID token, can't refresh credentials", -- exc_info=True) -- del self.credentials_store[id_token['sub']] -- return self.redirect_to_auth_server(request.url) -- -- # make ID token available to views -- g.oidc_id_token = id_token -- -- return None -+ resp = self.oauth.oidc.get( -+ current_app.config["OIDC_USERINFO_URL"], token=token -+ ) -+ userinfo = resp.json() -+ session["userinfo"] = userinfo -+ return userinfo -+ except Exception: -+ raise - - def require_login(self, view_func): - """ -@@ -480,245 +159,18 @@ class OpenIDConnect(object): - .. versionadded:: 1.0 - This was :func:`check` before. - """ -+ - @wraps(view_func) - def decorated(*args, **kwargs): -- if g.oidc_id_token is None: -- return self.redirect_to_auth_server(request.url) -+ if session.get("token") is None: -+ redirect_uri = url_for( -+ "_oidc_callback", _scheme="https", _external=True -+ ) -+ return self.oauth.oidc.authorize_redirect(redirect_uri) - return view_func(*args, **kwargs) -- return decorated -- # Backwards compatibility -- check = require_login -- """ -- .. deprecated:: 1.0 -- Use :func:`require_login` instead. -- """ -- -- def flow_for_request(self): -- """ -- .. deprecated:: 1.0 -- Use :func:`require_login` instead. -- """ -- warn('You are using a deprecated function (flow_for_request). ' -- 'Please reconsider using this', DeprecationWarning) -- return self._flow_for_request() -- -- def _flow_for_request(self): -- """ -- Build a flow with the correct absolute callback URL for this request. -- :return: -- """ -- flow = copy(self.flow) -- redirect_uri = current_app.config['OVERWRITE_REDIRECT_URI'] -- if not redirect_uri: -- flow.redirect_uri = url_for('_oidc_callback', _external=True) -- else: -- flow.redirect_uri = redirect_uri -- return flow -- -- def redirect_to_auth_server(self, destination=None, customstate=None): -- """ -- Set a CSRF token in the session, and redirect to the IdP. - -- :param destination: The page that the user was going to, -- before we noticed they weren't logged in. -- :type destination: Url to return the client to if a custom handler is -- not used. Not available with custom callback. -- :param customstate: The custom data passed via the ODIC state. -- Note that this only works with a custom_callback, and this will -- ignore destination. -- :type customstate: Anything that can be serialized -- :returns: A redirect response to start the login process. -- :rtype: Flask Response -- -- .. deprecated:: 1.0 -- Use :func:`require_login` instead. -- """ -- if not self._custom_callback and customstate: -- raise ValueError('Custom State is only avilable with a custom ' -- 'handler') -- if 'oidc_csrf_token' not in session: -- csrf_token = urlsafe_b64encode(os.urandom(24)).decode('utf-8') -- session['oidc_csrf_token'] = csrf_token -- state = { -- 'csrf_token': session['oidc_csrf_token'], -- } -- statefield = 'destination' -- statevalue = destination -- if customstate is not None: -- statefield = 'custom' -- statevalue = customstate -- state[statefield] = self.extra_data_serializer.dumps( -- statevalue).decode('utf-8') -- -- extra_params = { -- 'state': urlsafe_b64encode(json.dumps(state).encode('utf-8')), -- } -- if current_app.config['OIDC_GOOGLE_APPS_DOMAIN']: -- extra_params['hd'] = current_app.config['OIDC_GOOGLE_APPS_DOMAIN'] -- if current_app.config['OIDC_OPENID_REALM']: -- extra_params['openid.realm'] = current_app.config[ -- 'OIDC_OPENID_REALM'] -- -- flow = self._flow_for_request() -- auth_url = '{url}&{extra_params}'.format( -- url=flow.step1_get_authorize_url(), -- extra_params=urlencode(extra_params)) -- # if the user has an ID token, it's invalid, or we wouldn't be here -- self._set_cookie_id_token(None) -- return redirect(auth_url) -- -- def _is_id_token_valid(self, id_token): -- """ -- Check if `id_token` is a current ID token for this application, -- was issued by the Apps domain we expected, -- and that the email address has been verified. -- -- @see: http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation -- """ -- if not id_token: -- return False -- -- # step 2: check issuer -- if id_token['iss'] not in current_app.config['OIDC_VALID_ISSUERS']: -- logger.error('id_token issued by non-trusted issuer: %s' -- % id_token['iss']) -- return False -- -- if isinstance(id_token['aud'], list): -- # step 3 for audience list -- if self.flow.client_id not in id_token['aud']: -- logger.error('We are not a valid audience') -- return False -- # step 4 -- if 'azp' not in id_token: -- logger.error('Multiple audiences and not authorized party') -- return False -- else: -- # step 3 for single audience -- if id_token['aud'] != self.flow.client_id: -- logger.error('We are not the audience') -- return False -- -- # step 5 -- if 'azp' in id_token and id_token['azp'] != self.flow.client_id: -- logger.error('Authorized Party is not us') -- return False -- -- # step 6-8: TLS checked -- -- # step 9: check exp -- if int(time.time()) >= int(id_token['exp']): -- logger.error('Token has expired') -- return False -- -- # step 10: check iat -- if id_token['iat'] < (time.time() - -- current_app.config['OIDC_CLOCK_SKEW']): -- logger.error('Token issued in the past') -- return False -- -- # (not required if using HTTPS?) step 11: check nonce -- -- # step 12-13: not requested acr or auth_time, so not needed to test -- -- # additional steps specific to our usage -- if current_app.config['OIDC_GOOGLE_APPS_DOMAIN'] and \ -- id_token.get('hd') != current_app.config[ -- 'OIDC_GOOGLE_APPS_DOMAIN']: -- logger.error('Invalid google apps domain') -- return False -- -- if not id_token.get('email_verified', False) and \ -- current_app.config['OIDC_REQUIRE_VERIFIED_EMAIL']: -- logger.error('Email not verified') -- return False -- -- return True -- -- WRONG_GOOGLE_APPS_DOMAIN = 'WRONG_GOOGLE_APPS_DOMAIN' -- -- def custom_callback(self, view_func): -- """ -- Wrapper function to use a custom callback. -- The custom OIDC callback will get the custom state field passed in with -- redirect_to_auth_server. -- """ -- @wraps(view_func) -- def decorated(*args, **kwargs): -- plainreturn, data = self._process_callback('custom') -- if plainreturn: -- return data -- else: -- return view_func(data, *args, **kwargs) -- self._custom_callback = decorated - return decorated - -- def _oidc_callback(self): -- plainreturn, data = self._process_callback('destination') -- if plainreturn: -- return data -- else: -- return redirect(data) -- -- def _process_callback(self, statefield): -- """ -- Exchange the auth code for actual credentials, -- then redirect to the originally requested page. -- """ -- # retrieve session and callback variables -- try: -- session_csrf_token = session.get('oidc_csrf_token') -- -- state = _json_loads(urlsafe_b64decode(request.args['state'].encode('utf-8'))) -- csrf_token = state['csrf_token'] -- -- code = request.args['code'] -- except (KeyError, ValueError): -- logger.debug("Can't retrieve CSRF token, state, or code", -- exc_info=True) -- return True, self._oidc_error() -- -- # check callback CSRF token passed to IdP -- # against session CSRF token held by user -- if csrf_token != session_csrf_token: -- logger.debug("CSRF token mismatch") -- return True, self._oidc_error() -- -- # make a request to IdP to exchange the auth code for OAuth credentials -- flow = self._flow_for_request() -- credentials = flow.step2_exchange(code) -- id_token = credentials.id_token -- if not self._is_id_token_valid(id_token): -- logger.debug("Invalid ID token") -- if id_token.get('hd') != current_app.config[ -- 'OIDC_GOOGLE_APPS_DOMAIN']: -- return True, self._oidc_error( -- "You must log in with an account from the {0} domain." -- .format(current_app.config['OIDC_GOOGLE_APPS_DOMAIN']), -- self.WRONG_GOOGLE_APPS_DOMAIN) -- return True, self._oidc_error() -- -- # store credentials by subject -- # when Google is the IdP, the subject is their G+ account number -- self.credentials_store[id_token['sub']] = credentials.to_json() -- -- # Retrieve the extra statefield data -- try: -- response = self.extra_data_serializer.loads(state[statefield]) -- except BadSignature: -- logger.error('State field was invalid') -- return True, self._oidc_error() -- -- # set a persistent signed cookie containing the ID token -- # and redirect to the final destination -- self._set_cookie_id_token(id_token) -- return False, response -- -- def _oidc_error(self, message='Not Authorized', code=None): -- return (message, 401, { -- 'Content-Type': 'text/plain', -- }) -- - def logout(self): - """ - Request the browser to please forget the cookie we set, to clear the -@@ -733,164 +185,6 @@ class OpenIDConnect(object): - - .. versionadded:: 1.0 - """ -- # TODO: Add single logout -- self._set_cookie_id_token(None) -- -- # Below here is for resource servers to validate tokens -- def validate_token(self, token, scopes_required=None): -- """ -- This function can be used to validate tokens. -- -- Note that this only works if a token introspection url is configured, -- as that URL will be queried for the validity and scopes of a token. -- -- :param scopes_required: List of scopes that are required to be -- granted by the token before returning True. -- :type scopes_required: list -- -- :returns: True if the token was valid and contained the required -- scopes. An ErrStr (subclass of string for which bool() is False) if -- an error occured. -- :rtype: Boolean or String -- -- .. versionadded:: 1.1 -- """ -- valid = self._validate_token(token, scopes_required) -- if valid is True: -- return True -- else: -- return ErrStr(valid) -- -- def _validate_token(self, token, scopes_required=None): -- """The actual implementation of validate_token.""" -- if scopes_required is None: -- scopes_required = [] -- scopes_required = set(scopes_required) -- -- token_info = None -- valid_token = False -- has_required_scopes = False -- if token: -- try: -- token_info = self._get_token_info(token) -- except Exception as ex: -- token_info = {'active': False} -- logger.error('ERROR: Unable to get token info') -- logger.error(str(ex)) -- -- valid_token = token_info.get('active', False) -- -- if 'aud' in token_info and \ -- current_app.config['OIDC_RESOURCE_CHECK_AUD']: -- valid_audience = False -- aud = token_info['aud'] -- clid = self.client_secrets['client_id'] -- if isinstance(aud, list): -- valid_audience = clid in aud -- else: -- valid_audience = clid == aud -- -- if not valid_audience: -- logger.error('Refused token because of invalid ' -- 'audience') -- valid_token = False -- -- if valid_token: -- token_scopes = token_info.get('scope', '').split(' ') -- else: -- token_scopes = [] -- has_required_scopes = scopes_required.issubset( -- set(token_scopes)) -- -- if not has_required_scopes: -- logger.debug('Token missed required scopes') -- -- if (valid_token and has_required_scopes): -- g.oidc_token_info = token_info -- return True -- -- if not valid_token: -- return 'Token required but invalid' -- elif not has_required_scopes: -- return 'Token does not have required scopes' -- else: -- return 'Something went wrong checking your token' -- -- def accept_token(self, require_token=False, scopes_required=None, -- render_errors=True): -- """ -- Use this to decorate view functions that should accept OAuth2 tokens, -- this will most likely apply to API functions. -- -- Tokens are accepted as part of the query URL (access_token value) or -- a POST form value (access_token). -- -- Note that this only works if a token introspection url is configured, -- as that URL will be queried for the validity and scopes of a token. -- -- :param require_token: Whether a token is required for the current -- function. If this is True, we will abort the request if there -- was no token provided. -- :type require_token: bool -- :param scopes_required: List of scopes that are required to be -- granted by the token before being allowed to call the protected -- function. -- :type scopes_required: list -- :param render_errors: Whether or not to eagerly render error objects -- as JSON API responses. Set to False to pass the error object back -- unmodified for later rendering. -- :type render_errors: callback(obj) or None -- -- .. versionadded:: 1.0 -- """ -- -- def wrapper(view_func): -- @wraps(view_func) -- def decorated(*args, **kwargs): -- token = None -- if 'Authorization' in request.headers and request.headers['Authorization'].startswith('Bearer '): -- token = request.headers['Authorization'].split(None,1)[1].strip() -- if 'access_token' in request.form: -- token = request.form['access_token'] -- elif 'access_token' in request.args: -- token = request.args['access_token'] -- -- validity = self.validate_token(token, scopes_required) -- if (validity is True) or (not require_token): -- return view_func(*args, **kwargs) -- else: -- response_body = {'error': 'invalid_token', -- 'error_description': validity} -- if render_errors: -- response_body = json.dumps(response_body) -- return response_body, 401, {'WWW-Authenticate': 'Bearer'} -- -- return decorated -- return wrapper -- -- def _get_token_info(self, token): -- # We hardcode to use client_secret_post, because that's what the Google -- # oauth2client library defaults to -- request = {'token': token} -- headers = {'Content-type': 'application/x-www-form-urlencoded'} -- -- hint = current_app.config['OIDC_TOKEN_TYPE_HINT'] -- if hint != 'none': -- request['token_type_hint'] = hint -- -- auth_method = current_app.config['OIDC_INTROSPECTION_AUTH_METHOD'] -- if (auth_method == 'client_secret_basic'): -- basic_auth_string = '%s:%s' % (self.client_secrets['client_id'], self.client_secrets['client_secret']) -- basic_auth_bytes = bytearray(basic_auth_string, 'utf-8') -- headers['Authorization'] = 'Basic %s' % b64encode(basic_auth_bytes) -- elif (auth_method == 'bearer'): -- headers['Authorization'] = 'Bearer %s' % token -- elif (auth_method == 'client_secret_post'): -- request['client_id'] = self.client_secrets['client_id'] -- request['client_secret'] = self.client_secrets['client_secret'] -- -- resp, content = httplib2.Http().request( -- self.client_secrets['token_introspection_uri'], 'POST', -- urlencode(request), headers=headers) -- # TODO: Cache this reply -- return _json_loads(content) -+ session.pop("token", None) -+ session.pop("userinfo", None) -+ return redirect("/") -Index: flask-oidc-1.4.0/flask_oidc/discovery.py -=================================================================== ---- flask-oidc-1.4.0.orig/flask_oidc/discovery.py -+++ /dev/null -@@ -1,44 +0,0 @@ --# Copyright (c) 2016, Patrick Uiterwijk --# All rights reserved. --# --# Redistribution and use in source and binary forms, with or without --# modification, are permitted provided that the following conditions are met: --# --# * Redistributions of source code must retain the above copyright notice, this --# list of conditions and the following disclaimer. --# --# * Redistributions in binary form must reproduce the above copyright notice, --# this list of conditions and the following disclaimer in the documentation --# and/or other materials provided with the distribution. --# --# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" --# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE --# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE --# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE --# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL --# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR --# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER --# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, --# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE --# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -- --import httplib2 -- --from flask_oidc import _json_loads -- -- --# OpenID Connect Discovery 1.0 --def discover_OP_information(OP_uri): -- """ -- Discovers information about the provided OpenID Provider. -- -- :param OP_uri: The base URI of the Provider information is requested for. -- :type OP_uri: str -- :returns: The contents of the Provider metadata document. -- :rtype: dict -- -- .. versionadded:: 1.0 -- """ -- _, content = httplib2.Http().request( -- '%s/.well-known/openid-configuration' % OP_uri) -- return _json_loads(content) -Index: flask-oidc-1.4.0/flask_oidc/registration.py -=================================================================== ---- flask-oidc-1.4.0.orig/flask_oidc/registration.py -+++ /dev/null -@@ -1,144 +0,0 @@ --# Copyright (c) 2016, Patrick Uiterwijk --# All rights reserved. --# --# Redistribution and use in source and binary forms, with or without --# modification, are permitted provided that the following conditions are met: --# --# * Redistributions of source code must retain the above copyright notice, this --# list of conditions and the following disclaimer. --# --# * Redistributions in binary form must reproduce the above copyright notice, --# this list of conditions and the following disclaimer in the documentation --# and/or other materials provided with the distribution. --# --# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" --# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE --# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE --# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE --# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL --# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR --# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER --# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, --# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE --# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -- --import json --import httplib2 -- --from flask_oidc import _json_loads -- -- --def check_redirect_uris(uris, client_type=None): -- """ -- This function checks all return uris provided and tries to deduce -- as what type of client we should register. -- -- :param uris: The redirect URIs to check. -- :type uris: list -- :param client_type: An indicator of which client type you are expecting -- to be used. If this does not match the deduced type, an error will -- be raised. -- :type client_type: str -- :returns: The deduced client type. -- :rtype: str -- :raises ValueError: An error occured while checking the redirect uris. -- -- .. versionadded:: 1.0 -- """ -- if client_type not in [None, 'native', 'web']: -- raise ValueError('Invalid client type indicator used') -- -- if not isinstance(uris, list): -- raise ValueError('uris needs to be a list of strings') -- -- if len(uris) < 1: -- raise ValueError('At least one return URI needs to be provided') -- -- for uri in uris: -- if uri.startswith('https://'): -- if client_type == 'native': -- raise ValueError('https url with native client') -- client_type = 'web' -- elif uri.startswith('http://localhost'): -- if client_type == 'web': -- raise ValueError('http://localhost url with web client') -- client_type = 'native' -- else: -- if (uri.startswith('http://') and -- not uri.startswith('http://localhost')): -- raise ValueError('http:// url with non-localhost is illegal') -- else: -- raise ValueError('Invalid uri provided: %s' % uri) -- -- return client_type -- -- --class RegistrationError(Exception): -- """ -- This class is used to pass errors reported by the OpenID Provider during -- dynamic registration. -- -- .. versionadded:: 1.0 -- """ -- errorcode = None -- errordescription = None -- -- def __init__(self, response): -- self.errorcode = response['error'] -- self.errordescription = response.get('error_description') -- -- --# OpenID Connect Dynamic Client Registration 1.0 --def register_client(provider_info, redirect_uris): -- """ -- This function registers a new client with the specified OpenID Provider, -- and then returns the regitered client ID and other information. -- -- :param provider_info: The contents of the discovery endpoint as -- specified by the OpenID Connect Discovery 1.0 specifications. -- :type provider_info: dict -- :param redirect_uris: The redirect URIs the application wants to -- register. -- :type redirect_uris: list -- :returns: An object containing the information needed to configure the -- actual client code to communicate with the OpenID Provider. -- :rtype: dict -- :raises ValueError: The same error as used by check_redirect_uris. -- :raises RegistrationError: Indicates an error was returned by the OpenID -- Provider during registration. -- -- .. versionadded:: 1.0 -- """ -- client_type = check_redirect_uris(redirect_uris) -- -- submit_info = {'redirect_uris': redirect_uris, -- 'application_type': client_type, -- 'token_endpoint_auth_method': 'client_secret_post'} -- -- headers = {'Content-type': 'application/json'} -- -- resp, content = httplib2.Http().request( -- provider_info['registration_endpoint'], 'POST', -- json.dumps(submit_info), headers=headers) -- -- if int(resp['status']) >= 400: -- raise Exception('Error: the server returned HTTP ' + resp['status']) -- -- client_info = _json_loads(content) -- -- if 'error' in client_info: -- raise Exception('Error occured during registration: %s (%s)' -- % (client_info['error'], -- client_info.get('error_description'))) -- -- json_file = {'web': { -- 'client_id': client_info['client_id'], -- 'client_secret': client_info['client_secret'], -- 'auth_uri': provider_info['authorization_endpoint'], -- 'token_uri': provider_info['token_endpoint'], -- 'userinfo_uri': provider_info['userinfo_endpoint'], -- 'redirect_uris': redirect_uris, -- 'issuer': provider_info['issuer'], -- }} -- -- return json_file -Index: flask-oidc-1.4.0/flask_oidc/registration_util.py -=================================================================== ---- flask-oidc-1.4.0.orig/flask_oidc/registration_util.py -+++ /dev/null -@@ -1,97 +0,0 @@ --# Copyright (c) 2016, Patrick Uiterwijk --# All rights reserved. --# --# Redistribution and use in source and binary forms, with or without --# modification, are permitted provided that the following conditions are met: --# --# * Redistributions of source code must retain the above copyright notice, this --# list of conditions and the following disclaimer. --# --# * Redistributions in binary form must reproduce the above copyright notice, --# this list of conditions and the following disclaimer in the documentation --# and/or other materials provided with the distribution. --# --# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" --# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE --# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE --# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE --# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL --# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR --# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER --# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, --# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE --# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -- --import argparse --import json --import logging --import os.path --import sys -- --from flask_oidc import discovery --from flask_oidc import registration -- --logging.basicConfig() --LOG = logging.getLogger("oidc-register") -- -- --def _parse_args(): -- parser = argparse.ArgumentParser(description='Help register an OpenID ' -- 'Client') -- parser.add_argument('provider_url', -- help='Base URL to the provider to register at') -- parser.add_argument('application_url', -- help='Base URL to the application') -- parser.add_argument('--token-introspection-uri', -- help='Token introspection URI') -- parser.add_argument('--output-file', default='client_secrets.json', -- help='File to write client info to') -- parser.add_argument('--debug', action='store_true') -- return parser.parse_args() -- -- --def main(): -- args = _parse_args() -- if os.path.exists(args.output_file): -- print('Output file exists. Please provide other filename') -- return 1 -- -- if args.debug: -- LOG.setLevel(logging.DEBUG) -- -- redirect_uris = ['%s/oidc_callback' % args.application_url] -- registration.check_redirect_uris(redirect_uris) -- try: -- OP = discovery.discover_OP_information(args.provider_url) -- except Exception as ex: -- print('Error discovering OP information') -- if args.debug: -- print(ex) -- LOG.exception("Error caught when discovering OP information:") -- return 1 -- if args.debug: -- print('Provider info: %s' % OP) -- try: -- reg_info = registration.register_client(OP, redirect_uris) -- except Exception as ex: -- print('Error registering client') -- if args.debug: -- print(ex) -- LOG.exception("Error caught when registering the client:") -- return 1 -- if args.debug: -- print('Registration info: %s' % reg_info) -- -- if args.token_introspection_uri: -- reg_info['web']['token_introspection_uri'] = \ -- args.token_introspection_uri -- -- with open(args.output_file, 'w') as outfile: -- outfile.write(json.dumps(reg_info)) -- print('Client information file written') -- -- --if __name__ == '__main__': -- retval = main() -- if retval: -- sys.exit(retval) -Index: flask-oidc-1.4.0/pyproject.toml -=================================================================== ---- /dev/null -+++ flask-oidc-1.4.0/pyproject.toml -@@ -0,0 +1,3 @@ -+[build-system] -+requires = ["setuptools", "wheel"] -+build-backend = "setuptools.build_meta" -Index: flask-oidc-1.4.0/requirements.txt -=================================================================== ---- flask-oidc-1.4.0.orig/requirements.txt -+++ flask-oidc-1.4.0/requirements.txt -@@ -1,4 +1,3 @@ - Flask --itsdangerous --oauth2client --six -+Authlib -+requests -Index: flask-oidc-1.4.0/setup.py -=================================================================== ---- flask-oidc-1.4.0.orig/setup.py -+++ flask-oidc-1.4.0/setup.py -@@ -25,17 +25,16 @@ setup( - description='OpenID Connect extension for Flask', - long_description=readme, - url='https://github.com/puiterwijk/flask-oidc', -- author='Jeremy Ehrhardt, Patrick Uiterwijk', -- author_email='jeremy@bat-country.us, patrick@puiterwijk.org', -- version='1.4.0', -+ author='Erica Ehrhardt, Patrick Uiterwijk', -+ author_email='patrick@puiterwijk.org', -+ version='2.0.0', - packages=[ - 'flask_oidc', - ], - install_requires=[ - 'Flask', -- 'itsdangerous', -- 'oauth2client', -- 'six', -+ 'Authlib', -+ 'requests', - ], - tests_require=['nose', 'mock'], - entry_points={ diff --git a/flask-oidc-1.4.0.tar.gz b/flask-oidc-1.4.0.tar.gz deleted file mode 120000 index 88fe712..0000000 --- a/flask-oidc-1.4.0.tar.gz +++ /dev/null @@ -1 +0,0 @@ -/ipfs/bafkreiamcikrcooupjlc4hc24ib7xhn4owp6or2myapaeof67auozzmpjy \ No newline at end of file diff --git a/flask_oidc-2.3.1.tar.gz b/flask_oidc-2.3.1.tar.gz new file mode 120000 index 0000000..6dbc129 --- /dev/null +++ b/flask_oidc-2.3.1.tar.gz @@ -0,0 +1 @@ +/ipfs/bafkreihtb5sfjgquuyrmxvbhemxkknoilm5rym2xnrjhblqokmodd3aara \ No newline at end of file diff --git a/ignore-quoting-madness.patch b/ignore-quoting-madness.patch new file mode 100644 index 0000000..1a83ade --- /dev/null +++ b/ignore-quoting-madness.patch @@ -0,0 +1,20 @@ +Index: flask_oidc-2.3.1/tests/test_flask_oidc.py +=================================================================== +--- flask_oidc-2.3.1.orig/tests/test_flask_oidc.py ++++ flask_oidc-2.3.1/tests/test_flask_oidc.py +@@ -299,10 +299,11 @@ def test_accept_token(client, mocked_res + def test_accept_token_no_token(client, mocked_responses): + resp = client.get("/need-token") + assert resp.status_code == 401 +- assert resp.json == { +- "error": "missing_authorization", +- "error_description": 'Missing "Authorization" in headers.', +- } ++ assert resp.json["error"] == "missing_authorization" ++ assert resp.json["error_description"] in ( ++ "Missing 'Authorization' in headers.", ++ 'Missing "Authorization" in headers.', ++ ) + + + def test_accept_token_invalid(client, mocked_responses): diff --git a/python-flask-oidc.changes b/python-flask-oidc.changes index b51c766..909a071 100644 --- a/python-flask-oidc.changes +++ b/python-flask-oidc.changes @@ -1,4 +1,39 @@ ------------------------------------------------------------------- +Tue May 6 06:20:57 UTC 2025 - Steve Kowalik + +- Update to 2.3.1: + * Important + + Rebased the Flask OIDC API on Authlib. + * Added + + Make the client_secrets.json file optional when OIDC is disabled + + Support Python 3.12 + + Re-add redirect_to_auth_server() for compatibility with v1.x + + Add a user model to flask.g with convenience properties + + Add signals to hook into the login and logout process + * Fixed + + Include the root_path when redirecting to the custom callback route + + Avoid redirect loops when the app is not mounted on the webserver root + + Handle token expiration when there is no refresh_token or no token URL + + Use the OIDC_CALLBACK_ROUTE with the ID provider when it is defined, + instead of the default + + Auto-renew tokens when they have expired (if possible), as version 1.x + used to do + + Avoid a redirect loop on logout when the token is expired + + Don't crash if the client_secrets don't contain a userinfo_uri key + + Handle older versions of Werkzeug. + * Changed + + Ship the licenses files in the sdist + + Don't request the profile scope by default, as version 1.x used to do. + * Deprecated + + Configuration option OIDC_USERINFO_URL (and the userinfo_uri key in + client_secrets) +- Switch to pyproject macros. +- Add patch ignore-quoting-madness.patch: + * Ignore quoting madness that is different for each Python version. +- Drop patch authlib.patch, included upstream. +- Run the testsuite, tests are included. + +------------------------------------------------------------------- Thu Apr 13 09:35:38 UTC 2023 - pgajdos@suse.com - authlib.patch removes dependency on six by the way diff --git a/python-flask-oidc.spec b/python-flask-oidc.spec index 1cafa00..32d1fdf 100644 --- a/python-flask-oidc.spec +++ b/python-flask-oidc.spec @@ -1,7 +1,7 @@ # -# spec file +# spec file for package python-flask-oidc # -# Copyright (c) 2023 SUSE LLC +# Copyright (c) 2025 SUSE LLC # Copyright (c) 2020 Neal Gompa . # # All modifications and additions to the file contributed by third parties @@ -19,26 +19,28 @@ %global pypi_name flask-oidc Name: python-%{pypi_name} -Version: 1.4.0 +Version: 2.3.1 Release: 0 Summary: OpenID Connect support for Flask License: BSD-2-Clause -Group: Development/Libraries/Python URL: https://github.com/fedora-infra/%{pypi_name} -Source0: https://pypi.io/packages/source/f/%{pypi_name}/%{pypi_name}-%{version}.tar.gz -# PATCH-FIX-OPENSUSE authlib.patch -- gh#puiterwijk/flask-oidc#138 -Patch0: authlib.patch -BuildRequires: %{python_module Authlib} +Source0: https://pypi.io/packages/source/f/%{pypi_name}/flask_oidc-%{version}.tar.gz +# PATCH-FIX-OPENSUSE Don't fail a test due to quoting +Patch0: ignore-quoting-madness.patch +BuildRequires: %{python_module Authlib >= 1.2} BuildRequires: %{python_module Flask} -BuildRequires: %{python_module requests} -BuildRequires: %{python_module setuptools} +BuildRequires: %{python_module base >= 3.8} +BuildRequires: %{python_module pip} +BuildRequires: %{python_module poetry-core} +BuildRequires: %{python_module pytest} +BuildRequires: %{python_module requests >= 2.20} +BuildRequires: %{python_module responses} BuildRequires: fdupes BuildRequires: python-rpm-macros -Requires: python-Authlib +Requires: python-Authlib >= 1.2 Requires: python-Flask -Requires: python-requests -Requires(post): update-alternatives -Requires(postun):update-alternatives +Requires: python-blinker >= 1.4 +Requires: python-requests >= 2.20 BuildArch: noarch %python_subpackages @@ -49,27 +51,22 @@ It has been tested with: * Ipsilon %prep -%autosetup -p1 -n %{pypi_name}-%{version} +%autosetup -p1 -n flask_oidc-%{version} %build -%python_build +%pyproject_wheel %install -%python_install +%pyproject_install %python_expand %fdupes %{buildroot}%{$python_sitelib} -%python_clone -a %{buildroot}%{_bindir}/oidc-register -%post -%python_install_alternative oidc-register - -%postun -%python_uninstall_alternative oidc-register +%check +%pytest %files %{python_files} %doc README.rst -%license LICENSE.txt +%license LICENSES/BSD-2-Clause.txt %{python_sitelib}/flask_oidc/ -%{python_sitelib}/*.egg-info/ -%python_alternative %{_bindir}/oidc-register +%{python_sitelib}/flask_oidc-%{version}.dist-info %changelog