Skip to content

Commit

Permalink
fix: custom stylesheet and js are not loaded after OIDC authentication
Browse files Browse the repository at this point in the history
* fix: allow access to assets and static endpoint
* docs: improve details about authentication
  • Loading branch information
FabienArcellier committed Sep 1, 2024
1 parent 9d3783d commit a6af9f7
Showing 3 changed files with 74 additions and 12 deletions.
19 changes: 19 additions & 0 deletions docs/framework/authentication.mdx
Original file line number Diff line number Diff line change
@@ -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).
@@ -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
44 changes: 34 additions & 10 deletions src/writer/auth.py
Original file line number Diff line number Diff line change
@@ -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,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.
@@ -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):
"""
23 changes: 21 additions & 2 deletions src/writer/serve.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit a6af9f7

Please sign in to comment.