Skip to content

Commit

Permalink
Fix swagger oauth2 redirect (#196)
Browse files Browse the repository at this point in the history
* fix swagger oauth2 redirect url

* use google oauth2 as example

* add client to the example

* add swagger oauth2 config

* use local lint config

* fix example

* fix oauth2 redirect page

* release 0.7.2
  • Loading branch information
kemingy committed Jan 26, 2022
1 parent 6729a63 commit e8d8851
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 34 deletions.
24 changes: 8 additions & 16 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,12 @@ repos:
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/pre-commit/mirrors-isort
rev: v5.7.0
- repo: local
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: 20.8b1
hooks:
- id: black
- repo: https://github.com/humitos/mirrors-autoflake.git
rev: v1.3
hooks:
- id: autoflake
args: ['--in-place']
- repo: https://gitlab.com/pycqa/flake8
rev: 3.8.4
hooks:
- id: flake8
- id: make-lint
name: Lint
entry: make lint
language: system
types: [python]
pass_filenames: false
always_run: true
9 changes: 4 additions & 5 deletions examples/security_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,10 @@ class Req(BaseModel):
"type": "oauth2",
"flows": {
"authorizationCode": {
"authorizationUrl": "https://example.com/oauth/authorize",
"tokenUrl": "https://example.com/oauth/token",
"authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth",
"tokenUrl": "https://sts.googleapis.com",
"scopes": {
"read": "Grants read access",
"write": "Grants write access",
"admin": "Grants access to admin operations",
"https://www.googleapis.com/auth/tasks.readonly": "tasks",
},
},
},
Expand All @@ -48,6 +46,7 @@ class Req(BaseModel):
"flask",
security_schemes=security_schemes,
SECURITY={"test_secure": []},
client_id="client_id",
)


Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

setup(
name="spectree",
version="0.7.1",
version="0.7.2",
license="Apache-2.0",
author="Keming Yang",
author_email="kemingy94@gmail.com",
Expand Down
52 changes: 52 additions & 0 deletions spectree/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import warnings
from enum import Enum
from typing import Dict, List, Optional

Expand Down Expand Up @@ -76,6 +77,25 @@ class Configuration(BaseSettings):
security_schemes: Optional[List[SecurityScheme]] = None
#: OpenAPI `security` JSON at the global level
security: Dict = {}
# Swagger OAuth2 configs
#: OAuth2 client id
client_id: str = ""
#: OAuth2 client secret
client_secret: str = ""
#: OAuth2 realm
realm: str = ""
#: OAuth2 app name
app_name: str = "spectree_app"
#: OAuth2 scope separator
scope_separator: str = " "
#: OAuth2 scopes
scopes: List[str] = []
#: OAuth2 additional query string params
additional_query_string_params: Dict[str, str] = {}
#: OAuth2 use basic authentication with access code grant
use_basic_authentication_with_access_code_grant: bool = False
#: OAuth2 use PKCE with authorization code grant
use_pkce_with_authorization_code_grant: bool = False

class Config:
env_prefix = "spectree_"
Expand All @@ -89,6 +109,38 @@ def convert_to_lower_case(cls, values):
def spec_url(self) -> str:
return f"/{self.path}/{self.filename}"

def swagger_oauth2_config(self) -> Dict:
"""
return the swagger UI OAuth2 configs
ref: https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/
"""
if self.client_secret:
warnings.warn("Do not use client_secret in production", UserWarning)

config = self.dict(
include={
"client_id",
"client_secret",
"realm",
"app_name",
"scope_separator",
"scopes",
"additional_query_string_params",
"use_basic_authentication_with_access_code_grant",
"use_pkce_with_authorization_code_grant",
}
)
config["use_basic_authentication_with_access_code_grant"] = (
"true"
if config["use_basic_authentication_with_access_code_grant"]
else "false"
)
config["use_pkce_with_authorization_code_grant"] = (
"true" if config["use_pkce_with_authorization_code_grant"] else "false"
)
return config

def openapi_info(self) -> Dict:
info = self.dict(
include={
Expand Down
96 changes: 91 additions & 5 deletions spectree/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,11 @@
<body>
<div id="swagger-ui"></div>
<script
src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui-bundle.js"></script>
<script
src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui-standalone-preset.js"></script>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui-bundle.js"></script>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui-standalone-preset.js"></script>
<script>
window.onload = function() {{
var full = location.protocol + '//' + location.hostname + (location.port ? ':' + location.port : '');
// Begin Swagger UI call region
const ui = SwaggerUIBundle({{
url: "{spec_url}",
Expand All @@ -81,13 +80,100 @@
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
oauth2RedirectUrl: full + "/{spec_path}/swagger/oauth2-redirect.html",
layout: "StandaloneLayout"
}})
ui.initOAuth({{
clientId: "{client_id}",
clientSecret: "{client_secret}",
realm: "{realm}",
appName: "{app_name}",
scopeSeparator: "{scope_separator}",
additionalQueryStringParams: {additional_query_string_params},
useBasicAuthenticationWithAccessCodeGrant: {use_basic_authentication_with_access_code_grant},
usePkceWithAuthorizationCodeGrant: {use_pkce_with_authorization_code_grant}
}})
// End Swagger UI call region
window.ui = ui
}}
</script>
</body>
</html>""",
</html>""", # noqa: E501
"swagger/oauth2-redirect.html": """
<!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);
}} 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 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();
}}
window.addEventListener('DOMContentLoaded', function () {{
run();
}});
</script>
</body>
</html>""", # noqa: E501
}
9 changes: 6 additions & 3 deletions spectree/plugins/falcon_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ def on_get(self, req, resp):


class DocPage:
def __init__(self, html, spec_url):
self.page = html.format(spec_url=spec_url)
def __init__(self, html, **kwargs):
self.page = html.format(**kwargs)

def on_get(self, req, resp):
resp.content_type = "text/html"
Expand Down Expand Up @@ -81,7 +81,10 @@ def register_route(self, app):
self.app.add_route(
f"/{self.config.path}/{ui}",
self.DOC_PAGE_ROUTE_CLASS(
self.config.page_templates[ui], self.config.spec_url
self.config.page_templates[ui],
spec_url=self.config.spec_url,
spec_path=self.config.path,
**self.config.swagger_oauth2_config(),
),
)

Expand Down
12 changes: 9 additions & 3 deletions spectree/plugins/flask_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,12 +216,16 @@ def gen_doc_page(ui):
)
)

return self.config.page_templates[ui].format(spec_url=spec_url)
return self.config.page_templates[ui].format(
spec_url=spec_url,
spec_path=self.config.path,
**self.config.swagger_oauth2_config(),
)

for ui in self.config.page_templates:
app.add_url_rule(
rule=f"/{self.config.path}/{ui}",
endpoint=f"openapi_{self.config.path}_{ui}",
endpoint=f"openapi_{self.config.path}_{ui.replace('.', '_')}",
view_func=lambda ui=ui: gen_doc_page(ui),
)

Expand All @@ -232,6 +236,8 @@ def gen_doc_page(ui):
rule=f"/{self.config.path}/{ui}",
endpoint=f"openapi_{self.config.path}_{ui}",
view_func=lambda ui=ui: self.config.page_templates[ui].format(
spec_url=self.config.spec_url
spec_url=self.config.spec_url,
spec_path=self.config.path,
**self.config.swagger_oauth2_config(),
),
)
6 changes: 5 additions & 1 deletion spectree/plugins/starlette_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ def register_route(self, app):
self.app.add_route(
f"/{self.config.path}/{ui}",
lambda request, ui=ui: HTMLResponse(
self.config.page_templates[ui].format(spec_url=self.config.spec_url)
self.config.page_templates[ui].format(
spec_url=self.config.spec_url,
spec_path=self.config.path,
**self.config.swagger_oauth2_config(),
)
),
)

Expand Down

0 comments on commit e8d8851

Please sign in to comment.