diff --git a/docs/framework/authentication.mdx b/docs/framework/authentication.mdx
index 63e19bce5..f7f6d4062 100644
--- a/docs/framework/authentication.mdx
+++ b/docs/framework/authentication.mdx
@@ -9,6 +9,10 @@ The Writer Framework authentication module allows you to restrict access to your
trigger authentication for certain pages exclusively.
+
+ Static assets from Writer Framework exposed through `/static` and `/extensions` endpoints are not protected behind Authentication.
+
+
## Use Basic Auth
Basic Auth is a simple authentication method that uses a username and password. Authentication configuration is done in the [server_setup.py module](/framework/custom-server).
@@ -134,6 +138,21 @@ writer.serve.register_auth(oidc)
+### App static assets
+
+Static assets in your application are inaccessible. You can use the `app_static_public` parameter to allow their usage.
+When `app_static_public` is set to `True`, the static assets in your application are accessible without authentication.
+
+```python
+oidc = writer.auth.Auth0(
+ client_id="xxxxxxx",
+ client_secret="xxxxxxxxxxxxx",
+ domain="xxx-xxxxx.eu.auth0.com",
+ host_url=os.getenv('HOST_URL', "http://localhost:5000"),
+ app_static_public=True
+)
+```
+
## User information in event handler
When the `user_info` route is configured, user information will be accessible
diff --git a/pyproject.toml b/pyproject.toml
index 97e9e72e4..4ea7296ec 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "writer"
-version = "0.7.3"
+version = "0.7.4"
description = "An open-source, Python framework for building feature-rich apps that are fully integrated with the Writer platform."
authors = ["Writer, Inc."]
readme = "README.md"
diff --git a/src/writer/auth.py b/src/writer/auth.py
index 2e90a8653..92ca5a6a7 100644
--- a/src/writer/auth.py
+++ b/src/writer/auth.py
@@ -176,6 +176,7 @@ class Oidc(Auth):
scope: str = "openid email profile"
callback_authorize: str = "authorize"
url_userinfo: Optional[str] = None
+ app_static_public: bool = False
authlib: OAuth2Session = None
callback_func: Optional[Callable[[Request, str, dict], None]] = None # Callback to validate user authentication
@@ -191,14 +192,25 @@ def register(self,
redirect_url = urljoin(self.host_url, self.callback_authorize)
host_url_path = urlpath(self.host_url)
callback_authorize_path = urljoin(host_url_path, self.callback_authorize)
- static_assets_path = urljoin(host_url_path, "static")
+
+ auth_authorized_prefix_paths = []
+ auth_authorized_routes = [callback_authorize_path]
+
+ for asset_path in writer.serve.wf_root_static_assets():
+ if asset_path.is_file():
+ auth_authorized_routes.append(urljoin(host_url_path, asset_path.name))
+ elif asset_path.is_dir():
+ auth_authorized_prefix_paths.append(urljoin(host_url_path, asset_path.name))
+
+ if self.app_static_public is True:
+ auth_authorized_prefix_paths += [urljoin(host_url_path, "static"), urljoin(host_url_path, "extensions")]
logger.debug(f"[auth] oidc - url redirect: {redirect_url}")
logger.debug(f"[auth] oidc - endpoint authorize: {self.url_authorize}")
logger.debug(f"[auth] oidc - endpoint token: {self.url_oauthtoken}")
logger.debug(f"[auth] oidc - path: {host_url_path}")
- logger.debug(f"[auth] oidc - authorize path: {callback_authorize_path}")
- logger.debug(f"[auth] oidc - static asset path: {static_assets_path}")
+ logger.debug(f"[auth] oidc - auth authorized routes: {auth_authorized_routes}")
+ logger.debug(f"[auth] oidc - auth authorized prefix paths: {auth_authorized_prefix_paths}")
self.authlib = OAuth2Session(
client_id=self.client_id,
client_secret=self.client_secret,
@@ -215,7 +227,8 @@ def register(self,
async def oidc_middleware(request: Request, call_next):
session = request.cookies.get('session')
- if session is not None or request.url.path in [callback_authorize_path] or request.url.path.startswith(static_assets_path):
+ is_one_of_url_prefix_allowed = any(request.url.path.startswith(url_prefix) for url_prefix in auth_authorized_prefix_paths)
+ if session is not None or request.url.path in auth_authorized_routes or is_one_of_url_prefix_allowed:
response: Response = await call_next(request)
return response
else:
@@ -259,7 +272,7 @@ async def route_callback(request: Request):
})
-def Google(client_id: str, client_secret: str, host_url: str) -> Oidc:
+def Google(client_id: str, client_secret: str, host_url: str, app_static_public = False) -> Oidc:
"""
Configure Google Social login configured through Client Id for Web application in Google Cloud Console.
@@ -269,6 +282,7 @@ def Google(client_id: str, client_secret: str, host_url: str) -> Oidc:
:param client_id: client id of Web application
:param client_secret: client secret of Web application
:param host_url: The URL of the Writer Framework application (for callback)
+ :param app_static_public: authorizes the exposure of the user's static assets (/static and /extensions)
"""
return Oidc(
client_id=client_id,
@@ -276,9 +290,11 @@ def Google(client_id: str, client_secret: str, host_url: str) -> Oidc:
host_url=host_url,
url_authorize="https://accounts.google.com/o/oauth2/auth",
url_oauthtoken="https://oauth2.googleapis.com/token",
- url_userinfo="https://www.googleapis.com/oauth2/v1/userinfo?alt=json")
+ url_userinfo="https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
+ app_static_public=app_static_public
+ )
-def Github(client_id: str, client_secret: str, host_url: str) -> Oidc:
+def Github(client_id: str, client_secret: str, host_url: str, app_static_public = False) -> Oidc:
"""
Configure Github authentication.
@@ -288,16 +304,20 @@ def Github(client_id: str, client_secret: str, host_url: str) -> Oidc:
:param client_id: client id
:param client_secret: client secret
:param host_url: The URL of the Writer Framework application (for callback)
+ :param app_static_public: authorizes the exposure of the user's static assets (/static and /extensions)
"""
+
return Oidc(
client_id=client_id,
client_secret=client_secret,
host_url=host_url,
url_authorize="https://github.com/login/oauth/authorize",
url_oauthtoken="https://github.com/login/oauth/access_token",
- url_userinfo="https://api.github.com/user")
+ url_userinfo="https://api.github.com/user",
+ app_static_public=app_static_public
+ )
-def Auth0(client_id: str, client_secret: str, domain: str, host_url: str) -> Oidc:
+def Auth0(client_id: str, client_secret: str, domain: str, host_url: str, app_static_public = False) -> Oidc:
"""
Configure Auth0 application for authentication.
@@ -308,14 +328,18 @@ def Auth0(client_id: str, client_secret: str, domain: str, host_url: str) -> Oid
:param client_secret: client secret
:param domain: Domain of the Auth0 application
:param host_url: The URL of the Writer Framework application (for callback)
+ :param app_static_public: authorizes the exposure of the user's static assets (/static and /extensions)
"""
+
return Oidc(
client_id=client_id,
client_secret=client_secret,
host_url=host_url,
url_authorize=f"https://{domain}/authorize",
url_oauthtoken=f"https://{domain}/oauth/token",
- url_userinfo=f"https://{domain}/userinfo")
+ url_userinfo=f"https://{domain}/userinfo",
+ app_static_public=app_static_public
+ )
def urlpath(url: str):
"""
diff --git a/src/writer/serve.py b/src/writer/serve.py
index c65012522..3e466f39a 100644
--- a/src/writer/serve.py
+++ b/src/writer/serve.py
@@ -9,7 +9,7 @@
import typing
from contextlib import asynccontextmanager
from importlib.machinery import ModuleSpec
-from typing import Any, Callable, Dict, List, Optional, Set, Union, cast
+from typing import Any, Callable, Dict, List, Literal, Optional, Set, Tuple, Union, cast
from urllib.parse import urlsplit
import uvicorn
@@ -593,7 +593,7 @@ def _mount_server_static_path(app: FastAPI, server_static_path: pathlib.Path) ->
Writer Framework routes remain priority. A developer cannot come and overload them.
"""
app.get('/')(lambda: FileResponse(server_static_path.joinpath('index.html')))
- for f in server_static_path.glob('*'):
+ for f in wf_root_static_assets():
if f.is_file():
app.get(f"/{f.name}")(lambda: FileResponse(f))
if f.is_dir():
@@ -613,3 +613,22 @@ def _execute_server_setup_hook(user_app_path: str) -> None:
def app_runner(asgi_app: WriterFastAPI) -> AppRunner:
return asgi_app.state.app_runner
+
+
+def wf_root_static_assets() -> List[pathlib.Path]:
+ """
+ Lists the root writer Framework static assets. Some of them are files, some other are directories.
+
+ >>> for f in wf_root_static_assets()
+ >>> print(f"{f.name}")
+ >>> # favicon.ico
+ >>> # assets
+
+ """
+ all_static_assets: List[pathlib.Path] = []
+ server_path = pathlib.Path(__file__)
+ server_static_path = server_path.parent / "static"
+ for f in server_static_path.glob('*'):
+ all_static_assets.append(f)
+
+ return all_static_assets