Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: custom stylesheet and js are not loaded after OIDC authentication #544

Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
fix: custom stylesheet and js are not loaded after OIDC authentication
* fix: allow access to assets and static endpoint
* docs: improve details about authentication
FabienArcellier committed Sep 1, 2024
commit a6af9f7506d657efa1a9aae973caa3702317dbf8
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
Loading