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