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
Show file tree
Hide file tree
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
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
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
Loading