Skip to content

Commit

Permalink
fix: Swagger UI missing oauth2 callback + Add PKCE support (#2524)
Browse files Browse the repository at this point in the history
* Add missing swagger-ui  oauth2-redirect.html

---------

Co-authored-by: Cody Fincher <[email protected]>
Co-authored-by: Janek Nouvertné <[email protected]>
  • Loading branch information
3 people authored Oct 30, 2023
1 parent dee35d4 commit 0784a0f
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 0 deletions.
34 changes: 34 additions & 0 deletions docs/usage/openapi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,40 @@ For example, lets say we wanted to change the base path of the OpenAPI related e
),
)
OAuth2 in Swagger UI
++++++++++++++++++++

When using Swagger, OAuth2 settings can be configured via :attr:`swagger_ui_init_oauth <litestar.openapi.controller.OpenAPIController.swagger_ui_init_oauth>`, which can be set to a dictionary containing the parameters described in the Swagger UI documentation for OAuth2 `here <https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/>`_.

We that you can preset your clientId or enable PKCE support.

Example Usage

.. code-block:: python
from litestar import Litestar
from litestar.openapi import OpenAPIConfig
from litestar.openapi import OpenAPIController
class MyOpenAPIController(OpenAPIController):
swagger_ui_init_oauth = {
"clientId": "your-client-id",
"appName": "your-app-name",
"scopeSeparator": " ",
"scopes": "openid profile",
"useBasicAuthenticationWithAccessCodeGrant": True,
"usePkceWithAuthorizationCodeGrant": True,
}
app = Litestar(
route_handlers=[...],
openapi_config=OpenAPIConfig(
title="My API", version="1.0.0", openapi_controller=MyOpenAPIController
),
)
CDN and offline file support
Expand Down
1 change: 1 addition & 0 deletions litestar/openapi/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ class OpenAPIConfig:
"openapi.json",
"openapi.yaml",
"openapi.yml",
"oauth2-redirect.html",
}
)
"""A set of the enabled documentation sites and schema download endpoints."""
Expand Down
116 changes: 116 additions & 0 deletions litestar/openapi/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ class OpenAPIController(Controller):
f"https://cdn.jsdelivr.net/npm/swagger-ui-dist@{swagger_ui_version}/swagger-ui-standalone-preset.js"
)
"""Download url for the Swagger Standalone Preset JS bundle."""
swagger_ui_init_oauth: dict[Any, Any] | bytes = {}
"""
JSON to initialize Swagger UI OAuth2 by calling the `initOAuth` method.
Refer to the following URL for details:
`Swagger-UI <https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/>`_.
"""
stoplight_elements_css_url: str = (
f"https://unpkg.com/@stoplight/elements@{stoplight_elements_version}/styles.min.css"
)
Expand Down Expand Up @@ -260,6 +267,114 @@ def rapidoc(self, request: Request[Any, Any, Any]) -> ASGIResponse:
return ASGIResponse(body=self.render_rapidoc(request), media_type=MediaType.HTML)
return ASGIResponse(body=self.render_404_page(), status_code=HTTP_404_NOT_FOUND, media_type=MediaType.HTML)

@get(path="/oauth2-redirect.html", media_type=MediaType.HTML, include_in_schema=False, sync_to_thread=False)
def swagger_ui_oauth2_redirect(self, request: Request[Any, Any, Any]) -> ASGIResponse: # pragma: no cover
"""Route handler responsible for rendering oauth2-redirect.html page for Swagger-UI.
Args:
request:
A :class:`Request <.connection.Request>` instance.
Returns:
A response with a rendered oauth2-redirect.html page for Swagger-UI.
"""
if self.should_serve_endpoint(request):
return ASGIResponse(body=self.render_swagger_ui_oauth2_redirect(request), media_type=MediaType.HTML)
return ASGIResponse(body=self.render_404_page(), status_code=HTTP_404_NOT_FOUND, media_type=MediaType.HTML)

def render_swagger_ui_oauth2_redirect(self, request: Request[Any, Any, Any]) -> bytes:
"""Render an HTML oauth2-redirect.html page for Swagger-UI.
Notes:
- override this method to customize the template.
Args:
request:
A :class:`Request <.connection.Request>` instance.
Returns:
A rendered html string.
"""
return rb"""<!doctype html>
<html lang="en-US">
<head>
<title>Swagger UI: OAuth2 Redirect</title>
</head>
<body>
<script>
'use strict';
function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;
if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1).replace('?', '&');
} else {
qp = location.search.substring(1);
}
arr = qp.split("&");
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
qp = qp ? JSON.parse('{' + arr.join() + '}',
function (key, value) {
return key === "" ? value : decodeURIComponent(value);
}
) : {};
isValid = qp.state === sentState;
if ((
oauth2.auth.schema.get("flow") === "accessCode" ||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
oauth2.auth.schema.get("flow") === "authorization_code"
) && !oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
});
}
if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
let oauthErrorMsg;
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
});
}
} else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
}
window.close();
}
if (document.readyState !== 'loading') {
run();
} else {
document.addEventListener('DOMContentLoaded', function () {
run();
});
}
</script>
</body>
</html>"""

def render_swagger_ui(self, request: Request[Any, Any, Any]) -> bytes:
"""Render an HTML page for Swagger-UI.
Expand Down Expand Up @@ -303,6 +418,7 @@ def render_swagger_ui(self, request: Request[Any, Any, Any]) -> bytes:
SwaggerUIBundle.SwaggerUIStandalonePreset
],
}})
ui.initOAuth({encode_json(self.swagger_ui_init_oauth).decode('utf-8')})
</script>
</body>
"""
Expand Down

0 comments on commit 0784a0f

Please sign in to comment.