Pass the configuration options in `init_app`
Opened this issue ยท 4 comments
I encounter issues when initializing flask-pyoidc and using url_for
to define callbacks URLs.
bp = Blueprint("endpoints", __name__)
user_provider_configuration = ProviderConfiguration(
...
post_logout_redirect_uris=url_for("endpoints.logout", _external=True),
)
auth = OIDCAuthentication({"default": user_provider_configuration}, current_app)
@bp.route("/logout")
@auth.oidc_logout
def logout():
...
There is a cycling dependency here, the logout
endpoint needs a OIDCAuthentication
object, that needs a ProviderConfiguration
, that calls url_for
, that needs the endpoint to be initialized.
I think this can be mitigated by delaying the ProviderConfiguration
part, for instance with the help of init_app
. I suggest to pass the configuration options to init_app
so this would be valid:
bp = Blueprint("endpoints", __name__)
auth = OIDCAuthentication()
@bp.route("/logout")
@auth.oidc_logout
def logout():
...
user_provider_configuration = ProviderConfiguration(
...
post_logout_redirect_uris=url_for("endpoints.logout", _external=True),
)
auth.init_app(app, {"default": user_provider_configuration})
What do you think?
flask.current_app
& flask.url_for
cannot work outside of application context. They can only be called either inside a view function or if the application context is manually pushed otherwise you will get this error:
RuntimeError: Working outside of application context.
This typically means that you attempted to use functionality that needed
the current application. To solve this, set up an application context
with app.app_context(). See the documentation for more information.
So, this line ๐ won't work:
>>> current_app.init_app({"default": user_provider_configuration})
You need access to the app
object here. You can either import app
object in your module or import your module to the module where your app
object lives and register the blueprint there.
...
post_logout_redirect_uris=url_for("endpoints.logout", _external=True),
...
This ๐ won't work and not even this ๐:
with app.app_context():
...
post_logout_redirect_uris=url_for("endpoints.logout", _external=True)
...
This is because in order for the app
to resolve URL of the view function, the view function must be registered on the app but it hasn't happened yet as the view function has not been initialized yet.
You need to restructure your code:
auth.py
from flask_pyoidc import OIDCAuthentication
from flask_pyoidc.provider_configuration import ClientMetadata, ProviderConfiguration
client_metadata = ClientMetadata(
client_id="client123",
client_secret="some_secret123",
post_logout_redirect_uris=["https://client.example.com/logout"]
)
provider_config = ProviderConfiguration(issuer="https://idp.example.com", client_metadata=client_metadata)
auth = OIDCAuthentication({"default": provider_config})
views.py
from flask import Blueprint
from auth import auth
bp = Blueprint("endpoints", __name__)
@bp.get("/")
def index():
return "hello flask"
@bp.get("/logout")
@auth.oidc_logout
def logout():
return "You have been logged out!"
run.py
from flask import Flask
from auth import auth
from views import bp
app = Flask(__name__)
app.register_blueprint(blueprint=bp)
auth.init_app(app=app)
if __name__ == "__main__":
app.run()
A single script will look like this
from flask import Blueprint, Flask
from flask_pyoidc import OIDCAuthentication
from flask_pyoidc.provider_configuration import ClientMetadata, ProviderConfiguration
app = Flask(__name__)
bp = Blueprint("endpoints", __name__)
client_metadata = ClientMetadata(
client_id="client123",
client_secret="some_secret123",
post_logout_redirect_uris=["https://client.example.com/logout"]
)
provider_config = ProviderConfiguration(issuer="https://idp.example.com", client_metadata=client_metadata)
# If app is present within the same module, you can directly pass it here,
# instead of calling "auth.init_app(app=app)".
auth = OIDCAuthentication(provider_configurations={"default": provider_config}, app=app)
@bp.get("/")
@auth.oidc_auth("default")
def index():
return "hello flask"
@bp.get("/logout")
@auth.oidc_logout
def logout():
return "You have been logged out!"
app.register_blueprint(blueprint=bp)
if __name__ == "__main__":
app.run()
Note
You wouldn't have to configure post_logout_redirect_uris
if your logout view function was decorated directly under app
and not under the blueprint
routes. Flask-pyoidc registers logout routes if they are registered on app but weren't declared in post_logout_redirect_uris
argument:
Flask-pyoidc/src/flask_pyoidc/flask_pyoidc.py
Lines 102 to 111 in 26b1235
But it is recommended to explicitly declare it so that someone else reading your code can know it upfront.
Thank you for your answer.
In my original post I actually oversimplified my actual code to get to the point. My real usecase has a setup closer to the run.py/auth.py/views.py you describe. The issue I have with your example is that the post_logout_redirect_uris
URL is an absolute URL, and I would like to use url_for
there.
This is because in order for the app to resolve URL of the view function, the view function must be registered on the app but it hasn't happened yet as the view function has not been initialized yet.
This is why I suggest to pass the OIDCAuthentication
configuration as init_app
arguments. This way you can call auth.init_app
within an app context, , allowing you to make calls to url_for
, after having registered the blueprints.
To be able to use url_for
to build a post_logout_redirect_uris
value, I currently do something hacky like this:
views.py
from flask import Blueprint
from auth import auth
bp = Blueprint("endpoints", __name__)
@bp.get("/")
def index():
return "hello flask"
@bp.get("/logout")
@auth.oidc_logout
def logout():
return "You have been logged out!"
run.py
from flask_pyoidc import OIDCAuthentication
from flask_pyoidc.provider_configuration import ClientMetadata, ProviderConfiguration
app = Flask(__name__)
auth = OIDCAuthentication({"default": None})
def setup_auth(app):
with app.app_context():
logout_url = url_for("myendpoint.logout", _external=True)
client_metadata = ClientMetadata(
client_id="client123",
client_secret="some_secret123",
post_logout_redirect_uris=[logout_url]
)
default_provider_config = ProviderConfiguration(issuer="https://idp.example.com", client_metadata=client_metadata)
auth._provider_configurations = {
"default": default_provider_configuration,
}
auth.init_app(app)
def create_app():
app.register_blueprint(blueprint=bp)
setup_auth(app)
return app
if __name__ == "__main__":
create_app().run()
The hacky part being auth._provider_configurations = {...}
.
I would love to be able to initialize flask-pyoidc this way:
app = Flask(__name__)
auth = OIDCAuthentication()
def setup_auth(app):
...
auth.init_app(app, {
"default": default_provider_configuration,
})
...
I think the bigger problem was that there was no way to dynamically resolve post logout redirect URI when logout routes are created by blueprints. The linked PR resolves that problem so you won't have to specify post_logout_redirect_uris
. You can also keep post_logout_redirect_uris in app config.
Thank you putting efforts in helping me to find a solution. I really appreciate this ๐
There is another thing I forgot to mention: some of the values passed to ProviderConfiguration
come from the flask configuration (app.config
), so an application context is needed at the moment the ProviderConfiguration
is initialized.
I really think the ideal solution would be to be able to initialize an OIDCAuthentication
object at the top level with no configuration, so it can be imported in blueprint files, and allow it to be configured by passing arguments to the init_app
method. This way init_app
could be called separately inside an application context, that would allow usage of app.config
, url_for
and any other Flask utility one would want to.
This two-steps configuration is a common pattern among other libraries like flask-babel, flask-caching, flask-cors or authlib.
This is an example of implementation in a project with huge quantities of legacy code I have no control over, including configuring the OIDC settings by the configuration:
https://github.com/numerique-gouv/b3desk/blob/main/web/b3desk/__init__.py#L182-L220