Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic Provider #70

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open

Conversation

HarryKodden
Copy link

Add ability to connect to an OIDC OP at Runtime. For backwards compatibility the existing config option OIDC_CLIENT_SECRETS is still supported.

Usage for dynamic OP connectivity is:

Initialize the Flask App as normal:

app = Flask(__name__)
oidc =  OpenIDConnect(app)

Specify the Provider to connect with:

my_provider = { 
   'base_url': 'https://example.org' , 
   'registration': { 'client_id': 'xxx', 'client_secret': 'yyy' }
} 

Now finalize the configuration for this OIDC Provider (meaning the discovery will take place)

oidc.init_provider(my_provider)

And use the authentication as normal:

@app.route('/private')
@oidc.require_login
def private():
   ...

@HarryKodden
Copy link
Author

Travis reports 1 test is failing on Python 2.6.
This because 2.6 python fails on the expression number in {2,3,5,7}
Since this is part of rsa package, not sure what i can do about that...

Copy link
Owner

@puiterwijk puiterwijk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this!
Could you look at my comments, and preferably look at adding some documentation for this?

# Backwards compatible: When provider argument is ommitted,
# the value would be None, then in method load_secrets, the
# client_secrets will be tried to load...
with app.app_context():
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would need to move to init_app, since when we get here, the developer might not yet have provided an app instance.

with app.app_context():
self.init_provider(provider)

def __exit__(self, exception_type, exception_value, traceback):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the use of this? I don't think there's an __enter__, so this class could not be used as a context manager anyway?
I also don't see a reason why this should support context manager calling methods?

# get stuff from the app's config, which may override stuff set above
if app is not None:

if app:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you change this line? The intention is that it'll be called with either a flask.Flask instance or None.

"""

secrets = self.load_secrets(provider)
assert secrets != None, "Problem with loading secrets"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that assert statements get removed when a python optimized bytecode file is written, as happens in packaging on various linux distros.
Please turn this into an explicit check and raise ValueError.

Do setup for a specific provider

:param provider: The provider to initialize.
:type provider: Dictionary with at lease 'base_url' item
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/lease/least/

def load_secrets(self, provider):

try:
static_secrets = current_app.config.get('OIDC_CLIENT_SECRETS', None)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe return an error if provider is not None in this case, to avoid confusing people?


registration = provider.get('registration', None)

if not registration:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, while I think dynamic discovery is a reasonable idea, fully automatic dynamic registration without any configuration (e.g. provider['dynamic_registration'] = True) might be confusing to people.
Specifically because depending on the IdP in use, if people re-register every server start, their previously issued tokens might suddenly become unusable because they can no longer retrieve the token information.

Because of this, I think making the registration explicit would be a good idea.

if not provider:
raise Exception("No Provider specified")

try:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is kind of long.
Maybe add two helper functions def _perform_discovery() and def _perform_dynamic_registration()?


try:
try:
jwks_keys = json.load(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that if we fail to load a jwks_uri, we should just error out entirely, and not even continue starting, since JWKS is required for the protocol.

except Exception as e:
raise Exception("Error in preparing result: {}".format(str(e)))

raise Exception("No secrets loaded !")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any way for us to arrive at this line of code?

@svintit
Copy link

svintit commented Jul 22, 2020

@HarryKodden If you want this resolved i'm maintaining a fork of this with some added extensibility. Feel free to open a PR here:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants