Repository: apiflask/apiflask (python)

Issue:
Relative redirect_uri not accepted by Okta
I'm trying to get APIFlask's swagger page to work with Okta auth and running into an issue. When I click the authorize button in the swagger ui I'm taken to a url like the following:
`https://my-domain.okta.com/oauth2/default/v1/authorize?response_type=token&client_id=123abc&redirect_uri=%2Flogin%2Fcallback`

The redirect_uri is a relative path to the route but okta requires a full url. I'm not sure if other authentication services accept relative routes, perhaps this is an incompatibility rather than a bug.

**Expected Behavior**:
Clicking on the authorize button from the swagger UI should take me to `https://my-domain.okta.com/oauth2/default/v1/authorize?response_type=token&client_id=123abc&redirect_uri=https%3A%2F%2F%2Fmy-domain.com%2Flogin%2Fcallback`

**Replicate:**
Here's my config:
```
    app = APIFlask(
        "My-App",
        docs_oauth2_redirect_path="/login/callback",
        docs_ui="swagger-ui",
    )
    oauth2 = {
        "type": "oauth2",
        "flows": {
            "implicit": {
                "authorizationUrl": "https://my-domain.okta.com/oauth2/default/v1/authorize",
                "scopes": {},
            }
        },
    }
    security_schemes = {
        "oauth2": oauth2,
    }
    app.security_schemes = security_schemes
    app.config["SWAGGER_UI_OAUTH_CONFIG"] = {
        "clientId": "123abc",
        "scopes": "openid",
        "additionalQueryStringParams": {
            "nonce": "123",
        },
    }
    app.config["SERVERS"] = [
        {"name": "Prod", "url": "https://my-domain.com"},
        {"name": "Dev", "url": "https://dev.my-domain.com"},
    ]
    app.config["SWAGGER_UI_OAUTH_REDIRECT_DECORATORS"] = [<my_custom_authenticate_method>]
```


Environment:
- Python version: 3.10.6
- Flask version: 2.2.2
- APIFlask version: 2.2.0


Pull request:
Add parameter 'docs_oauth2_redirect_path_external' to support absolute redirect url.
Add parameter 'docs_oauth2_redirect_path_external' to support absolute redirect url.

Fixes #602.
 
Checklist:

* [x]   Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
* [x]   Add or update relevant docs, in the `docs` folder and in code docstring.
* [x]   Add an entry in `CHANGES.md` summarizing the change and linking to the issue.
* [x]   Add `*Version changed*` or `*Version added*` note in any relevant docs and docstring.
* [x]   Run `pytest` and `tox`, no tests failed.

Patch:
diff --git a/CHANGES.md b/CHANGES.md
index 58d7239..a2233bb 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,12 @@
+## Version 2.3.3
+
+Released: -
+
+- Add 'docs_oauth2_redirect_path_external' parameter to support absolute redirect url ([issue #602][issue_602]).
+
+[issue_602]: https://github.com/apiflask/apiflask/issues/602
+
+
 ## Version 2.3.2
 
 Released: 2024/12/15
diff --git a/src/apiflask/app.py b/src/apiflask/app.py
index 8f259d9..82f33bd 100644
--- a/src/apiflask/app.py
+++ b/src/apiflask/app.py
@@ -16,6 +16,7 @@ from flask import json
 from flask import jsonify
 from flask import render_template_string
 from flask import request
+from flask import url_for
 from flask.config import ConfigAttribute
 from flask.wrappers import Response
 
@@ -268,6 +269,7 @@ class APIFlask(APIScaffold, Flask):
         spec_path: str | None = '/openapi.json',
         docs_path: str | None = '/docs',
         docs_oauth2_redirect_path: str | None = '/docs/oauth2-redirect',
+        docs_oauth2_redirect_path_external: bool = False,
         docs_ui: str = 'swagger-ui',
         openapi_blueprint_url_prefix: str | None = None,
         json_errors: bool = True,
@@ -299,6 +301,8 @@ class APIFlask(APIScaffold, Flask):
             docs_ui: The UI of API documentation, one of `swagger-ui` (default), `redoc`,
                 `elements`, `rapidoc`, and `rapipdf`.
             docs_oauth2_redirect_path: The path to Swagger UI OAuth redirect.
+            docs_oauth2_redirect_path_external: If `True`, the `docs_oauth2_redirect_path`
+                will be an external URL.
             openapi_blueprint_url_prefix: The url prefix of the OpenAPI blueprint. This
                 prefix will append before all the OpenAPI-related paths (`sepc_path`,
                 `docs_path`, etc.), defaults to `None`.
@@ -348,6 +352,7 @@ class APIFlask(APIScaffold, Flask):
         self.docs_ui = docs_ui
         self.docs_path = docs_path
         self.docs_oauth2_redirect_path = docs_oauth2_redirect_path
+        self.docs_oauth2_redirect_path_external = docs_oauth2_redirect_path_external
         self.openapi_blueprint_url_prefix = openapi_blueprint_url_prefix
         self.enable_openapi = enable_openapi
         self.json_errors = json_errors
@@ -509,6 +514,10 @@ class APIFlask(APIScaffold, Flask):
         The name of the blueprint is "openapi". This blueprint will hold the view
         functions for spec file and API docs.
 
+        *Version changed: 2.3.3*
+
+        - Add 'docs_oauth2_redirect_path_external' parameter to support absolute redirect url.
+
         *Version changed: 2.1.0*
 
         - Inject the OpenAPI endpoints decorators.
@@ -549,11 +558,20 @@ class APIFlask(APIScaffold, Flask):
             @bp.route(self.docs_path)
             @self._apply_decorators(config_name='DOCS_DECORATORS')
             def docs():
+                docs_oauth2_redirect_path = None
+                if self.docs_ui == 'swagger-ui':
+                    if self.docs_oauth2_redirect_path_external:
+                        docs_oauth2_redirect_path = url_for(
+                            'openapi.swagger_ui_oauth_redirect', _external=True
+                        )
+                    else:
+                        docs_oauth2_redirect_path = self.docs_oauth2_redirect_path
+
                 return render_template_string(
                     ui_templates[self.docs_ui],
                     title=self.title,
                     version=self.version,
-                    oauth2_redirect_path=self.docs_oauth2_redirect_path,
+                    oauth2_redirect_path=docs_oauth2_redirect_path,
                 )
 
             if self.docs_ui == 'swagger-ui':


Test patch:
diff --git a/tests/test_openapi_blueprint.py b/tests/test_openapi_blueprint.py
index bc73370..11bab53 100644
--- a/tests/test_openapi_blueprint.py
+++ b/tests/test_openapi_blueprint.py
@@ -69,6 +69,19 @@ def test_docs_oauth2_redirect_path(client):
     assert rv.status_code == 200
     assert b'oauth2RedirectUrl: "/docs/oauth2/redirect"' in rv.data
 
+    # Test the feature of external oauth2 redirect path
+    app = APIFlask(
+        __name__,
+        docs_oauth2_redirect_path='/docs/oauth2/redirect/external',
+        docs_oauth2_redirect_path_external=True,
+    )
+    rv = app.test_client().get('/docs/oauth2/redirect/external')
+    assert rv.status_code == 200
+    assert b'<title>Swagger UI: OAuth2 Redirect</title>' in rv.data
+    rv = app.test_client().get('/docs')
+    assert rv.status_code == 200
+    assert b'oauth2RedirectUrl: "http://localhost/docs/oauth2/redirect/external"' in rv.data
+
     app = APIFlask(__name__, docs_oauth2_redirect_path=None)
     assert app.docs_oauth2_redirect_path is None