Skip to content

Commit 8994ec1

Browse files
committed
feat: add oauth2 to webui
1 parent 37ad000 commit 8994ec1

File tree

4 files changed

+157
-6
lines changed

4 files changed

+157
-6
lines changed

aws_lambda_powertools/event_handler/api_gateway.py

+19-3
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
from aws_lambda_powertools.event_handler.exceptions import NotFoundError, ServiceError
3535
from aws_lambda_powertools.event_handler.openapi.constants import DEFAULT_API_VERSION, DEFAULT_OPENAPI_VERSION
3636
from aws_lambda_powertools.event_handler.openapi.exceptions import RequestValidationError
37-
from aws_lambda_powertools.event_handler.openapi.swagger_ui.html import generate_swagger_html
3837
from aws_lambda_powertools.event_handler.openapi.types import (
3938
COMPONENT_REF_PREFIX,
4039
METHODS_WITH_BODY,
@@ -88,7 +87,9 @@
8887
Tag,
8988
)
9089
from aws_lambda_powertools.event_handler.openapi.params import Dependant
91-
from aws_lambda_powertools.event_handler.openapi.swagger_ui.oauth2 import OAuth2Config
90+
from aws_lambda_powertools.event_handler.openapi.swagger_ui.oauth2 import (
91+
OAuth2Config,
92+
)
9293
from aws_lambda_powertools.event_handler.openapi.types import (
9394
TypeModelOrEnum,
9495
)
@@ -1738,9 +1739,25 @@ def enable_swagger(
17381739
"""
17391740
from aws_lambda_powertools.event_handler.openapi.compat import model_json
17401741
from aws_lambda_powertools.event_handler.openapi.models import Server
1742+
from aws_lambda_powertools.event_handler.openapi.swagger_ui import (
1743+
generate_oauth2_redirect_html,
1744+
generate_swagger_html,
1745+
)
17411746

17421747
@self.get(path, middlewares=middlewares, include_in_schema=False, compress=compress)
17431748
def swagger_handler():
1749+
query_params = self.current_event.query_string_parameters or {}
1750+
1751+
# Check for query parameters; if "format" is specified as "oauth2-redirect",
1752+
# send the oauth2-redirect HTML stanza so OAuth2 can be used
1753+
# Source: https://github.com/swagger-api/swagger-ui/blob/master/dist/oauth2-redirect.html
1754+
if query_params.get("format") == "oauth2-redirect":
1755+
return Response(
1756+
status_code=200,
1757+
content_type="text/html",
1758+
body=generate_oauth2_redirect_html(),
1759+
)
1760+
17441761
base_path = self._get_base_path()
17451762

17461763
if swagger_base_url:
@@ -1783,7 +1800,6 @@ def swagger_handler():
17831800
# Check for query parameters; if "format" is specified as "json",
17841801
# respond with the JSON used in the OpenAPI spec
17851802
# Example: https://www.example.com/swagger?format=json
1786-
query_params = self.current_event.query_string_parameters or {}
17871803
if query_params.get("format") == "json":
17881804
return Response(
17891805
status_code=200,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from aws_lambda_powertools.event_handler.openapi.swagger_ui.html import (
2+
generate_swagger_html,
3+
)
4+
from aws_lambda_powertools.event_handler.openapi.swagger_ui.oauth2 import (
5+
OAuth2Config,
6+
OAuth2UnsafeConfig,
7+
generate_oauth2_redirect_html,
8+
)
9+
10+
__all__ = [
11+
"generate_swagger_html",
12+
"generate_oauth2_redirect_html",
13+
"OAuth2Config",
14+
"OAuth2UnsafeConfig",
15+
]

aws_lambda_powertools/event_handler/openapi/swagger_ui/html.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ def generate_swagger_html(
6666
{swagger_js_content}
6767
6868
<script>
69+
var currentUrl = new URL(window.location.href);
70+
var baseUrl = currentUrl.protocol + "//" + currentUrl.host + currentUrl.pathname;
71+
6972
var swaggerUIOptions = {{
7073
dom_id: "#swagger-ui",
7174
docExpansion: "list",
@@ -81,7 +84,9 @@ def generate_swagger_html(
8184
],
8285
plugins: [
8386
SwaggerUIBundle.plugins.DownloadUrl
84-
]
87+
],
88+
withCredentials: true,
89+
oauth2RedirectUrl: baseUrl + "?format=oauth2-redirect",
8590
}}
8691
8792
var ui = SwaggerUIBundle(swaggerUIOptions)

aws_lambda_powertools/event_handler/openapi/swagger_ui/oauth2.py

+117-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import Dict, Sequence
1+
# ruff: noqa: E501
2+
from typing import Dict, Optional, Sequence
23

34
from pydantic import BaseModel, Field
45

@@ -7,15 +8,32 @@
78

89
# Based on https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/
910
class OAuth2Config(BaseModel):
11+
"""
12+
OAuth2 configuration for Swagger UI
13+
"""
14+
15+
# The client ID for the OAuth2 application
1016
clientId: str = Field(alias="client_id")
11-
realm: str
17+
18+
# The realm in which the OAuth2 application is registered. Optional.
19+
realm: Optional[str]
20+
21+
# The name of the OAuth2 application
1222
appName: str = Field(alias="app_name")
23+
24+
# The scopes that the OAuth2 application requires. Defaults to an empty list.
1325
scopes: Sequence[str] = Field(default=[])
26+
27+
# Additional query string parameters to be included in the OAuth2 request. Defaults to an empty dictionary.
1428
additionalQueryStringParams: Dict[str, str] = Field(alias="additional_query_string_params", default={})
29+
30+
# Whether to use basic authentication with the access code grant type. Defaults to False.
1531
useBasicAuthenticationWithAccessCodeGrant: bool = Field(
1632
alias="use_basic_authentication_with_access_code_grant",
1733
default=False,
1834
)
35+
36+
# Whether to use PKCE with the authorization code grant type. Defaults to False.
1937
usePkceWithAuthorizationCodeGrant: bool = Field(alias="use_pkce_with_authorization_code_grant", default=False)
2038

2139
if PYDANTIC_V2:
@@ -25,3 +43,100 @@ class OAuth2Config(BaseModel):
2543
class Config:
2644
extra = "allow"
2745
allow_population_by_field_name = True
46+
47+
48+
class OAuth2UnsafeConfig(OAuth2Config):
49+
"""
50+
This class extends the OAuth2Config class and includes the client secret.
51+
This class NEVER BE USED IN PRODUCTION as it will expose sensitive information.
52+
"""
53+
54+
# The client secret for the OAuth2 application. This is sensitive information.
55+
clientSecret: str = Field(alias="client_secret")
56+
57+
58+
def generate_oauth2_redirect_html() -> str:
59+
"""
60+
Generates the HTML content for the OAuth2 redirect page.
61+
"""
62+
return """
63+
<!doctype html>
64+
<html lang="en-US">
65+
<head>
66+
<title>Swagger UI: OAuth2 Redirect</title>
67+
</head>
68+
<body>
69+
<script>
70+
'use strict';
71+
function run () {
72+
var oauth2 = window.opener.swaggerUIRedirectOauth2;
73+
var sentState = oauth2.state;
74+
var redirectUrl = oauth2.redirectUrl;
75+
var isValid, qp, arr;
76+
77+
if (/code|token|error/.test(window.location.hash)) {
78+
qp = window.location.hash.substring(1).replace('?', '&');
79+
} else {
80+
qp = location.search.substring(1);
81+
}
82+
83+
arr = qp.split("&");
84+
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
85+
qp = qp ? JSON.parse('{' + arr.join() + '}',
86+
function (key, value) {
87+
return key === "" ? value : decodeURIComponent(value);
88+
}
89+
) : {};
90+
91+
isValid = qp.state === sentState;
92+
93+
if ((
94+
oauth2.auth.schema.get("flow") === "accessCode" ||
95+
oauth2.auth.schema.get("flow") === "authorizationCode" ||
96+
oauth2.auth.schema.get("flow") === "authorization_code"
97+
) && !oauth2.auth.code) {
98+
if (!isValid) {
99+
oauth2.errCb({
100+
authId: oauth2.auth.name,
101+
source: "auth",
102+
level: "warning",
103+
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
104+
});
105+
}
106+
107+
if (qp.code) {
108+
delete oauth2.state;
109+
oauth2.auth.code = qp.code;
110+
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
111+
} else {
112+
let oauthErrorMsg;
113+
if (qp.error) {
114+
oauthErrorMsg = "["+qp.error+"]: " +
115+
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
116+
(qp.error_uri ? "More info: "+qp.error_uri : "");
117+
}
118+
119+
oauth2.errCb({
120+
authId: oauth2.auth.name,
121+
source: "auth",
122+
level: "error",
123+
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
124+
});
125+
}
126+
} else {
127+
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
128+
}
129+
window.close();
130+
}
131+
132+
if (document.readyState !== 'loading') {
133+
run();
134+
} else {
135+
document.addEventListener('DOMContentLoaded', function () {
136+
run();
137+
});
138+
}
139+
</script>
140+
</body>
141+
</html>
142+
""".strip()

0 commit comments

Comments
 (0)