Package: oidcat
oidcatSimple OIDC.It's all so confusing, so I wrote a (small) wrapper package aroundrequestsandflask-oidcthat provides a (slightly) easier interface to protect and connect to private resources.I don't have time to wait for PRs to merge inflask_oidc, but maybe I'll get some of this merged eventually!InstallpipinstalloidcatUsageClientThis is arequests.Sessionobject that will handle tokens entirely for you. No need to refresh tokens, no need to manually log back in when both your access and refresh tokens expire.importosimportoidcat# basic login:sess=oidcat.Session('auth.myapp.com',os.getenv('USERNAME'),os.getenv('PASSWORD'))# that's it! all future requests will use the token# and it will automatically refresh so effectively, it'll never expire!out=sess.get('https://api.myapp.com/view').json()Resource ServerHere's an example resource server.NOTE: Technically you can do this without creating a client (and omit them inwith_well_known_secrets_file) and it will use theadmin-cliclient.importosimportflaskimportoidcat.serverapp=flask.Flask(__name__)app.config.update(# Create the client configuration (makes request to well known url)OIDC_CLIENT_SECRETS=oidcat.util.with_well_known_secrets_file('auth.myapp.com','myclient','supersecret'),# or:# Create keycloak client configuration (doesn't need request)# OIDC_CLIENT_SECRETS=oidcat.util.with_keycloak_secrets_file(#     'auth.myapp.com', 'myclient', 'supersecret', 'myrealm'),)importsqlitedictoidc=oidcat.server.OpenIDConnect(app,credentials_store=sqlitedict.SqliteDict('creds.db',autocommit=True))# or equivalently:oidc=oidcat.server.OpenIDConnect(app,'creds.db')# various forms of protecting endpoints@app.route('/')@oidc.require_logindefindex():'''This will redirect you to a login screen.'''5returnflask.jsonify({'message':'Welcome!'})# question - what exactly is the difference between these?@app.route('/edit')@oidc.accept_token(role='editor')# client/realm roledefedit():'''This will give a 402 if you don't pass `access_token`.'''returnflask.jsonify({'message':'you did something!'})@app.route('/edit')@oidc.accept_token(role='editor',realm=False)# client roledefedit():'''This will give a 402 if you don't pass `access_token`.'''returnflask.jsonify({'message':'you did something!'})@app.route('/view')@oidc.accept_token(scopes_required=['reader'])# client scopesdefview():'''This will give a 402 if you don't pass `access_token`.'''returnflask.jsonify({'message':'something interesting!'})@app.route('/ultimatepower')@oidc.accept_token(role='admin',client=None)# realm roledefultimatepower():'''This will give a 402 if you don't pass `access_token`.'''returnflask.jsonify({'message':'mwahahah!'})if__name__=='__main__':app.run(host='0.0.0.0',port=PORT,debug=True)ChangesSessionaddAccessabstraction which encapsulates the access and refresh tokens, well known, and login/logout logic.the access token is automatically added to requests using the Bearer token method.to disable this on a per-request basis, passtoken=Falseto your request method (e.g.sess.get(..., token=False))to disable this for all requests on the object, you can doSession(..., require_token=False)and to re-enable it on a per-request basis:sess.get(..., token=False)add alogin/logoutmethod (which is a convenience wrapper for the Access object)tokenadd a token class which encapsulates the token, token data, and expiration logica token's truthiness can be used to determine if it needs to be refreshedadd token checking functionhas_roleserver:accept_tokentakes additional parameters:role (str, list): roles to check for in the tokenclient (str, bool, default=True): seehas_rolechecks (list of callables): you can pass arbitraryhas_rolechecks for keycloak roles in the token. right now we just support Keycloak compatible token formats*roles (tuple[str]): the roles to compare againstclient_id (str, bool, default=True): if a string, it will check for roles in that
client_id. If True, it will check in the current client. If False/None, it
will check for realm roles.util.with_keycloak_secrets_file: generate the client secrets file and return the path to it. See usage above.this also handles all of the additional urls (token introspection, etc) from the base url.