Skip to content

Commit

Permalink
Merge pull request #553 from writer/dev
Browse files Browse the repository at this point in the history
chore: Merge for release
  • Loading branch information
ramedina86 authored Sep 3, 2024
2 parents 8bc8894 + 8c3290c commit 426882b
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 13 deletions.
19 changes: 19 additions & 0 deletions docs/framework/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ The Writer Framework authentication module allows you to restrict access to your
trigger authentication for certain pages exclusively.
</Warning>

<Warning>
Static assets from Writer Framework exposed through `/static` and `/extensions` endpoints are not protected behind Authentication.
</Warning>

## 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).
Expand Down Expand Up @@ -134,6 +138,21 @@ writer.serve.register_auth(oidc)

<img src="/framework/images/authentication_oidc.png" />

### 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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
44 changes: 34 additions & 10 deletions src/writer/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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:
Expand Down Expand Up @@ -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.
Expand All @@ -269,16 +282,19 @@ 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,
client_secret=client_secret,
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.
Expand All @@ -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.
Expand All @@ -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):
"""
Expand Down
23 changes: 21 additions & 2 deletions src/writer/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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():
Expand All @@ -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

0 comments on commit 426882b

Please sign in to comment.