├── oidc ├── common │ ├── __init__.py │ ├── logs.py │ ├── secrets.py │ ├── routes.py │ └── redis.py ├── .gitignore ├── docs │ ├── aad-demo.png │ ├── auth0-demo.png │ ├── okta-demo.png │ ├── google-demo.png │ ├── aad-demo-claims.png │ ├── google-app-settings.png │ └── auth0-demo-with-google.png ├── requirements.txt ├── basic_okta.py ├── basic_aad.py ├── basic_auth0.py ├── basic_google.py ├── scopes_okta.py ├── scopes_auth0.py ├── scopes_storage_aad.py ├── basic_storage_aad.py ├── basic_storage_google.py ├── scopes_redis_aad.py ├── scopes_aad.py ├── README.md └── static │ ├── jwt-decode.js │ └── index.html ├── proxy-1 ├── .gitignore ├── requirements.txt ├── flask_app │ ├── static │ │ └── Octocat.jpg │ └── server.py ├── README.md ├── example.html └── blacksheep_proxy │ └── server.py ├── proxy-2 ├── .gitignore ├── requirements.txt ├── blacksheep_app │ ├── static │ │ └── Octocat.jpg │ └── server.py ├── README.md ├── example-2.html └── blacksheep_proxy │ └── server.py ├── testing-api ├── app │ ├── __init__.py │ ├── routes │ │ ├── __init__.py │ │ ├── router.py │ │ └── todos.py │ ├── main.py │ └── docs.py ├── data │ ├── __init__.py │ └── mocks.py ├── tests │ ├── __init__.py │ ├── utils.py │ ├── conftest.py │ ├── test_todos_api.py │ └── test_uvicorn_int.py ├── server.py ├── requirements.txt ├── domain.py └── README.md ├── websocket-chat ├── app │ ├── __init__.py │ ├── message.py │ ├── main.py │ ├── connection.py │ └── static │ │ └── index.html ├── server.py ├── requirements.txt └── README.md ├── dependency-injector ├── app │ ├── __init__.py │ └── di.py ├── requirements.txt ├── docs │ ├── README.md │ ├── example1.py │ └── example2.py ├── README.md └── main.py ├── max-body-size ├── .gitignore ├── requirements.txt ├── README.md ├── main.py └── static │ └── index.html ├── oauth2-password-provider ├── src │ ├── __init__.py │ ├── db.py │ ├── app.py │ ├── user.py │ └── password_auth.py ├── requirements.txt ├── server.py ├── example.http └── README.md ├── piccolo-admin ├── .gitignore ├── requirements.txt ├── README.md ├── Piccolo-Admin-LICENSE ├── server.py └── piccoloexample.py ├── server-sent-events ├── .gitignore ├── requirements.txt ├── static │ ├── index.html │ └── client.js ├── README.md └── server.py ├── long-polling ├── requirements.txt ├── static │ ├── index.html │ └── browser.js ├── README.md └── server.py ├── jwt-validation ├── .isort.cfg ├── workspace.code-workspace ├── README.md ├── requirements.txt ├── example.py └── dev │ └── request.http ├── aad-machine-to-machine ├── requirements.txt ├── certs │ ├── common.sh │ ├── README.md │ └── create-cert.sh ├── client_http_example.py ├── server.py ├── client_using_secret.py ├── client_using_certificate.py └── README.md ├── LICENSE ├── otel ├── grafanaexample.py ├── azureexample.py ├── requirements.txt ├── otel │ ├── otlp.py │ ├── azure.py │ └── __init__.py └── README.md ├── .gitignore └── README.md /oidc/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /oidc/.gitignore: -------------------------------------------------------------------------------- 1 | venvv1 2 | -------------------------------------------------------------------------------- /proxy-1/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | -------------------------------------------------------------------------------- /proxy-2/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | -------------------------------------------------------------------------------- /testing-api/app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testing-api/data/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testing-api/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /websocket-chat/app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dependency-injector/app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /max-body-size/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | 3 | -------------------------------------------------------------------------------- /oauth2-password-provider/src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo-admin/.gitignore: -------------------------------------------------------------------------------- 1 | example.sqlite 2 | -------------------------------------------------------------------------------- /server-sent-events/.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | .local/ 3 | -------------------------------------------------------------------------------- /testing-api/server.py: -------------------------------------------------------------------------------- 1 | from app.main import app 2 | -------------------------------------------------------------------------------- /websocket-chat/server.py: -------------------------------------------------------------------------------- 1 | from app.main import app 2 | -------------------------------------------------------------------------------- /max-body-size/requirements.txt: -------------------------------------------------------------------------------- 1 | blacksheep 2 | uvicorn 3 | -------------------------------------------------------------------------------- /websocket-chat/requirements.txt: -------------------------------------------------------------------------------- 1 | uvicorn[standard] 2 | blacksheep -------------------------------------------------------------------------------- /proxy-2/requirements.txt: -------------------------------------------------------------------------------- 1 | blacksheep>=1.2.17 2 | uvicorn==0.22.0 3 | -------------------------------------------------------------------------------- /long-polling/requirements.txt: -------------------------------------------------------------------------------- 1 | blacksheep==2.0.4 2 | uvicorn==0.25.0 3 | -------------------------------------------------------------------------------- /jwt-validation/.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | profile = black 3 | multi_line_output = 3 4 | -------------------------------------------------------------------------------- /proxy-1/requirements.txt: -------------------------------------------------------------------------------- 1 | blacksheep==1.2.17 2 | Flask==2.3.2 3 | uvicorn==0.22.0 4 | -------------------------------------------------------------------------------- /testing-api/app/routes/__init__.py: -------------------------------------------------------------------------------- 1 | from .router import * 2 | from .todos import * 3 | -------------------------------------------------------------------------------- /piccolo-admin/requirements.txt: -------------------------------------------------------------------------------- 1 | blacksheep==1.0.9 2 | piccolo-admin==0.14.0 3 | uvicorn==0.14.0 4 | -------------------------------------------------------------------------------- /testing-api/requirements.txt: -------------------------------------------------------------------------------- 1 | blacksheep 2 | uvicorn 3 | pytest 4 | pytest-asyncio 5 | pydantic 6 | -------------------------------------------------------------------------------- /aad-machine-to-machine/requirements.txt: -------------------------------------------------------------------------------- 1 | blacksheep 2 | msal 3 | uvicorn 4 | python-dotenv 5 | httpx 6 | -------------------------------------------------------------------------------- /server-sent-events/requirements.txt: -------------------------------------------------------------------------------- 1 | blacksheep>=2.0.6 2 | uvicorn==0.34.2 3 | Hypercorn==0.17.3 4 | -------------------------------------------------------------------------------- /oidc/docs/aad-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neoteroi/BlackSheep-Examples/HEAD/oidc/docs/aad-demo.png -------------------------------------------------------------------------------- /dependency-injector/requirements.txt: -------------------------------------------------------------------------------- 1 | blacksheep>=2.2.0 2 | dependency-injector==4.46.0 3 | uvicorn==0.34.2 4 | -------------------------------------------------------------------------------- /oidc/docs/auth0-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neoteroi/BlackSheep-Examples/HEAD/oidc/docs/auth0-demo.png -------------------------------------------------------------------------------- /oidc/docs/okta-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neoteroi/BlackSheep-Examples/HEAD/oidc/docs/okta-demo.png -------------------------------------------------------------------------------- /oidc/docs/google-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neoteroi/BlackSheep-Examples/HEAD/oidc/docs/google-demo.png -------------------------------------------------------------------------------- /oidc/docs/aad-demo-claims.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neoteroi/BlackSheep-Examples/HEAD/oidc/docs/aad-demo-claims.png -------------------------------------------------------------------------------- /oidc/docs/google-app-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neoteroi/BlackSheep-Examples/HEAD/oidc/docs/google-app-settings.png -------------------------------------------------------------------------------- /oidc/docs/auth0-demo-with-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neoteroi/BlackSheep-Examples/HEAD/oidc/docs/auth0-demo-with-google.png -------------------------------------------------------------------------------- /proxy-1/flask_app/static/Octocat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neoteroi/BlackSheep-Examples/HEAD/proxy-1/flask_app/static/Octocat.jpg -------------------------------------------------------------------------------- /oauth2-password-provider/requirements.txt: -------------------------------------------------------------------------------- 1 | pydantic==1.10.2 2 | blacksheep==1.2.17 3 | pyjwt==2.6.0 4 | uvicorn==0.23.2 5 | cryptography==41.0.4 6 | -------------------------------------------------------------------------------- /proxy-2/blacksheep_app/static/Octocat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neoteroi/BlackSheep-Examples/HEAD/proxy-2/blacksheep_app/static/Octocat.jpg -------------------------------------------------------------------------------- /testing-api/app/routes/router.py: -------------------------------------------------------------------------------- 1 | from blacksheep.server.routing import Router 2 | 3 | 4 | router = Router() 5 | 6 | get = router.get 7 | post = router.post 8 | delete = router.delete 9 | -------------------------------------------------------------------------------- /testing-api/app/main.py: -------------------------------------------------------------------------------- 1 | from .routes import router 2 | from .docs import docs 3 | from blacksheep.server.application import Application 4 | 5 | 6 | app = Application(router=router) 7 | docs.bind_app(app) 8 | -------------------------------------------------------------------------------- /dependency-injector/docs/README.md: -------------------------------------------------------------------------------- 1 | # Examples from the BlackSheep documentation 2 | 3 | This folder contains full examples described in [the documentation](https://www.neoteroi.dev/blacksheep/dependency-injection/). 4 | -------------------------------------------------------------------------------- /websocket-chat/app/message.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | 3 | 4 | @dataclasses.dataclass 5 | class Message: 6 | author: str 7 | text: str 8 | timestamp: str 9 | 10 | def asdict(self): 11 | return dataclasses.asdict(self) 12 | -------------------------------------------------------------------------------- /server-sent-events/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Server Sent Events

7 | 8 | Message: 9 | 10 | 11 | -------------------------------------------------------------------------------- /jwt-validation/workspace.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {}, 8 | "extensions": { 9 | "recommendations": [ 10 | "humao.rest-client", 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /testing-api/tests/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from blacksheep.contents import Content 3 | from essentials.json import dumps 4 | 5 | 6 | def json_content(data: Any) -> Content: 7 | return Content( 8 | b"application/json", 9 | dumps(data, separators=(",", ":")).encode("utf8"), 10 | ) 11 | -------------------------------------------------------------------------------- /oauth2-password-provider/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import uvicorn 4 | 5 | # from tmw_server.app import app 6 | 7 | if __name__ == "__main__": 8 | uvicorn.run( 9 | "src.app:app", 10 | host="127.0.0.1", 11 | port=8000, 12 | log_level="debug", 13 | reload=True, 14 | ) 15 | -------------------------------------------------------------------------------- /testing-api/app/docs.py: -------------------------------------------------------------------------------- 1 | from blacksheep.server.openapi.v3 import OpenAPIHandler 2 | from openapidocs.v3 import Info 3 | 4 | docs = OpenAPIHandler( 5 | info=Info(title="Demo API", version="0.0.1"), anonymous_access=True 6 | ) 7 | 8 | # include only endpoints whose path starts with "/api/" 9 | docs.include = lambda path, _: path.startswith("/api/") 10 | -------------------------------------------------------------------------------- /oidc/common/logs.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from blacksheep.baseapp import get_logger 4 | from blacksheep.server.authentication.oidc import get_logger as get_oidc_logger 5 | 6 | logging.basicConfig(level=logging.DEBUG, format="%(message)s") 7 | 8 | for logger in {get_logger(), get_oidc_logger()}: 9 | logger.setLevel(logging.DEBUG) 10 | logger.addHandler(logging.StreamHandler()) 11 | -------------------------------------------------------------------------------- /aad-machine-to-machine/certs/common.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | die () { 4 | lightred='\033[1;31m' 5 | nocolor='\033[0m' 6 | echo -e "${lightred}${1}${nocolor}" >&2 7 | exit 1 8 | } 9 | 10 | print_info() { 11 | lightcyan='\033[1;36m' 12 | nocolor='\033[0m' 13 | echo -e "${lightcyan}[*] $1${nocolor}" 14 | } 15 | 16 | print_warn() { 17 | yellow='\e[93m' 18 | nocolor='\033[0m' 19 | echo -e "${yellow}[*] $1${nocolor}" 20 | } 21 | -------------------------------------------------------------------------------- /server-sent-events/static/client.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var eventSource = new EventSource("/events"); 3 | 4 | eventSource.onmessage = function(event) { 5 | console.log(event); 6 | var element = document.getElementById("message"); 7 | element.innerHTML = event.data; 8 | console.log("Data:", JSON.parse(event.data)); 9 | } 10 | 11 | eventSource.onerror = function(err) { 12 | console.error("EventSource failed:", err); 13 | } 14 | })() 15 | -------------------------------------------------------------------------------- /oidc/common/secrets.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dataclasses import dataclass 3 | 4 | 5 | @dataclass 6 | class Secrets: 7 | auth0_client_secret: str 8 | okta_client_secret: str 9 | aad_client_secret: str 10 | 11 | @classmethod 12 | def from_env(cls): 13 | return cls( 14 | auth0_client_secret=os.environ["AUTH0_CLIENT_SECRET"], 15 | okta_client_secret=os.environ["OKTA_CLIENT_SECRET"], 16 | aad_client_secret=os.environ["AAD_CLIENT_SECRET"], 17 | ) 18 | -------------------------------------------------------------------------------- /oauth2-password-provider/example.http: -------------------------------------------------------------------------------- 1 | ### Register new user 2 | POST http://localhost:8000/api/register 3 | Content-Type: application/json 4 | 5 | { 6 | "username": "test@example.com", 7 | "password": "password" 8 | } 9 | 10 | ### Get JWT token 11 | POST http://localhost:8000/api/token 12 | Content-Type: application/json 13 | 14 | { 15 | "username": "test@example.com", 16 | "password": "password" 17 | } 18 | 19 | ### Get secret message 20 | GET http://localhost:8000/api/protected 21 | Authorization: Bearer 22 | -------------------------------------------------------------------------------- /dependency-injector/README.md: -------------------------------------------------------------------------------- 1 | # Using Dependency Injector instead of Rodi 2 | 3 | This example illustrates how to use Dependency Injector instead of Rodi. 4 | 5 | The example supports automatic injection of dependencies in request handlers 6 | and controllers' constructors when dependencies are type annotated. 7 | 8 | To run the example: 9 | 10 | ```bash 11 | uvicorn main:app 12 | ``` 13 | 14 | Make requests to "/" and "/controller-test" to test DI resolution in request 15 | handlers defined using functions and `Controller` methods respectively. 16 | -------------------------------------------------------------------------------- /jwt-validation/README.md: -------------------------------------------------------------------------------- 1 | # JWT example 2 | 3 | This folder contains an example showing how to use JWTs (JSON Web Tokens) for 4 | authenticating and authorizing users. 5 | 6 | This example illustrates how to validate JSON Web Tokens issued by Azure Active 7 | Directory, but the same principles apply to any identity provider implementing 8 | OAuth. 9 | 10 | Since version `1.2.1`, blacksheep has built-in support for JWT Bearer 11 | authentication. Refer to the [documentation for more 12 | details](https://www.neoteroi.dev/blacksheep/authentication/#jwt-bearer). 13 | -------------------------------------------------------------------------------- /testing-api/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | from blacksheep.testing import TestClient 5 | from server import app as app_server 6 | 7 | 8 | @pytest.fixture(scope="session") 9 | def event_loop(request): 10 | loop = asyncio.get_event_loop_policy().new_event_loop() 11 | yield loop 12 | loop.close() 13 | 14 | 15 | @pytest.fixture(scope="session") 16 | async def api(): 17 | await app_server.start() 18 | yield app_server 19 | await app_server.stop() 20 | 21 | 22 | @pytest.fixture(scope="session") 23 | async def test_client(api): 24 | return TestClient(api) 25 | -------------------------------------------------------------------------------- /oauth2-password-provider/src/db.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pydantic import UUID4, BaseModel, Field 4 | from datetime import datetime 5 | 6 | 7 | from uuid import uuid4 8 | 9 | 10 | class User(BaseModel): 11 | id: UUID4 = Field(default_factory=uuid4) 12 | username: str 13 | password: str = Field(exclude=True) 14 | active: bool = True 15 | 16 | class Config: 17 | exclude = {"password"} 18 | 19 | 20 | class Token(BaseModel): 21 | id: UUID4 22 | user_id: UUID4 23 | session_id: UUID4 24 | expired_at: datetime 25 | 26 | 27 | USER_DB: dict[str, User] = {} 28 | TOKEN_DB: dict[UUID4, Token] = {} 29 | -------------------------------------------------------------------------------- /testing-api/domain.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import List 3 | from pydantic import BaseModel 4 | 5 | 6 | class ToDo(BaseModel): 7 | id: int 8 | title: str 9 | description: str 10 | 11 | 12 | class CreateToDoInput(BaseModel): 13 | title: str 14 | description: str 15 | 16 | 17 | class ToDosRepository(ABC): 18 | @abstractmethod 19 | async def get_todos(self) -> List[ToDo]: 20 | ... 21 | 22 | @abstractmethod 23 | async def store_todo(self, item: ToDo) -> None: 24 | ... 25 | 26 | @abstractmethod 27 | async def delete_todo(self, item: ToDo) -> None: 28 | ... 29 | -------------------------------------------------------------------------------- /oidc/requirements.txt: -------------------------------------------------------------------------------- 1 | async-timeout==4.0.2 2 | black==23.1.0 3 | blacksheep==2.0a5 4 | certifi==2022.12.7 5 | cffi==1.15.1 6 | chardet==5.0.0 7 | click==8.1.3 8 | cryptography==38.0.4 9 | essentials==1.1.5 10 | essentials-openapi==1.0.6 11 | guardpost==1.0.1 12 | h11==0.14.0 13 | httptools==0.5.0 14 | isort==5.12.0 15 | itsdangerous==2.1.2 16 | Jinja2==3.1.2 17 | MarkupSafe==2.1.2 18 | mypy-extensions==1.0.0 19 | packaging==23.0 20 | pathspec==0.11.0 21 | platformdirs==3.0.0 22 | pycparser==2.21 23 | PyJWT==2.6.0 24 | python-dateutil==2.8.2 25 | python-dotenv==0.21.1 26 | PyYAML==6.0 27 | redis==4.5.1 28 | rodi==2.0.2 29 | ruff==0.0.253 30 | six==1.16.0 31 | uvicorn==0.20.0 32 | websockets==10.4 33 | -------------------------------------------------------------------------------- /jwt-validation/requirements.txt: -------------------------------------------------------------------------------- 1 | anyio==3.3.0 2 | appdirs==1.4.4 3 | asgiref==3.4.1 4 | black==21.7b0 5 | blacksheep~=1.2.1 6 | cchardet==2.1.7 7 | certifi==2021.5.30 8 | cffi==1.14.6 9 | click==8.0.1 10 | cryptography==3.4.7 11 | essentials==1.1.4 12 | essentials-configuration==0.0.2 13 | essentials-openapi==0.1.5 14 | guardpost==0.0.9 15 | h11==0.12.0 16 | httpcore==0.13.6 17 | httptools==0.2.0 18 | idna==3.2 19 | isort==5.9.3 20 | itsdangerous==2.0.1 21 | Jinja2==3.0.2 22 | MarkupSafe==2.0.1 23 | mypy-extensions==0.4.3 24 | pathspec==0.9.0 25 | pycparser==2.20 26 | PyJWT==2.1.0 27 | python-dateutil==2.8.2 28 | PyYAML==5.4.1 29 | regex==2021.8.3 30 | rfc3986==1.5.0 31 | rodi==1.1.1 32 | six==1.16.0 33 | sniffio==1.2.0 34 | tomli==1.2.1 35 | uvicorn==0.15.0 36 | -------------------------------------------------------------------------------- /long-polling/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | All visitors of this page will see messages of each other. 8 | 9 |
10 | 11 | 12 |
13 | 14 |
15 |
16 | 17 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /testing-api/data/mocks.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | 3 | from domain import ToDo, ToDosRepository 4 | from rodi import Container 5 | 6 | 7 | class MockedToDosRepository(ToDosRepository): 8 | def __init__(self) -> None: 9 | self._todos: Dict[int, ToDo] = {} 10 | 11 | async def get_todos(self) -> List[ToDo]: 12 | return list(self._todos.values()) 13 | 14 | async def store_todo(self, item: ToDo) -> None: 15 | self._todos[item.id] = item 16 | 17 | async def delete_todo(self, item: ToDo) -> None: 18 | try: 19 | del self._todos[item.id] 20 | except KeyError: 21 | pass 22 | 23 | 24 | def register_mocked_services(container: Container) -> None: 25 | container.add_scoped(ToDosRepository, MockedToDosRepository) 26 | -------------------------------------------------------------------------------- /websocket-chat/app/main.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | from blacksheep import Application, WebSocket, WebSocketDisconnectError 4 | from blacksheep.server.responses import redirect 5 | from .connection import ConnectionManager 6 | 7 | APP_PATH = pathlib.Path(__file__).parent / 'static' 8 | 9 | app = Application() 10 | app.serve_files(APP_PATH, root_path='app') 11 | 12 | manager = ConnectionManager() 13 | 14 | 15 | @app.router.ws('/ws/{client_id}') 16 | async def ws(websocket: WebSocket, client_id: str): 17 | conn = await manager.connect(websocket, client_id) 18 | 19 | try: 20 | while True: 21 | await manager.manage(conn) 22 | except WebSocketDisconnectError: 23 | await manager.disconnect(conn) 24 | 25 | 26 | @app.router.get('/') 27 | def index(): 28 | return redirect('/app') 29 | -------------------------------------------------------------------------------- /proxy-2/README.md: -------------------------------------------------------------------------------- 1 | # Example showing an HTTP Proxy implemented with BlackSheep 2 | 3 | This example shows an HTTP Proxy implementation, proxying requests for another 4 | `blacksheep` back-end. 5 | 6 | Run the frst BlackSheep application: 7 | 8 | ```bash 9 | python blacksheep_app/server.py 10 | ``` 11 | 12 | Run the BlackSheep proxy application: 13 | 14 | ```bash 15 | python blacksheep_proxy/server.py 16 | ``` 17 | 18 | Open `example-2.html` in a browser and use its forms to test uploading to the 19 | server directly, and to the BlackSheep proxy. The result should be the same. 20 | 21 | ## Note 22 | The example proxy in `blacksheep_proxy` handles memory in the proper way: 23 | 24 | - it reads input streams as chunks (never whole in memory) 25 | - it reads response streams from the back-end in chunks (never whole in memory) 26 | -------------------------------------------------------------------------------- /testing-api/README.md: -------------------------------------------------------------------------------- 1 | # TestClient example 2 | 3 | This folder contains an example showing how to test an API using the built-in 4 | `TestClient` and `pytest`. 5 | 6 | The preparation of this example is described in the official documentation under [testing](https://www.neoteroi.dev/blacksheep/testing/). 7 | This demo uses SQLite, refer to Piccolo's documentation to use PostgreSQL. 8 | 9 | ## Getting started 10 | 11 | 1. Create a Python virtual environment 12 | 2. Activate the virtual environment 13 | 3. Install dependencies in `requirements.txt` 14 | 4. Run tests using `pytest` 15 | 16 | ```bash 17 | # create a Python virtual environment 18 | python -m venv venv 19 | 20 | # activate 21 | source venv/bin/activate # (Linux) 22 | 23 | venv\Scripts\activate # (Windows) 24 | 25 | # install dependencies 26 | pip install -r requirements.txt 27 | 28 | # run tests 29 | pytest 30 | ``` 31 | -------------------------------------------------------------------------------- /oidc/basic_okta.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to configure an OpenID Connect integration with Okta, obtaining 3 | only an id_token, exchanged with the client using a response cookie. 4 | """ 5 | import uvicorn 6 | from blacksheep.server.application import Application 7 | from blacksheep.server.authentication.oidc import OpenIDSettings, use_openid_connect 8 | 9 | from common.routes import register_routes 10 | 11 | app = Application(show_error_details=True) 12 | 13 | 14 | # basic Okta integration that handles only an id_token 15 | use_openid_connect( 16 | app, 17 | OpenIDSettings( 18 | authority="https://dev-34685660.okta.com", 19 | client_id="0oa2gy88qiVyuOClI5d7", 20 | callback_path="/authorization-code/callback", 21 | ), 22 | ) 23 | 24 | register_routes(app) 25 | 26 | 27 | if __name__ == "__main__": 28 | uvicorn.run(app, host="127.0.0.1", port=5000, log_level="debug") 29 | -------------------------------------------------------------------------------- /oidc/basic_aad.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to configure an OpenID Connect integration with Azure Active 3 | Directory, obtaining only an id_token, exchanged with the client using a response 4 | cookie. 5 | """ 6 | import uvicorn 7 | from blacksheep.server.application import Application 8 | from blacksheep.server.authentication.oidc import OpenIDSettings, use_openid_connect 9 | 10 | from common.routes import register_routes 11 | 12 | app = Application(show_error_details=True) 13 | 14 | 15 | # basic AAD integration that handles only an id_token 16 | use_openid_connect( 17 | app, 18 | OpenIDSettings( 19 | authority="https://login.microsoftonline.com/b62b317a-19c2-40c0-8650-2d9672324ac4/v2.0/", 20 | client_id="499adb65-5e26-459e-bc35-b3e1b5f71a9d", 21 | ), 22 | ) 23 | 24 | register_routes(app) 25 | 26 | 27 | if __name__ == "__main__": 28 | uvicorn.run(app, host="127.0.0.1", port=5000, log_level="debug") 29 | -------------------------------------------------------------------------------- /oidc/basic_auth0.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to configure an OpenID Connect integration with Auth0, obtaining 3 | only an id_token, exchanged with the client using a response cookie. 4 | """ 5 | import uvicorn 6 | from blacksheep.server.application import Application 7 | from blacksheep.server.authentication.oidc import ( 8 | OpenIDSettings, 9 | use_openid_connect, 10 | ) 11 | 12 | from common.routes import register_routes 13 | 14 | app = Application(show_error_details=True) 15 | 16 | 17 | # basic Auth0 integration that handles only an id_token 18 | use_openid_connect( 19 | app, 20 | OpenIDSettings( 21 | authority="https://neoteroi.eu.auth0.com", 22 | client_id="OOGPl4dgG7qKsm2IOWq72QhXV4wsLhbQ", 23 | callback_path="/signin-oidc", 24 | ), 25 | ) 26 | 27 | register_routes(app) 28 | 29 | 30 | if __name__ == "__main__": 31 | uvicorn.run(app, host="127.0.0.1", port=5000, log_level="debug") 32 | -------------------------------------------------------------------------------- /aad-machine-to-machine/certs/README.md: -------------------------------------------------------------------------------- 1 | # Example with cliet credential flow using a certificate 2 | 3 | This folder contains an utility script that can be used to generate certificates 4 | for app registrations in Azure Active Directory. 5 | 6 | It is for Bash (it also works in the Git Bash for Windows, but it requires an 7 | extra variable like explained below). 8 | Example usage: 9 | 10 | ```bash 11 | # Examples: 12 | NAME=foo ./create-cert.sh 13 | 14 | # With subject: 15 | NAME=example SUBJECT="/C=IT/ST=TO/L=TO/O=E/OU=Example Team/CN=example.com" ./create-cert.sh 16 | 17 | # With password for PFX: 18 | NAME=example SUBJECT="/C=IT/ST=TO/L=TO/O=E/OU=Example Team/CN=example.com" PFX_PASS=FooFoo ./create-cert.sh 19 | ``` 20 | 21 | When using the Git Bash for Windows, include **MSYS_NO_PATHCONV=1**, like in: 22 | 23 | ```bash 24 | MSYS_NO_PATHCONV=1 NAME=example SUBJECT="/C=IT/ST=TO/L=TO/O=E/OU=Example Team/CN=example.com" ./create-cert.sh 25 | ``` 26 | -------------------------------------------------------------------------------- /oauth2-password-provider/README.md: -------------------------------------------------------------------------------- 1 | # Example of Self Hosted OAuth2 Password Provider 2 | 3 | A simple example of a self hosted OAuth2 password provider. 4 | It can be used to authenticate users in own applications with password flow. 5 | 6 | ## Running the example 7 | 8 | - create a Python virtual environment 9 | 10 | ```bash 11 | python -m venv venv 12 | ``` 13 | 14 | - install dependencies 15 | 16 | ```bash 17 | pip install -r requirements.txt 18 | ``` 19 | 20 | - run the dev server 21 | 22 | ```bash 23 | python server.py 24 | ``` 25 | 26 | - look at [example.http](example.http) to find at the example requests 27 | 28 | ## Server has 4 endpoints 29 | 30 | - `/api/register` - register a new user 31 | - `/api/token` - get a new access token 32 | - `/api/refresh` - refresh an access token 33 | - `/api/revoke` - revoke an access token 34 | - `/api/anonymous` - get a resource without authentication 35 | - `/api/protected` - get a resource with authentication -------------------------------------------------------------------------------- /oidc/basic_google.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to configure an OpenID Connect integration with Google, obtaining 3 | only an id_token, exchanged with the client using a response cookie. 4 | """ 5 | import uvicorn 6 | from blacksheep.server.application import Application 7 | from blacksheep.server.authentication.oidc import OpenIDSettings, use_openid_connect 8 | 9 | from common.routes import register_routes 10 | 11 | app = Application(show_error_details=True) 12 | 13 | client_id = "349036756498-715barque0aq00qplb3fon9i6hig7ib9.apps.googleusercontent.com" 14 | 15 | # basic Google integration that handles only an id_token 16 | use_openid_connect( 17 | app, 18 | OpenIDSettings( 19 | authority="https://accounts.google.com", 20 | client_id=client_id, 21 | callback_path="/authorization-callback", 22 | ), 23 | ) 24 | 25 | register_routes(app) 26 | 27 | 28 | if __name__ == "__main__": 29 | uvicorn.run(app, host="127.0.0.1", port=5000, log_level="debug") 30 | -------------------------------------------------------------------------------- /piccolo-admin/README.md: -------------------------------------------------------------------------------- 1 | # Piccolo Admin example 2 | 3 | This folder contains an example showing how to use the `mount` feature to run 4 | a `Piccolo Admin` application in `BlackSheep`. 5 | 6 | For more information on the `mount` feature, refer to the [documentation](https://www.neoteroi.dev/blacksheep/mounting/). 7 | 8 | For more information on Piccolo Admin, refer to its [project in GitHub](https://github.com/piccolo-orm/piccolo_admin). 9 | 10 | ## Getting started 11 | 12 | 1. Create a Python virtual environment 13 | 2. Activate the virtual environment 14 | 3. Install dependencies in `requirements.txt` 15 | 4. Run, using the desired HTTP server (e.g. `uvicorn server:app --reload`) 16 | 17 | ```bash 18 | # create a Python virtual environment 19 | python -m venv venv 20 | 21 | # activate 22 | source venv/bin/activate # (Linux) 23 | 24 | venv\Scripts\activate # (Windows) 25 | 26 | # install dependencies 27 | pip install -r requirements.txt 28 | 29 | # run 30 | uvicorn server:app --reload 31 | ``` 32 | -------------------------------------------------------------------------------- /server-sent-events/README.md: -------------------------------------------------------------------------------- 1 | ## Server-Sent events example 2 | 3 | This example illustrates how to use built-in features for [Server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) (SSE). 4 | 5 | Note that, even though BlackSheep supports built-in features for SSE only since 6 | version 2.0.6, previous versions of the web framework also could support SSE, 7 | as they support response streaming. 8 | 9 | Running the example: 10 | 11 | 1. create a Python virtual environment 12 | 2. install dependencies 13 | 3. run with `uvicorn server:app` to test with `uvicorn`, or 14 | `hypercorn server:app` to test with `hypercorn` 15 | 4. open the page in a web browser, you should see the message on the page 16 | updated every second, using information from the server 17 | 18 | --- 19 | 20 | This example also shows how the `is_stopping` function can be used to detect 21 | when the application server is shutting down. 22 | 23 | ```python 24 | from blacksheep.server.application import is_stopping 25 | ``` 26 | -------------------------------------------------------------------------------- /jwt-validation/example.py: -------------------------------------------------------------------------------- 1 | from blacksheep.server.application import Application 2 | from blacksheep.server.authentication.jwt import JWTBearerAuthentication 3 | from blacksheep.server.authorization import auth 4 | from guardpost.common import AuthenticatedRequirement, Policy 5 | 6 | app = Application() 7 | 8 | 9 | app.use_authentication().add( 10 | JWTBearerAuthentication( 11 | authority="https://login.microsoftonline.com/robertoprevatogmail.onmicrosoft.com", 12 | valid_audiences=["104bca60-c5a7-4ab9-83e1-7b9c8dad71e2"], 13 | valid_issuers=[ 14 | "https://login.microsoftonline.com/b62b317a-19c2-40c0-8650-2d9672324ac4/v2.0" 15 | ], 16 | ) 17 | ) 18 | 19 | authorization = app.use_authorization() 20 | 21 | authorization += Policy("any_name", AuthenticatedRequirement()) 22 | 23 | get = app.router.get 24 | 25 | 26 | @get("/") 27 | def home(): 28 | return "Hello, World" 29 | 30 | 31 | @auth("any_name") 32 | @get("/api/message") 33 | def example(): 34 | return "This is only for authenticated users" 35 | -------------------------------------------------------------------------------- /server-sent-events/server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | from collections.abc import AsyncIterable 4 | 5 | from blacksheep import Application, Request, get 6 | from blacksheep.server.process import is_stopping 7 | from blacksheep.server.sse import ServerSentEvent 8 | 9 | app = Application(show_error_details=True) 10 | app.serve_files("static") 11 | 12 | 13 | # Enable the signal handler to detect when the application is stopping. 14 | os.environ["APP_SIGNAL_HANDLER"] = "1" 15 | 16 | 17 | @get("/events") 18 | async def events_handler(request: Request) -> AsyncIterable[ServerSentEvent]: 19 | i = 0 20 | 21 | while True: 22 | if await request.is_disconnected(): 23 | print("The request is disconnected!") 24 | break 25 | 26 | if is_stopping(): 27 | print("The application is stopping!") 28 | break 29 | 30 | i += 1 31 | yield ServerSentEvent({"message": f"Hello World {i}"}) 32 | 33 | try: 34 | await asyncio.sleep(1) 35 | except asyncio.exceptions.CancelledError: 36 | break 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Neoteroi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /max-body-size/README.md: -------------------------------------------------------------------------------- 1 | # Validating Max Body Size 2 | This example shows a way to validate the maximum body size using 3 | `request.stream`, and how to post a file to a BlackSheep server using the HTML5 4 | `fetch` API. 5 | 6 | ## Current limitations 7 | 8 | At the time of this writing, blacksheep does not support configuring a maximum body size 9 | globally, nor configuring a maximum body size for specific request handlers that would 10 | validate the request body size also when trying to read the whole request content as 11 | JSON, text, form data. 12 | 13 | The following methods: 14 | 15 | ```python 16 | text = await request.text() 17 | data = await request.json() 18 | data = await request.form() 19 | ``` 20 | 21 | all cause the whole request body to be read. 22 | 23 | ## Running the example 24 | 25 | - create a Python virtual environment 26 | - install dependencies 27 | - run the dev server `python main.py` 28 | - navigate to [http://localhost:44555](http://localhost:44555) 29 | - use the HTML page to select a file and upload it: only files smaller than 30 | ~1.5 MB are accepted by the server, and written to an `out` folder under 31 | `CWD` 32 | -------------------------------------------------------------------------------- /piccolo-admin/Piccolo-Admin-LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Daniel Townsend 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /websocket-chat/README.md: -------------------------------------------------------------------------------- 1 | # Chat app example 2 | 3 | This folder contains a simple chat application built using WebSocket and VueJS. 4 | You can use it as a starting point to build your own real-time application using 5 | WebSocket. 6 | 7 | Bear in mind, though, that this is merely an example. In the real world, you would 8 | probably like to use a message queue like Redis to broadcast messages to the clients 9 | and some persistent storage like PostgreSQL or MongoDB to store your messages, users, etc. 10 | 11 | ## Getting started 12 | 13 | 1. Create a Python virtual environment 14 | 2. Activate the virtual environment 15 | 3. Install dependencies in `requirements.txt` 16 | 4. Run the application using `uvicorn --reload server:app` 17 | 5. Navigate to `http://localhost:8000` in your browser and try sending 18 | some messages. You can open it in multiple tabs to simulate 19 | multiple clients connected. 20 | 21 | ```bash 22 | # create a Python virtual environment 23 | python -m venv venv 24 | 25 | # activate 26 | source venv/bin/activate # (Linux) 27 | 28 | venv\Scripts\activate # (Windows) 29 | 30 | # install dependencies 31 | pip install -r requirements.txt 32 | 33 | # run app 34 | uvicorn --reload server:app 35 | ``` -------------------------------------------------------------------------------- /proxy-1/flask_app/server.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example application to test a proxy implemented with BlackSheep. 3 | """ 4 | from essentials.folders import ensure_folder 5 | from flask import Flask, jsonify, request 6 | from markupsafe import escape 7 | 8 | # https://flask.palletsprojects.com/en/1.1.x/server/#server 9 | app = Flask(__name__, static_url_path="", static_folder="static") 10 | 11 | 12 | @app.route("/hello-world") 13 | def hello_world(): 14 | name = request.args.get("name", "World") 15 | return f"Hello, {escape(name)}!", 200, {"Content-Type": "text/plain"} 16 | 17 | 18 | # https://flask.palletsprojects.com/en/1.1.x/patterns/fileuploads/ 19 | @app.route("/upload", methods=["POST"]) 20 | def upload_files(): 21 | files = request.files 22 | 23 | assert bool(files) 24 | 25 | folder = "out" 26 | 27 | ensure_folder(folder) 28 | all_files = files.getlist("files") 29 | 30 | for part in all_files: 31 | part.save(f"./{folder}/{part.filename}") 32 | 33 | return jsonify( 34 | { 35 | "folder": folder, 36 | "data": request.form, 37 | "files": [{"name": file.filename} for file in all_files], 38 | } 39 | ) 40 | 41 | 42 | if __name__ == "__main__": 43 | app.run(host="localhost", port=44777, debug=True) 44 | -------------------------------------------------------------------------------- /oidc/scopes_okta.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to configure an OpenID Connect integration with Okta, obtaining 3 | an id_token, an access_token, and a refresh_token. The id_token is exchanged with the 4 | client using a response cookie (also used to authenticate users 5 | for following requests), while access token and the refresh token are not stored and 6 | can only be accessed using optional events. 7 | """ 8 | import uvicorn 9 | from blacksheep.server.application import Application 10 | from blacksheep.server.authentication.oidc import OpenIDSettings, use_openid_connect 11 | from dotenv import load_dotenv 12 | 13 | from common.routes import register_routes 14 | from common.secrets import Secrets 15 | 16 | load_dotenv() 17 | secrets = Secrets.from_env() 18 | app = Application(show_error_details=True) 19 | 20 | 21 | # Okta with custom scope 22 | use_openid_connect( 23 | app, 24 | OpenIDSettings( 25 | discovery_endpoint="https://dev-34685660.okta.com/oauth2/default/.well-known/oauth-authorization-server", 26 | client_id="0oa2gy88qiVyuOClI5d7", 27 | client_secret=secrets.okta_client_secret, 28 | callback_path="/authorization-code/callback", 29 | scope="openid read:todos", 30 | ), 31 | ) 32 | 33 | register_routes(app) 34 | 35 | 36 | if __name__ == "__main__": 37 | uvicorn.run(app, host="127.0.0.1", port=5000, log_level="debug") 38 | -------------------------------------------------------------------------------- /proxy-1/README.md: -------------------------------------------------------------------------------- 1 | # Example showing an HTTP Proxy implemented with BlackSheep 2 | 3 | Run the Flask application: 4 | 5 | ```bash 6 | python flask_app/server.py 7 | ``` 8 | 9 | Run the BlackSheep proxy application: 10 | 11 | ```bash 12 | python blacksheep_proxy/server.py 13 | ``` 14 | 15 | Open the file "example.html" and use its forms to test uploading to the Flask 16 | server directly (uploaded files should be written to the `out` folder), and 17 | test uploading to the BlackSheep proxy. The result should be the same. 18 | 19 | ## Note 20 | The example proxy in `blacksheep_proxy` handles memory in the proper way: 21 | 22 | - it reads input streams as chunks (never whole in memory) 23 | - it reads response streams from the back-end in chunks (never whole in memory) 24 | 25 | It always sends response contents backs using `Transfer-Encoding: chunked`, 26 | which might or might not be desirable, but ensures memory is handled 27 | efficiently. 28 | 29 | ## Other example 30 | `other-example.html` is similar to `example.html`, with the exception that both 31 | the back-end app and the proxy server are implemented using BlackSheep. 32 | 33 | Run the frst BlackSheep application: 34 | 35 | ```bash 36 | python blacksheep_app/server.py 37 | ``` 38 | 39 | Run the BlackSheep proxy application: 40 | 41 | ```bash 42 | python blacksheep_proxy/server.py 43 | ``` 44 | 45 | Open `other-example.html` in a browser. 46 | -------------------------------------------------------------------------------- /oidc/scopes_auth0.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to configure an OpenID Connect integration with Auth0, obtaining 3 | an id_token, an access_token, and a refresh_token. The id_token is exchanged with the 4 | client using a response cookie (also used to authenticate users 5 | for following requests), while access token and the refresh token are not stored and 6 | can only be accessed using optional events. 7 | """ 8 | import uvicorn 9 | from blacksheep.server.application import Application 10 | from blacksheep.server.authentication.oidc import OpenIDSettings, use_openid_connect 11 | from dotenv import load_dotenv 12 | 13 | from common.routes import register_routes 14 | from common.secrets import Secrets 15 | 16 | load_dotenv() 17 | secrets = Secrets.from_env() 18 | app = Application(show_error_details=True) 19 | 20 | 21 | # Auth0 with custom scope 22 | use_openid_connect( 23 | app, 24 | OpenIDSettings( 25 | authority="https://neoteroi.eu.auth0.com", 26 | audience="http://localhost:5000/api/todos", 27 | client_id="OOGPl4dgG7qKsm2IOWq72QhXV4wsLhbQ", 28 | client_secret=secrets.auth0_client_secret, 29 | callback_path="/signin-oidc", 30 | scope="openid profile read:todos", 31 | error_redirect_path="/sign-in-error", 32 | ), 33 | ) 34 | 35 | register_routes(app) 36 | 37 | 38 | if __name__ == "__main__": 39 | uvicorn.run(app, host="127.0.0.1", port=5000, log_level="debug") 40 | -------------------------------------------------------------------------------- /oauth2-password-provider/src/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from blacksheep import Application 4 | from blacksheep.server.responses import json 5 | from blacksheep.server.authorization import allow_anonymous, auth 6 | from blacksheep.server.bindings import FromJSON 7 | from .db import User 8 | 9 | 10 | from .password_auth import HMACAlgorithm, OAuth2PasswordSettings, use_oauth2_password 11 | from .user import UserDAL 12 | 13 | app = Application() 14 | use_oauth2_password( 15 | app, 16 | OAuth2PasswordSettings( 17 | secret="secret", 18 | algorithm=HMACAlgorithm("HS256"), 19 | token_path="/api/token", 20 | refresh_path="/api/refresh", 21 | revoke_path="/api/revoke", 22 | ), 23 | ) 24 | 25 | 26 | @allow_anonymous() 27 | @app.router.get("/api/anonymous") 28 | def anonymous(): 29 | return json({"message": "Hello, anonymous!"}) 30 | 31 | 32 | @allow_anonymous() 33 | @app.router.post("/api/register") 34 | async def register( 35 | user_registration: FromJSON[User], 36 | ): 37 | """Register user.""" 38 | username = user_registration.value.username 39 | password = user_registration.value.password 40 | 41 | user_dal = UserDAL() 42 | user = user_dal.register(username, password) 43 | 44 | return json(data=user) 45 | 46 | 47 | @auth("authenticated") 48 | @app.router.get("/api/protected") 49 | async def protected(): 50 | """Protected endpoint.""" 51 | return json({"message": "Hello, authenticated user!"}) 52 | -------------------------------------------------------------------------------- /piccolo-admin/server.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | from blacksheep.messages import Response 3 | from blacksheep.server import Application 4 | from blacksheep.server.responses import html 5 | 6 | from piccoloexample import APP, create_schema, populate_data, set_engine 7 | 8 | app = Application() 9 | 10 | 11 | @app.route("/") 12 | def home() -> Response: 13 | return html( 14 | """ 15 | 16 | 17 | 18 | 19 | 20 | BlackSheep - mount example 21 | 22 | 23 |

BlackSheep - mount example

24 |
    25 |
  1. Navigate to the Piccolo Admin login page
  2. 26 |
  3. Sign in using (username: piccolo, password: piccolo123)
  4. 27 |
  5. Read more about Piccolo Admin here
  6. 28 |
29 | 30 | 31 | """ 32 | ) 33 | 34 | 35 | app.mount("/admin", APP) 36 | 37 | 38 | def configure(): 39 | engine = "sqlite" 40 | persist = False 41 | set_engine("sqlite") 42 | 43 | create_schema(persist=persist) 44 | 45 | if not persist: 46 | populate_data(inflate=1, engine=engine) 47 | 48 | 49 | @app.on_start 50 | async def configure_sqlite(_): 51 | configure() 52 | 53 | 54 | if __name__ == "__main__": 55 | 56 | configure() 57 | uvicorn.run(app, host="127.0.0.1", port=44777, log_level="debug") 58 | -------------------------------------------------------------------------------- /proxy-2/blacksheep_app/server.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example application to test a proxy implemented with BlackSheep. 3 | This application handles requests from the proxy (defined in blacksheep_proxy). 4 | """ 5 | from pathlib import Path 6 | 7 | import uvicorn 8 | from blacksheep import Application, Request, json 9 | from essentials.folders import ensure_folder 10 | 11 | app = Application() 12 | 13 | app.serve_files("./blacksheep_app/static") 14 | 15 | 16 | @app.route("/hello-world") 17 | def hello_world(): 18 | return "Hello, World!" 19 | 20 | 21 | @app.router.post("/upload") 22 | async def upload_files(request: Request): 23 | files = await request.files() 24 | folder = "out" 25 | 26 | all_files = [] 27 | ensure_folder(folder) 28 | 29 | for part in files: 30 | assert part.file_name is not None 31 | with open(Path(folder) / part.file_name.decode(), mode="wb") as output_file: 32 | output_file.write(part.data) 33 | all_files.append(part.file_name.decode()) 34 | 35 | data = await request.form() 36 | 37 | return json( 38 | { 39 | "folder": folder, 40 | "data": { 41 | "fname": data["fname"], # type: ignore 42 | "lname": data["lname"], # type: ignore 43 | }, 44 | "files": [{"name": file} for file in all_files], 45 | } 46 | ) 47 | 48 | 49 | if __name__ == "__main__": 50 | uvicorn.run(app, host="localhost", port=44777, lifespan="on") # , http="h11" 51 | -------------------------------------------------------------------------------- /aad-machine-to-machine/client_http_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module shows an example of how the client credentials flow with secret can be used 3 | directly using an HTTP Client, for Azure Active Directory. 4 | This should be used only in those scenario when legacy applications would not support 5 | MSAL for Python. 6 | """ 7 | import os 8 | 9 | import httpx 10 | from dotenv import load_dotenv 11 | 12 | # read .env file into environment variables 13 | load_dotenv() 14 | 15 | 16 | def ensure_success(response: httpx.Response) -> None: 17 | if response.status_code < 200 or response.status_code > 399: 18 | body = response.text 19 | raise ValueError( 20 | f"The response status does not indicate success {response.status_code}; " 21 | f"Response body: {body}" 22 | ) 23 | 24 | 25 | def get_access_token() -> str: 26 | response = httpx.post( 27 | os.environ["AAD_AUTHORITY"].rstrip("/") + "/oauth2/v2.0/token", 28 | data={ 29 | "grant_type": "client_credentials", 30 | "client_id": os.environ["APP_CLIENT_ID"], 31 | "client_secret": os.environ["APP_CLIENT_SECRET"], 32 | "scope": os.environ["APP_CLIENT_SCOPE"], 33 | }, 34 | ) 35 | ensure_success(response) 36 | data = response.json() 37 | assert "access_token" in data, "The response body must include an access token" 38 | return data["access_token"] 39 | 40 | 41 | if __name__ == "__main__": 42 | print("Access token: " + get_access_token()) 43 | -------------------------------------------------------------------------------- /testing-api/app/routes/todos.py: -------------------------------------------------------------------------------- 1 | # /app/routes/todos.py 2 | 3 | from typing import Dict, List, Optional 4 | 5 | from blacksheep.server.responses import not_found 6 | from domain import CreateToDoInput, ToDo 7 | 8 | from .router import delete, get, post 9 | 10 | _MOCKED: Dict[int, ToDo] = { 11 | 1: ToDo( 12 | id=1, 13 | title="BlackSheep Documentation", 14 | description="Update the documentation with information about the new features.", 15 | ), 16 | 2: ToDo( 17 | id=2, 18 | title="Transfer the documentation", 19 | description="Transfer the documentation from Azure DevOps to GitHub.", 20 | ), 21 | 3: ToDo( 22 | id=3, 23 | title="Mow the grass", 24 | description="Like in title.", 25 | ), 26 | } 27 | 28 | 29 | @get("/api/todos") 30 | async def get_todos() -> List[ToDo]: 31 | return list(_MOCKED.values()) 32 | 33 | 34 | @get("/api/todos/{todo_id}") 35 | async def get_todo(todo_id: int) -> Optional[ToDo]: 36 | try: 37 | return _MOCKED[todo_id] 38 | except KeyError: 39 | return not_found() 40 | 41 | 42 | @post("/api/todos") 43 | async def create_todo(data: CreateToDoInput) -> ToDo: 44 | item = ToDo(id=len(_MOCKED) + 1, title=data.title, description=data.description) 45 | _MOCKED[item.id] = item 46 | return item 47 | 48 | 49 | @delete("/api/todos/{todo_id}") 50 | async def delete_todo(todo_id: int) -> None: 51 | try: 52 | del _MOCKED[todo_id] 53 | except KeyError: 54 | pass 55 | -------------------------------------------------------------------------------- /testing-api/tests/test_todos_api.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | import pytest 4 | from blacksheep.contents import Content 5 | from blacksheep.testing import TestClient 6 | from domain import CreateToDoInput, ToDo 7 | from essentials.json import dumps 8 | 9 | 10 | def json_content(data: Any) -> Content: 11 | return Content( 12 | b"application/json", 13 | dumps(data, separators=(",", ":")).encode("utf8"), 14 | ) 15 | 16 | 17 | @pytest.mark.asyncio 18 | async def test_create_and_get_todo(test_client: TestClient) -> None: 19 | 20 | create_input = CreateToDoInput( 21 | title="Update documentation", 22 | description="Update blacksheep's documentation to describe all new features.", 23 | ) 24 | 25 | response = await test_client.post( 26 | "/api/todos", 27 | content=json_content(create_input), 28 | ) 29 | 30 | assert response is not None 31 | 32 | data = await response.json() 33 | 34 | assert data is not None 35 | assert "id" in data 36 | 37 | todo_id = data["id"] 38 | response = await test_client.get(f"/api/todos/{todo_id}") 39 | 40 | assert response is not None 41 | data = await response.json() 42 | 43 | assert data is not None 44 | 45 | todo = ToDo(**data) 46 | 47 | assert todo.title == create_input.title 48 | assert todo.description == create_input.description 49 | 50 | 51 | @pytest.mark.asyncio 52 | async def test_get_not_existing_todo_returns_404(test_client: TestClient) -> None: 53 | 54 | response = await test_client.get(f"/api/todos/-1") 55 | 56 | assert response is not None 57 | assert response.status == 404 58 | -------------------------------------------------------------------------------- /testing-api/tests/test_uvicorn_int.py: -------------------------------------------------------------------------------- 1 | import os 2 | from multiprocessing import Process 3 | from time import sleep 4 | from urllib.parse import urljoin 5 | 6 | import pytest 7 | import requests 8 | import uvicorn 9 | from server import app 10 | 11 | 12 | class ClientSession(requests.Session): 13 | def __init__(self, base_url): 14 | self.base_url = base_url 15 | super().__init__() 16 | 17 | def request(self, method, url, *args, **kwargs): 18 | return super().request(method, urljoin(self.base_url, url), *args, **kwargs) 19 | 20 | 21 | def get_sleep_time(): 22 | # when starting a server process, 23 | # a longer sleep time is necessary on Windows 24 | if os.name == "nt": 25 | return 1.5 26 | return 0.5 27 | 28 | 29 | server_host = "127.0.0.1" 30 | server_port = 44555 31 | 32 | 33 | @pytest.fixture(scope="session") 34 | def client_session(): 35 | return ClientSession(f"http://{server_host}:{server_port}") 36 | 37 | 38 | def _start_server(): 39 | uvicorn.run(app, host=server_host, port=server_port, log_level="debug") 40 | 41 | 42 | @pytest.fixture(scope="session", autouse=True) 43 | def server(): 44 | server_process = Process(target=_start_server) 45 | server_process.start() 46 | sleep(get_sleep_time()) 47 | 48 | if not server_process.is_alive(): 49 | raise TypeError("The server process did not start!") 50 | 51 | yield 1 52 | 53 | sleep(1.2) 54 | server_process.terminate() 55 | 56 | 57 | @pytest.mark.asyncio 58 | async def test_get(client_session): 59 | response = client_session.get("/api/todos/1") 60 | 61 | assert response.status_code == 200 62 | -------------------------------------------------------------------------------- /otel/grafanaexample.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import os 4 | 5 | from blacksheep import Application, HTTPException, Response, json, text 6 | from dotenv import load_dotenv 7 | 8 | from otel import logcall 9 | from otel.otlp import use_open_telemetry_otlp 10 | 11 | load_dotenv() 12 | 13 | os.environ["OTEL_RESOURCE_ATTRIBUTES"] = ( 14 | "service.name=learning-app,service.namespace=learning,deployment.environment=local" 15 | ) 16 | 17 | app = Application() 18 | 19 | use_open_telemetry_otlp(app) 20 | 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | # The following is to illustrate support for controlling the request-response cycle 26 | # using exceptions and exception handlers 27 | class CustomException(HTTPException): 28 | def __init__(self): 29 | super().__init__(400, "Something wrong!") 30 | 31 | 32 | @app.exception_handler(CustomException) 33 | async def handler_example(app, request, exc: CustomException): 34 | return json({"message": "Oh, no! " + str(exc)}, 400) 35 | 36 | 37 | @logcall("Example") 38 | async def dependency_example(): 39 | await asyncio.sleep(0.1) 40 | 41 | 42 | @app.router.get("/") 43 | async def home(request) -> Response: 44 | # logger.warning appear in the traces table 45 | logger.warning("Example warning") 46 | await dependency_example() 47 | return text("Hello, traced BlackSheep!") 48 | 49 | 50 | @app.router.get("/{name}") 51 | async def greetings(name: str) -> Response: 52 | return text(f"Hello, {name}!") 53 | 54 | 55 | @app.router.get("/bad-request") 56 | async def bad_request(): 57 | raise CustomException() 58 | 59 | 60 | @app.router.get("/crash") 61 | async def crash_test() -> Response: 62 | raise RuntimeError("Crash test") 63 | 64 | 65 | if __name__ == "__main__": 66 | import uvicorn 67 | 68 | uvicorn.run(app, port=44777) 69 | -------------------------------------------------------------------------------- /long-polling/README.md: -------------------------------------------------------------------------------- 1 | # Long-polling example 2 | 3 | This example shows an example of long-polling implemented with BlackSheep. 4 | 5 | The JavaScript and front-end part of this example was adopted from: 6 | [https://javascript.info/long-polling](https://javascript.info/long-polling). 7 | 8 | ## Trying the example 9 | 10 | 1. Create a Python virtual environment 11 | 2. Install depedencies (`pip install blacksheep uvicorn`) 12 | 3. Run the server with `uvicorn server:app` 13 | 4. Open the web site in several browser tabs to see the effect of long-polling 14 | 5. Submit messages in a browser tab: see how messages are immediately visible 15 | in all tabs, thanks to long-polling 16 | 6. Read the source code in `server.py` to see how long-polling is achieved using 17 | `asyncio.Queue`, and how `signal.getsignal(signal.SIGINT)` is used in the 18 | `@app.on_start` event handler. 19 | 7. The `/subscribe` method is used to subscribe for long-polling. 20 | 8. The `/publish` method is used to publish a message to all subscribers. 21 | 22 | ## How to test a disconnection 23 | 24 | To test a client that disconnects, refresh a browser tab, then send a message from an active tab. 25 | The console should display messages like this one: 26 | 27 | ```bash 28 | INFO: 127.0.0.1:40834 - "POST /publish HTTP/1.1" 200 OK 29 | 🔥🔥🔥 Request is disconnected! 30 | 🔥🔥🔥 Request is disconnected! 31 | 🔥🔥🔥 Request is disconnected! 32 | 🔥🔥🔥 Request is disconnected! 33 | 🔥🔥🔥 Request is disconnected! 34 | INFO: 127.0.0.1:40832 - "GET /subscribe?random=0.16177484986012614 HTTP/1.1" 200 OK 35 | INFO: 127.0.0.1:40882 - "GET /subscribe?random=0.08956117949704501 HTTP/1.1" 200 OK 36 | INFO: 127.0.0.1:40882 - "GET /subscribe?random=0.08956117949704501 HTTP/1.1" 200 OK 37 | INFO: 127.0.0.1:40832 - "GET /subscribe?random=0.16177484986012614 HTTP/1.1" 200 OK 38 | ``` 39 | -------------------------------------------------------------------------------- /oidc/scopes_storage_aad.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to configure an OpenID Connect integration having tokens 3 | exchanged with the client using the HTML5 Storage API, instead of response cookies. 4 | This scenario enables better reusability of web APIs. 5 | See how the id_token is used in ./static/index.html to authenticate following requests 6 | ('Authorization: Bearer ***' headers), and how the refresh token endpoint can be used 7 | to obtain fresh tokens. 8 | """ 9 | import uvicorn 10 | from blacksheep.server.application import Application 11 | from blacksheep.server.authentication.jwt import JWTBearerAuthentication 12 | from blacksheep.server.authentication.oidc import ( 13 | JWTOpenIDTokensHandler, 14 | OpenIDSettings, 15 | use_openid_connect, 16 | ) 17 | from dotenv import load_dotenv 18 | 19 | from common.routes import register_routes 20 | from common.secrets import Secrets 21 | 22 | load_dotenv() 23 | secrets = Secrets.from_env() 24 | app = Application(show_error_details=True) 25 | 26 | 27 | AUTHORITY = ( 28 | "https://login.microsoftonline.com/b62b317a-19c2-40c0-8650-2d9672324ac4/v2.0" 29 | ) 30 | CLIENT_ID = "499adb65-5e26-459e-bc35-b3e1b5f71a9d" 31 | use_openid_connect( 32 | app, 33 | OpenIDSettings( 34 | authority=AUTHORITY, 35 | client_id=CLIENT_ID, 36 | client_secret=secrets.aad_client_secret, 37 | scope=( 38 | "openid profile offline_access email " 39 | "api://65d21481-4f1a-4731-9508-ad965cb4d59f/example" 40 | ), 41 | ), 42 | auth_handler=JWTOpenIDTokensHandler( 43 | JWTBearerAuthentication( 44 | authority=AUTHORITY, 45 | valid_audiences=[CLIENT_ID], 46 | ), 47 | ), 48 | ) 49 | 50 | register_routes(app, static_home=True) 51 | 52 | 53 | if __name__ == "__main__": 54 | uvicorn.run(app, host="127.0.0.1", port=5000, log_level="debug") 55 | -------------------------------------------------------------------------------- /oidc/basic_storage_aad.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to configure an OpenID Connect integration to obtain only an 3 | id_token, and having it exchanged with the client using the HTML5 Storage API, instead 4 | of a response cookie. This scenario enables better reusability of web APIs. 5 | See how the id_token is used in ./static/index.html to authenticate following requests 6 | ('Authorization: Bearer ***' headers). 7 | """ 8 | import uvicorn 9 | from blacksheep.server.application import Application 10 | from blacksheep.server.authentication.jwt import JWTBearerAuthentication 11 | from blacksheep.server.authentication.oidc import ( 12 | JWTOpenIDTokensHandler, 13 | OpenIDSettings, 14 | use_openid_connect, 15 | ) 16 | 17 | from common.routes import register_routes 18 | 19 | app = Application(show_error_details=True) 20 | 21 | # Basic AAD integration that handles only an id_token. 22 | # In this case, the back-end authenticates users using id_tokens. 23 | # Another possible scenario is to obtain both an id_token and an access_token in the 24 | # UI (front-end and back-end are represented by distinct app registrations in the IDP); 25 | # thus requiring access tokens for an API (this is the generally recommended approach). 26 | AUTHORITY = ( 27 | "https://login.microsoftonline.com/b62b317a-19c2-40c0-8650-2d9672324ac4/v2.0" 28 | ) 29 | CLIENT_ID = "499adb65-5e26-459e-bc35-b3e1b5f71a9d" 30 | use_openid_connect( 31 | app, 32 | OpenIDSettings( 33 | authority=AUTHORITY, 34 | client_id=CLIENT_ID, 35 | ), 36 | auth_handler=JWTOpenIDTokensHandler( 37 | JWTBearerAuthentication( 38 | authority=AUTHORITY, 39 | valid_audiences=[CLIENT_ID], 40 | ) 41 | ), 42 | ) 43 | 44 | register_routes(app, static_home=True) 45 | 46 | 47 | if __name__ == "__main__": 48 | uvicorn.run(app, host="127.0.0.1", port=5000, log_level="debug") 49 | -------------------------------------------------------------------------------- /oidc/basic_storage_google.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to configure an OpenID Connect integration to obtain only an 3 | id_token, and having it exchanged with the client using the HTML5 Storage API, instead 4 | of a response cookie. This scenario enables better reusability of web APIs. 5 | See how the id_token is used in ./static/index.html to authenticate following requests 6 | ('Authorization: Bearer ***' headers). 7 | """ 8 | import uvicorn 9 | from blacksheep.server.application import Application 10 | from blacksheep.server.authentication.jwt import JWTBearerAuthentication 11 | from blacksheep.server.authentication.oidc import ( 12 | JWTOpenIDTokensHandler, 13 | OpenIDSettings, 14 | use_openid_connect, 15 | ) 16 | 17 | from common.routes import register_routes 18 | 19 | app = Application(show_error_details=True) 20 | 21 | # Basic AAD integration that handles only an id_token. 22 | # In this case, the back-end authenticates users using id_tokens. 23 | # Another possible scenario is to obtain both an id_token and an access_token in the 24 | # UI (front-end and back-end are represented by distinct app registrations in the IDP); 25 | # thus requiring access tokens for an API (this is the generally recommended approach). 26 | AUTHORITY = "https://accounts.google.com" 27 | CLIENT_ID = "349036756498-715barque0aq00qplb3fon9i6hig7ib9.apps.googleusercontent.com" 28 | 29 | use_openid_connect( 30 | app, 31 | OpenIDSettings( 32 | authority=AUTHORITY, 33 | client_id=CLIENT_ID, 34 | callback_path="/authorization-callback", 35 | ), 36 | auth_handler=JWTOpenIDTokensHandler( 37 | JWTBearerAuthentication( 38 | authority=AUTHORITY, 39 | valid_audiences=[CLIENT_ID], 40 | ) 41 | ), 42 | ) 43 | 44 | register_routes(app, static_home=True) 45 | 46 | 47 | if __name__ == "__main__": 48 | uvicorn.run(app, host="127.0.0.1", port=5000, log_level="debug") 49 | -------------------------------------------------------------------------------- /otel/azureexample.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import os 4 | 5 | from blacksheep import Application, HTTPException, Response, json, text 6 | from dotenv import load_dotenv 7 | 8 | from otel.azure import log_dependency as logcall, use_application_insights 9 | 10 | load_dotenv() 11 | 12 | os.environ["OTEL_RESOURCE_ATTRIBUTES"] = ( 13 | "service.name=learning-app2,service.namespace=learning2,deployment.environment=local" 14 | ) 15 | 16 | app = Application() 17 | 18 | connection_string = os.getenv("APP_INSIGHTS_CONNECTION_STRING") 19 | 20 | if connection_string is None: 21 | raise ValueError("Missing env variable: APP_INSIGHTS_CONNECTION_STRING") 22 | 23 | use_application_insights(app, connection_string) 24 | 25 | 26 | logger = logging.getLogger(__name__) 27 | 28 | 29 | class CustomException(HTTPException): 30 | def __init__(self): 31 | super().__init__(400, "Something wrong!") 32 | 33 | 34 | @app.exception_handler(CustomException) 35 | async def handler_example(app, request, exc: CustomException): 36 | return json({"message": "Oh, no! " + str(exc)}, 400) 37 | 38 | 39 | @logcall("Example") 40 | async def dependency_example(): 41 | await asyncio.sleep(0.1) 42 | 43 | 44 | @app.router.get("/") 45 | async def home(request) -> Response: 46 | # logger.warning appear in the traces table 47 | logger.warning("Example warning") 48 | await dependency_example() 49 | return text("Hello, traced BlackSheep!") 50 | 51 | 52 | @app.router.get("/{name}") 53 | async def greetings(name: str) -> Response: 54 | return text(f"Hello, {name}!") 55 | 56 | 57 | @app.router.get("/bad-request") 58 | async def bad_request(): 59 | raise CustomException() 60 | 61 | 62 | @app.router.get("/crash") 63 | async def crash_test() -> Response: 64 | raise RuntimeError("Crash test") 65 | 66 | 67 | if __name__ == "__main__": 68 | import uvicorn 69 | 70 | uvicorn.run(app, port=44777) 71 | -------------------------------------------------------------------------------- /dependency-injector/main.py: -------------------------------------------------------------------------------- 1 | from blacksheep import Application, get 2 | from blacksheep.server.controllers import Controller 3 | from dependency_injector import containers, providers 4 | 5 | from app.di import DependencyInjectorConnector 6 | 7 | 8 | class APIClient: ... 9 | 10 | 11 | class SomeService: 12 | 13 | def __init__(self, api_client: APIClient) -> None: 14 | self.api_client = api_client 15 | 16 | 17 | class AnotherService: ... 18 | 19 | 20 | # Define the Dependency Injector container 21 | class AppContainer(containers.DeclarativeContainer): 22 | APIClient = providers.Singleton(APIClient) 23 | SomeService = providers.Factory(SomeService, api_client=APIClient) 24 | AnotherService = providers.Factory(AnotherService) 25 | 26 | 27 | # Create the container instance 28 | container = AppContainer() 29 | 30 | 31 | app = Application( 32 | services=DependencyInjectorConnector(container), show_error_details=True 33 | ) 34 | 35 | 36 | @get("/") 37 | def home(service: SomeService): 38 | # DependencyInjector resolved the dependencies 39 | assert isinstance(service, SomeService) 40 | assert isinstance(service.api_client, APIClient) 41 | return id(service) 42 | 43 | 44 | class TestController(Controller): 45 | 46 | def __init__(self, another_dep: AnotherService) -> None: 47 | super().__init__() 48 | self._another_dep = ( 49 | another_dep # another_dep is resolved by Dependency Injector 50 | ) 51 | 52 | @app.controllers_router.get("/controller-test") 53 | def controller_test(self, service: SomeService): 54 | # DependencyInjector resolved the dependencies 55 | assert isinstance(self._another_dep, AnotherService) 56 | 57 | assert isinstance(service, SomeService) 58 | assert isinstance(service.api_client, APIClient) 59 | return id(service) 60 | 61 | 62 | if __name__ == "__main__": 63 | import uvicorn 64 | 65 | uvicorn.run(app, port=44777) 66 | -------------------------------------------------------------------------------- /long-polling/static/browser.js: -------------------------------------------------------------------------------- 1 | // Sending messages, a simple POST 2 | function PublishForm(form, url) { 3 | 4 | function sendMessage(message) { 5 | fetch(url, { 6 | method: 'POST', 7 | headers: { 8 | 'Content-Type': 'application/json' 9 | }, 10 | body: JSON.stringify({ text: message }) 11 | }); 12 | } 13 | 14 | form.onsubmit = function() { 15 | let message = form.message.value; 16 | if (message) { 17 | form.message.value = ''; 18 | sendMessage(message); 19 | } 20 | return false; 21 | }; 22 | } 23 | 24 | // Receiving messages with long polling 25 | function SubscribePane(elem, url) { 26 | 27 | function showMessage(message) { 28 | let messageElem = document.createElement('div'); 29 | messageElem.append(message); 30 | elem.append(messageElem); 31 | } 32 | 33 | async function subscribe() { 34 | let response = await fetch(url).catch(() => { 35 | // This can happen if the server restarts, 36 | // we need to try again polling 37 | setTimeout(() => { 38 | subscribe(); 39 | }, 5000); 40 | return; 41 | }); 42 | 43 | if (response.status == 502) { 44 | // Connection timeout 45 | // happens when the connection was pending for too long 46 | // let's reconnect 47 | await subscribe(); 48 | } else if (response.status != 200) { 49 | // Show Error 50 | showMessage(response.statusText); 51 | // Reconnect in one second 52 | await new Promise(resolve => setTimeout(resolve, 1000)); 53 | await subscribe(); 54 | } else { 55 | // Got message 56 | let message = await response.text(); 57 | showMessage(message); 58 | await subscribe(); 59 | } 60 | } 61 | 62 | subscribe(); 63 | } 64 | -------------------------------------------------------------------------------- /otel/requirements.txt: -------------------------------------------------------------------------------- 1 | azure-core==1.34.0 2 | azure-identity==1.23.0 3 | azure-monitor-opentelemetry-exporter==1.0.0b37 4 | blacksheep==2.3.1 5 | certifi==2025.4.26 6 | cffi==1.17.1 7 | charset-normalizer==3.4.2 8 | click==8.2.1 9 | cryptography==45.0.4 10 | essentials==1.1.6 11 | essentials-openapi==1.2.0 12 | fixedint==0.1.6 13 | flake8==7.2.0 14 | googleapis-common-protos==1.70.0 15 | grpcio==1.73.0 16 | guardpost==1.0.2 17 | h11==0.16.0 18 | idna==3.10 19 | importlib_metadata==8.7.0 20 | isodate==0.7.2 21 | itsdangerous==2.2.0 22 | MarkupSafe==3.0.2 23 | mccabe==0.7.0 24 | msal==1.32.3 25 | msal-extensions==1.3.1 26 | msrest==0.7.1 27 | oauthlib==3.2.2 28 | opentelemetry-api==1.34.1 29 | opentelemetry-distro==0.55b1 30 | opentelemetry-exporter-otlp==1.34.1 31 | opentelemetry-exporter-otlp-proto-common==1.34.1 32 | opentelemetry-exporter-otlp-proto-grpc==1.34.1 33 | opentelemetry-exporter-otlp-proto-http==1.34.1 34 | opentelemetry-instrumentation==0.55b1 35 | opentelemetry-instrumentation-asyncio==0.55b1 36 | opentelemetry-instrumentation-dbapi==0.55b1 37 | opentelemetry-instrumentation-grpc==0.55b1 38 | opentelemetry-instrumentation-logging==0.55b1 39 | opentelemetry-instrumentation-requests==0.55b1 40 | opentelemetry-instrumentation-sqlite3==0.55b1 41 | opentelemetry-instrumentation-threading==0.55b1 42 | opentelemetry-instrumentation-urllib==0.55b1 43 | opentelemetry-instrumentation-urllib3==0.55b1 44 | opentelemetry-instrumentation-wsgi==0.55b1 45 | opentelemetry-proto==1.34.1 46 | opentelemetry-sdk==1.34.1 47 | opentelemetry-semantic-conventions==0.55b1 48 | opentelemetry-util-http==0.55b1 49 | packaging==25.0 50 | protobuf==5.29.5 51 | psutil==7.0.0 52 | pycodestyle==2.13.0 53 | pycparser==2.22 54 | pyflakes==3.3.2 55 | PyJWT==2.10.1 56 | python-dateutil==2.9.0.post0 57 | python-dotenv==1.1.0 58 | PyYAML==6.0.2 59 | requests==2.32.4 60 | requests-oauthlib==2.0.0 61 | rodi==2.0.8 62 | six==1.17.0 63 | typing_extensions==4.14.0 64 | urllib3==2.4.0 65 | uvicorn==0.34.3 66 | wrapt==1.17.2 67 | zipp==3.23.0 68 | -------------------------------------------------------------------------------- /aad-machine-to-machine/server.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from blacksheep.server.application import Application 5 | from blacksheep.server.authentication.jwt import JWTBearerAuthentication 6 | from blacksheep.server.responses import html 7 | from dotenv import load_dotenv 8 | from guardpost.authentication import Identity 9 | from guardpost.authorization import Policy 10 | from guardpost.common import AuthenticatedRequirement 11 | 12 | # read .env file into environment variables 13 | load_dotenv() 14 | 15 | app = Application() 16 | 17 | aad_authority = os.environ["API_ISSUER"] 18 | api_audience = os.environ["API_AUDIENCE"] 19 | 20 | 21 | # configure the application to support authentication using JWT access tokens obtained 22 | # from "Authorization: Bearer {...}" request headers; 23 | # access tokens are validated using OpenID Connect configuration from the configured 24 | # authority 25 | app.use_authentication().add( 26 | JWTBearerAuthentication( 27 | authority=aad_authority, 28 | valid_audiences=[api_audience], 29 | ) 30 | ) 31 | 32 | # configure authorization with a default policy that requires an authenticated user for 33 | # all endpoints, except when request handlers are explicitly decorated by 34 | # @allow_anonymous 35 | app.use_authorization().with_default_policy( 36 | Policy("authenticated", AuthenticatedRequirement()) 37 | ) 38 | 39 | get = app.router.get 40 | 41 | 42 | @get("/") 43 | def home(user: Identity): 44 | assert user.is_authenticated() 45 | 46 | return html( 47 | f""" 48 | 49 | 50 | 51 | 57 | 58 | 59 |

Welcome! These are your claims:

60 |
{json.dumps(user.claims, ensure_ascii=False, indent=4)}
61 | 62 | 63 | """ 64 | ) 65 | 66 | 67 | if __name__ == "__main__": 68 | import uvicorn 69 | 70 | uvicorn.run(app, host="127.0.0.1", port=5000, log_level="debug") 71 | -------------------------------------------------------------------------------- /otel/otel/otlp.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides integration for OpenTelemetry using OTLP (OpenTelemetry Protocol) exporters 3 | for logging and tracing in BlackSheep applications. 4 | 5 | It defines a helper function to configure OpenTelemetry with OTLPLogExporter and OTLPSpanExporter, 6 | ensuring that all required OTLP-related environment variables are set before initialization. 7 | 8 | Install: 9 | pip install opentelemetry-distro opentelemetry-exporter-otlp 10 | opentelemetry-bootstrap --action=install 11 | 12 | Usage: 13 | from otel.otlp import use_open_telemetry_otlp 14 | 15 | app = Application() 16 | use_open_telemetry_otlp(app) 17 | """ 18 | 19 | import os 20 | 21 | from blacksheep import Application 22 | from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter 23 | from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter 24 | 25 | from . import use_open_telemetry 26 | 27 | 28 | __all__ = ["use_open_telemetry_otlp"] 29 | 30 | 31 | def use_open_telemetry_otlp(app: Application): 32 | """ 33 | Configures OpenTelemetry for a BlackSheep application using OTLP exporters. 34 | 35 | This function checks for required OTLP-related environment variables and sets up 36 | OpenTelemetry logging and tracing using OTLPLogExporter and OTLPSpanExporter. 37 | 38 | Args: 39 | app: The BlackSheep Application instance. 40 | 41 | Raises: 42 | ValueError: If any required OTLP environment variables are missing. 43 | """ 44 | expected_vars = [ 45 | "OTEL_RESOURCE_ATTRIBUTES", 46 | "OTEL_EXPORTER_OTLP_ENDPOINT", 47 | "OTEL_EXPORTER_OTLP_HEADERS", 48 | "OTEL_EXPORTER_OTLP_PROTOCOL", 49 | ] 50 | missing_vars = [var for var in expected_vars if os.environ.get(var) is None] 51 | if missing_vars: 52 | raise ValueError(f"Missing env variables: {', '.join(missing_vars)}") 53 | 54 | # Uses environment variables for configuration 55 | use_open_telemetry( 56 | app, 57 | OTLPLogExporter(), 58 | OTLPSpanExporter(), 59 | ) 60 | -------------------------------------------------------------------------------- /aad-machine-to-machine/certs/create-cert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Use this bash script to create certificates that can be used with 4 | # Azure Active Directory 5 | # 6 | # Examples: 7 | # NAME=foo ./create-cert.sh 8 | # 9 | # With subject: 10 | # NAME=example SUBJECT="/C=IT/ST=TO/L=TO/O=E/OU=Example Team/CN=example.com" ./create-cert.sh 11 | # 12 | # ATTENTION: if you use Git Bash for Windows, 13 | # use this command instead (to avoid POSIX-to-Windows path conversion for the SUBJECT): 14 | # MSYS_NO_PATHCONV=1 NAME=example SUBJECT="/C=IT/ST=TO/L=TO/O=E/OU=Example Team/CN=example.com" ./create-cert.sh 15 | 16 | source common.sh 17 | 18 | if ! command -v openssl; then 19 | die "openssl is not installed; it is required for this script" 20 | fi 21 | 22 | if [ -z "$NAME" ]; then 23 | die 'missing `NAME`' 24 | fi 25 | 26 | if [ -z "$PFX_PASS" ]; then 27 | print_info "The generated PFX will NOT be password protected" 28 | else 29 | print_info "The generated PFX will be password protected" 30 | fi 31 | 32 | PRIVATE_RSA_NAME=$NAME.pri.pem 33 | PUBLIC_KEY_NAME=$NAME-publickey.cer 34 | PFX_NAME=$NAME-for-aad.pfx 35 | 36 | print_info "Generating private RSA key $PRIVATE_RSA_NAME - store this safely!" 37 | 38 | # private RSA key 39 | openssl genrsa -out $PRIVATE_RSA_NAME 40 | 41 | print_info "Generating public key $PUBLIC_KEY_NAME" 42 | 43 | # security certificate (public key) this one will be uploaded to Azure AD app registration 44 | 45 | if [ -z "$SUBJECT" ]; then 46 | # the user will be prompted for subject information; 47 | # to avoid the prompt, provide with SUBJECT variable 48 | openssl req -new -x509 -key $PRIVATE_RSA_NAME -out $PUBLIC_KEY_NAME -days 365 49 | else 50 | openssl req -new -x509 -key $PRIVATE_RSA_NAME -out $PUBLIC_KEY_NAME -days 365 -subj "$SUBJECT" 51 | fi 52 | 53 | print_info "Generating PFX certificate $PFX_NAME - store this safely!" 54 | 55 | # PFX certificate containing the private key – this must be stored safely and is used to obtain access tokens! 56 | openssl pkcs12 -export -in $PUBLIC_KEY_NAME -inkey $PRIVATE_RSA_NAME -out $PFX_NAME -passin pass: -passout pass:$PFX_PASS 57 | -------------------------------------------------------------------------------- /aad-machine-to-machine/client_using_secret.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module shows an example of how the client credentials flow with secret can be 3 | used with Azure Active Directory and MSAL for Python. 4 | """ 5 | import asyncio 6 | import logging 7 | import os 8 | 9 | import httpx 10 | import msal 11 | from dotenv import load_dotenv 12 | 13 | # read .env file into environment variables 14 | load_dotenv() 15 | 16 | logging.basicConfig(level=logging.DEBUG) 17 | logging.getLogger("msal").setLevel(logging.INFO) 18 | 19 | app = msal.ConfidentialClientApplication( 20 | os.environ["APP_CLIENT_ID"], 21 | authority=os.environ["AAD_AUTHORITY"], 22 | client_credential=os.environ["APP_CLIENT_SECRET"], 23 | ) 24 | 25 | scope = [os.environ["APP_CLIENT_SCOPE"]] 26 | 27 | result = app.acquire_token_silent(scope, account=None) 28 | 29 | if not result: 30 | logging.info("No suitable token exists in cache. Let's get a new one from AAD.") 31 | result = app.acquire_token_for_client(scopes=scope) 32 | 33 | if "access_token" in result: 34 | access_token = result["access_token"] 35 | logging.info("Access token %s", access_token) 36 | 37 | async def calls(): 38 | # call the API using the access token 39 | async with httpx.AsyncClient(timeout=60) as client: 40 | for _ in range(4): 41 | response = await client.get( 42 | "http://localhost:5000", 43 | headers={"Authorization": f"Bearer {access_token}"}, 44 | ) 45 | 46 | if response.status_code != 200: 47 | logging.error( 48 | "The request to the API failed, with status %s", 49 | response.status_code, 50 | ) 51 | else: 52 | logging.info( 53 | "The request to the API server succeeded. Response body: %s", 54 | response.text, 55 | ) 56 | 57 | asyncio.run(calls()) 58 | 59 | else: 60 | print(result.get("error")) 61 | print(result.get("error_description")) 62 | print(result.get("correlation_id")) 63 | -------------------------------------------------------------------------------- /otel/otel/azure.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides functions to configure OpenTelemetry logging with an 3 | Azure Application Insights service. 4 | 5 | To use, install the following dependencies: 6 | 7 | Install: 8 | pip install opentelemetry-distro opentelemetry-exporter-otlp 9 | opentelemetry-bootstrap --action=install 10 | 11 | pip install azure-monitor-opentelemetry-exporter 12 | 13 | Usage: 14 | from otel.azure import use_application_insights 15 | 16 | app = Application() 17 | use_application_insights(app, connection_string) 18 | """ 19 | 20 | from functools import wraps 21 | 22 | from azure.monitor.opentelemetry.exporter import ( 23 | AzureMonitorLogExporter, 24 | AzureMonitorTraceExporter, 25 | ) 26 | from blacksheep import Application 27 | 28 | from . import client_span_context, use_open_telemetry 29 | 30 | 31 | __all__ = ["use_application_insights", "log_dependency"] 32 | 33 | 34 | def use_application_insights( 35 | app: Application, 36 | connection_string: str, 37 | ): 38 | """ 39 | Configures OpenTelemetry for a BlackSheep application using Azure Application Insights. 40 | 41 | Sets up logging and tracing exporters for Azure Monitor using the provided connection string. 42 | 43 | Args: 44 | app: The BlackSheep Application instance. 45 | connection_string: Azure Application Insights connection string. 46 | """ 47 | use_open_telemetry( 48 | app, 49 | AzureMonitorLogExporter(connection_string=connection_string), 50 | AzureMonitorTraceExporter(connection_string=connection_string), 51 | ) 52 | 53 | 54 | def log_dependency(namespace="Service"): 55 | """ 56 | Wraps a function to log each call using OpenTelemetry, as dependency 57 | in Azure Application Insights. 58 | """ 59 | 60 | def log_decorator(fn): 61 | @wraps(fn) 62 | async def wrapper(*args, **kwargs): 63 | with client_span_context( 64 | fn.__name__, {"az.namespace": namespace}, *args, **kwargs 65 | ): 66 | return await fn(*args, **kwargs) 67 | 68 | return wrapper 69 | 70 | return log_decorator 71 | -------------------------------------------------------------------------------- /websocket-chat/app/connection.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import List 3 | 4 | from blacksheep import WebSocket 5 | from .message import Message 6 | 7 | 8 | class Connection: 9 | def __init__(self, socket: WebSocket, client_id: str): 10 | self.socket = socket 11 | self.client_id = client_id 12 | 13 | async def receive(self) -> Message: 14 | data = await self.socket.receive_json() 15 | message = Message(**data) 16 | return message 17 | 18 | async def send(self, message: Message): 19 | await self.socket.send_json(message.asdict()) 20 | 21 | 22 | class ConnectionManager: 23 | def __init__(self): 24 | self.active_connections: List[Connection] = [] 25 | 26 | async def connect(self, websocket: WebSocket, client_id: str) -> Connection: 27 | await websocket.accept() 28 | connection = Connection(websocket, client_id) 29 | await self.greet(connection) 30 | self.active_connections.append(connection) 31 | return connection 32 | 33 | async def disconnect(self, connection: Connection): 34 | self.active_connections.remove(connection) 35 | await self.bye(connection) 36 | 37 | async def manage(self, connection: Connection): 38 | message = await connection.receive() 39 | await self.broadcast(message) 40 | 41 | async def broadcast(self, message: Message): 42 | print('Broadcast to %s connections' % len(self.active_connections)) 43 | for connection in self.active_connections: 44 | await connection.send(message) 45 | 46 | async def greet(self, connection: Connection): 47 | message = Message( 48 | author='Server', 49 | timestamp=datetime.now().isoformat(), 50 | text=f'{connection.client_id} enters the chat', 51 | ) 52 | await self.broadcast(message) 53 | 54 | async def bye(self, connection: Connection): 55 | message = Message( 56 | author='Server', 57 | timestamp=datetime.now().isoformat(), 58 | text=f'{connection.client_id} disconnected', 59 | ) 60 | await self.broadcast(message) 61 | -------------------------------------------------------------------------------- /aad-machine-to-machine/client_using_certificate.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module shows an example of how the client credentials flow with certificate can be 3 | used with Azure Active Directory and MSAL for Python. 4 | For information on how to generate valid certificates, refer to the README files in this 5 | repository. 6 | """ 7 | import asyncio 8 | import logging 9 | import os 10 | 11 | import httpx 12 | import msal 13 | from dotenv import load_dotenv 14 | 15 | # read .env file into environment variables 16 | load_dotenv() 17 | 18 | logging.basicConfig(level=logging.DEBUG) 19 | logging.getLogger("msal").setLevel(logging.INFO) 20 | 21 | app = msal.ConfidentialClientApplication( 22 | os.environ["APP_CLIENT_ID"], 23 | authority=os.environ["AAD_AUTHORITY"], 24 | client_credential={ 25 | "thumbprint": os.environ["APP_CLIENT_CERT_THUMBPRINT"], 26 | "private_key": open("example.pri.pem").read(), 27 | }, 28 | ) 29 | 30 | scope = [os.environ["APP_CLIENT_SCOPE"]] 31 | 32 | result = app.acquire_token_silent(scope, account=None) 33 | 34 | if not result: 35 | logging.info("No suitable token exists in cache. Let's get a new one from AAD.") 36 | result = app.acquire_token_for_client(scopes=scope) 37 | 38 | if "access_token" in result: 39 | access_token = result["access_token"] 40 | logging.info("Access token %s", access_token) 41 | 42 | async def calls(): 43 | # call the API using the access token 44 | async with httpx.AsyncClient(timeout=60) as client: 45 | for _ in range(4): 46 | response = await client.get( 47 | "http://localhost:5000", 48 | headers={"Authorization": f"Bearer {access_token}"}, 49 | ) 50 | 51 | if response.status_code != 200: 52 | logging.error( 53 | "The request to the API failed, with status %s", 54 | response.status_code, 55 | ) 56 | else: 57 | logging.info( 58 | "The request to the API server succeeded. Response body: %s", 59 | response.text, 60 | ) 61 | 62 | asyncio.run(calls()) 63 | 64 | else: 65 | print(result.get("error")) 66 | print(result.get("error_description")) 67 | print(result.get("correlation_id")) 68 | -------------------------------------------------------------------------------- /oidc/scopes_redis_aad.py: -------------------------------------------------------------------------------- 1 | """ 2 | In this example the default Cookie handler is used to store user 3 | context in a cookie (the id_token), and an access token and refresh token that 4 | were obtained during the sign-in are stored in a Redis server. 5 | 6 | In this case, to refresh a token from the client side, it's sufficient a POST request to 7 | /refresh-token (by default), or the configured refresh_token_path. 8 | 9 | async function refreshToken() { 10 | await fetch('/refresh-token', { 11 | method: "POST" 12 | }); 13 | } 14 | """ 15 | import redis.asyncio as redis 16 | import uvicorn 17 | from blacksheep.server.application import Application 18 | from blacksheep.server.authentication.oidc import ( 19 | CookiesOpenIDTokensHandler, 20 | OpenIDSettings, 21 | use_openid_connect, 22 | ) 23 | from dotenv import load_dotenv 24 | 25 | from common.redis import RedisTokensStore 26 | from common.routes import register_routes 27 | from common.secrets import Secrets 28 | 29 | load_dotenv() 30 | secrets = Secrets.from_env() 31 | app = Application(show_error_details=True) 32 | 33 | 34 | # AAD with custom scope 35 | handler = use_openid_connect( 36 | app, 37 | OpenIDSettings( 38 | authority="https://login.microsoftonline.com/b62b317a-19c2-40c0-8650-2d9672324ac4/v2.0/", 39 | client_id="499adb65-5e26-459e-bc35-b3e1b5f71a9d", 40 | client_secret=secrets.aad_client_secret, 41 | scope="openid profile offline_access email " 42 | "api://65d21481-4f1a-4731-9508-ad965cb4d59f/example", 43 | ), 44 | ) 45 | 46 | 47 | @app.lifespan 48 | async def configure_redis(): 49 | """ 50 | Configure an async Redis client, and dispose its connections when the application 51 | stops. 52 | See: 53 | https://redis.readthedocs.io/en/stable/examples/asyncio_examples.html 54 | """ 55 | connection = redis.Redis() 56 | print(f"Ping successful: {await connection.ping()}") 57 | 58 | app.services.register(redis.Redis, instance=connection) 59 | 60 | # configure the tokens store of the authentication handler 61 | assert isinstance(handler.auth_handler, CookiesOpenIDTokensHandler) 62 | handler.auth_handler.tokens_store = RedisTokensStore(redis.Redis()) 63 | 64 | yield connection 65 | 66 | print("Disposing the Redis connection pool...") 67 | await connection.close() 68 | 69 | 70 | register_routes(app) 71 | 72 | 73 | if __name__ == "__main__": 74 | uvicorn.run(app, host="127.0.0.1", port=5000, log_level="debug") 75 | -------------------------------------------------------------------------------- /dependency-injector/app/di.py: -------------------------------------------------------------------------------- 1 | from typing import Type, TypeVar, get_type_hints 2 | 3 | from dependency_injector import containers, providers 4 | 5 | T = TypeVar("T") 6 | 7 | 8 | class DependencyInjectorConnector: 9 | """ 10 | This class connects a Dependency Injector container with a 11 | BlackSheep application. 12 | Dependencies are registered using the code API offered by 13 | Dependency Injector. The BlackSheep application activates services 14 | using the container when needed. 15 | """ 16 | 17 | def __init__(self, container: containers.Container) -> None: 18 | self._container = container 19 | 20 | def register(self, obj_type: Type[T]) -> None: 21 | """ 22 | Registers a type with the container. 23 | The code below inspects the object's constructor's types annotations to 24 | automatically configure the provider to activate the type. 25 | 26 | It is not necessary to use @inject or Provide core on the __init__ method. This 27 | helps reducing code verbosity and keeping the source code not polluted by DI 28 | specific code. 29 | """ 30 | constructor = getattr(obj_type, "__init__", None) 31 | 32 | if not constructor: 33 | raise ValueError( 34 | f"Type {obj_type.__name__} does not have an __init__ method." 35 | ) 36 | 37 | # Get the type hints for the constructor parameters 38 | type_hints = get_type_hints(constructor) 39 | 40 | # Exclude 'self' from the parameters 41 | dependencies = { 42 | param_name: getattr(self._container, param_type.__name__) 43 | for param_name, param_type in type_hints.items() 44 | if param_name not in {"self", "return"} 45 | and hasattr(self._container, param_type.__name__) 46 | } 47 | 48 | # Create a provider for the type with its dependencies 49 | provider = providers.Factory(obj_type, **dependencies) 50 | setattr(self._container, obj_type.__name__, provider) 51 | 52 | def resolve(self, obj_type: Type[T], _) -> T: 53 | """Resolves an instance of the given type.""" 54 | provider = getattr(self._container, obj_type.__name__, None) 55 | if provider is None: 56 | raise TypeError( 57 | f"Type {obj_type.__name__} is not registered in the container." 58 | ) 59 | return provider() 60 | 61 | def __contains__(self, item: Type[T]) -> bool: 62 | """Checks if a type is registered in the container.""" 63 | return hasattr(self._container, item.__name__) 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # VSCode 88 | .vscode/ 89 | 90 | # ASDF 91 | .tool-versions 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 101 | __pypackages__/ 102 | 103 | # Celery stuff 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | 137 | *.pfx 138 | *.pem 139 | *.cer 140 | .ruff_cache 141 | /**/.ruff_cache 142 | .local/ 143 | -------------------------------------------------------------------------------- /jwt-validation/dev/request.http: -------------------------------------------------------------------------------- 1 | GET http://localhost:8000/api/message 2 | Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imwzc1EtNTBjQ0g0eEJWWkxIVEd3blNSNzY4MCJ9.eyJhdWQiOiIxMDRiY2E2MC1jNWE3LTRhYjktODNlMS03YjljOGRhZDcxZTIiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vYjYyYjMxN2EtMTljMi00MGMwLTg2NTAtMmQ5NjcyMzI0YWM0L3YyLjAiLCJpYXQiOjE2MzU3MDI5NTMsIm5iZiI6MTYzNTcwMjk1MywiZXhwIjoxNjM1NzA2ODUzLCJpZHAiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC85MTg4MDQwZC02YzY3LTRjNWItYjExMi0zNmEzMDRiNjZkYWQvIiwibmFtZSI6IlJvYmVydG8gUHJldmF0byIsIm5vbmNlIjoiZmE3YTdjMTAtMzQxZi00NjYwLWI5MTQtZDhhNDg0MjhjNmE4Iiwib2lkIjoiNWY0Mzc3MDMtNGMwNS00ZGJiLWJjNWQtMjZjNDJhYWI2NzA5IiwicHJlZmVycmVkX3VzZXJuYW1lIjoicm9iZXJ0by5wcmV2YXRvQGdtYWlsLmNvbSIsInJoIjoiMC5BUkVBZWpFcnRzSVp3RUNHVUMyV2NqSkt4R0RLU3hDbnhibEtnLUY3bkkydGNlSVJBSGsuIiwicm9sZXMiOlsiQURNSU4iLCJVU0VSIl0sInN1YiI6IjFHX1hZSzlTRFZRcmZsOWJSdWNjNXgyTWdiZ3RHMkFhakNxazF0Z1FaTEkiLCJ0aWQiOiJiNjJiMzE3YS0xOWMyLTQwYzAtODY1MC0yZDk2NzIzMjRhYzQiLCJ1dGkiOiJpZV9OWU5fMy0wVzN1RDJRRkg0eUFBIiwidmVyIjoiMi4wIn0.f7x63025UVQgIe239rnzZ2nNJWkoY2OBdkVaTH3VAdYeQM_UMkbvWZYU_Vs00ivcP1TaTqHaxPNaOyXXeSkAoJ2iBifK_8EoUpfqHDwxFMOoeWtJ1W9ea3nIChd1hwlMmsNtWym4EneEwle4SGrUKejgi5lDgPe_PwSEkNXnNJ9_Z1DPgIkO2W-_QyxqrVXFDjck9FUxMTCDmRZPJUiv4AEX31laTFrALA7O6tbYmCPsf3hCgrzI8bMWjiMFCQ0YtWenpx4MuTUkClnZ0MJpYCUB9pT-0wKt5ptMO5ri7APSmNreOODDNwg223_JmdjhwUs8Eoh2OBPjF4X2ojpcYg 3 | # Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Im5PbzNaRHJPRFhFSzFqS1doWHNsSFJfS1hFZyJ9.eyJhdWQiOiJhYzhlYTg1MC01MTM4LTRmYmItOTYyYy1iMWVkN2FjNTUxNjMiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vYjYyYjMxN2EtMTljMi00MGMwLTg2NTAtMmQ5NjcyMzI0YWM0L3YyLjAiLCJpYXQiOjE2MjkxNDg2NzUsIm5iZiI6MTYyOTE0ODY3NSwiZXhwIjoxNjI5MTUyNTc1LCJpZHAiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC85MTg4MDQwZC02YzY3LTRjNWItYjExMi0zNmEzMDRiNjZkYWQvIiwibmFtZSI6IlJvYmVydG8gUHJldmF0byIsIm5vbmNlIjoiOGVjNDA1ZDktMzM5NC00NTc5LWIzM2EtMDg4NmQwYTY1NmE2Iiwib2lkIjoiNWY0Mzc3MDMtNGMwNS00ZGJiLWJjNWQtMjZjNDJhYWI2NzA5IiwicHJlZmVycmVkX3VzZXJuYW1lIjoicm9iZXJ0by5wcmV2YXRvQGdtYWlsLmNvbSIsInJoIjoiMC5BUkVBZWpFcnRzSVp3RUNHVUMyV2NqSkt4RkNvanF3NFVidFBsaXl4N1hyRlVXTVJBSGsuIiwicm9sZXMiOlsiVVNFUiIsIkFETUlOIl0sInN1YiI6InotLS1sZnZiLXRSTzJ0OGQ5NGYyYWRWbzdWMDNJZlVJckFIQ2QwTXRtdlEiLCJ0aWQiOiJiNjJiMzE3YS0xOWMyLTQwYzAtODY1MC0yZDk2NzIzMjRhYzQiLCJ1dGkiOiJOLXFEbWV1c0kwV1BwemZjaWtEREFRIiwidmVyIjoiMi4wIn0.SGAi_dWTSi7j7i7BB5D1F8li686fapqChLH4kYLtpURiIF2ONIw9oU44ThuQvIpWh4oGHNtwUApEn9-6pIikswg2CAM7c55PvSstDX0VxAuE4DTTfrgYANR5dT_AjJ92DBsF_X3kb91RkgpPd_KdOYHuezrVnW9JvOcKFxZ0fS0qjxnEOi4aPH7eWAhOIv041QZa7ELJRt5ylEvs9x5RcJzV1PCB4sv4gDb7BCfE5kD6cK3tyO967b5_pp8t2qUKT8xvj2CPCcGr4JQauW-97mmTuU67z9BpYhbJPo4Gh1efMJBfkWE0fj-ETMAnIyWJtPl33ji2Kp3tYaQdSwPPWA 4 | 5 | 6 | ### 7 | 8 | GET http://localhost:8000/api/message 9 | -------------------------------------------------------------------------------- /max-body-size/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example illustrates how the maximum body size from a client can be validated inside 3 | a request handler, efficiently processing chunks in memory. 4 | 5 | At the time of this writing, blacksheep does not support configuring a maximum body size 6 | globally, nor configuring a maximum body size for specific request handlers that would 7 | validate the request body size also when trying to read the whole request content as 8 | JSON, text, form data. 9 | 10 | The following methods: 11 | text = await request.text() 12 | data = await request.json() 13 | data = await request.form() 14 | 15 | all cause the whole request body to be read. 16 | """ 17 | from pathlib import Path 18 | 19 | from blacksheep import Application, Request, json 20 | from blacksheep.exceptions import BadRequest, HTTPException 21 | from essentials.folders import ensure_folder 22 | 23 | 24 | class MaxBodyExceededError(HTTPException): 25 | def __init__(self, max_size: int): 26 | super().__init__(413, "The request body exceeds the maximum size.") 27 | self.max_size = max_size 28 | 29 | 30 | async def read_stream(request: Request, max_body_size: int = 1500000): 31 | """ 32 | Reads a request stream, up to a maximum body length (default to 1.5 MB). 33 | """ 34 | current_length = 0 35 | async for chunk in request.stream(): 36 | current_length += len(chunk) 37 | 38 | if max_body_size > -1 and current_length > max_body_size: 39 | raise MaxBodyExceededError(max_body_size) 40 | 41 | yield chunk 42 | 43 | 44 | app = Application(show_error_details=True) 45 | 46 | 47 | @app.exception_handler(413) 48 | async def handle_max_body_size(app, request, exc: MaxBodyExceededError): 49 | return json({"error": "Maximum body size exceeded", "max_size": exc.max_size}) 50 | 51 | 52 | app.serve_files("static") 53 | 54 | app.use_cors( 55 | allow_methods="POST", 56 | allow_origins="*", 57 | max_age=300, 58 | ) 59 | 60 | 61 | ensure_folder("out") 62 | 63 | 64 | @app.router.post("/upload-file") 65 | async def file_uploader(request: Request): 66 | file_name = request.get_first_header(b"File-Name") 67 | if not file_name: 68 | raise BadRequest("Missing file name.") 69 | 70 | file_name = file_name.decode() 71 | file_path = Path("out") / file_name 72 | 73 | try: 74 | with open(file_path, mode="wb") as example_file: 75 | async for chunk in read_stream(request): 76 | example_file.write(chunk) 77 | except MaxBodyExceededError: 78 | file_path.unlink() 79 | raise 80 | 81 | return {"status": "OK", "uploaded_file": file_name} 82 | 83 | 84 | if __name__ == "__main__": 85 | import uvicorn 86 | 87 | uvicorn.run(app, host="localhost", port=44555, lifespan="on") 88 | -------------------------------------------------------------------------------- /max-body-size/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 21 | 22 | 23 | 24 |

Uploading a file

25 |
26 |
27 | 28 | 29 |
30 |
31 | 32 |
33 |

34 |     
83 | 
84 | 
85 | 
86 | 


--------------------------------------------------------------------------------
/proxy-1/example.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 |     
19 | 
20 | 
21 | 
22 |     

Proxy example

23 | 24 |
    25 |
  1. Start the Flask application.
  2. 26 |
  3. Start the BlackSheep application.
  4. 27 |
  5. Try using this page.
  6. 28 |
  7. Try uploading files.
  8. 29 |
  9. Check the response, and the `out` folder.
  10. 30 |
31 | 32 |
33 |

Static files

34 |

The following image is served by the Flask app.

35 | Octocat served by the Flask app 36 |
37 |

The following image is served by the BlackSheep app.

38 | Octocat served by the BlackSheep app 39 |
40 | 41 |
42 |

Test the upload to the Flask Server directly:

43 |

The response should display JSON information about the request, and uploaded files should be written to the 44 | "out" folder.

45 |
46 | 47 |

48 | 49 |

50 | 51 | 52 | 53 |
54 | 55 | 56 |

Test the upload to the BlackSheep proxy (which proxies to the Flask app):

57 |

The response should display JSON information about the request, and uploaded files should be written to the 58 | "out" folder.

59 |
60 | 61 |

62 | 63 |

64 | 65 | 66 | 67 |
68 |
69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /proxy-2/example-2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 20 | 21 | 22 |

Proxy example

23 | 24 |
    25 |
  1. Start the first BlackSheep application (localhost:44777).
  2. 26 |
  3. Start the proxy BlackSheep application (localhost:44555).
  4. 27 |
  5. Try using this page.
  6. 28 |
  7. Try uploading files.
  8. 29 |
  9. Check the response, and the `out` folder.
  10. 30 |
31 | 32 |
33 |

Static files

34 |

The following image is served by the first BlackSheep app.

35 | Octocat served by the second BlackSheep app 36 |
37 |

The following image is served by the BlackSheep proxy app.

38 | Octocat served by the first BlackSheep app 39 |
40 | 41 |
42 |

Test the upload to the first BlackSheep app directly:

43 |

The response should display JSON information about the request, and uploaded files should be written to the 44 | "out" folder.

45 |
46 | 47 |

48 | 49 |

50 | 51 | 52 | 53 |
54 | 55 | 56 |

Test the upload to the BlackSheep proxy (which proxies to the first BlackSheep app):

57 |

The response should display JSON information about the request, and uploaded files should be written to the 58 | "out" folder.

59 |
60 | 61 |

62 | 63 |

64 | 65 | 66 | 67 |
68 |
69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /oidc/scopes_aad.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to configure an OpenID Connect integration with Azure Active 3 | Directory, obtaining an id_token, an access_token, and a refresh_token. The id_token 4 | is exchanged with the client using a response cookie (also used to authenticate users 5 | for following requests), while the access token and the refresh token are stored using 6 | a custom implementation of TokensStore. 7 | """ 8 | from datetime import datetime 9 | 10 | import uvicorn 11 | from blacksheep import Request, Response 12 | from blacksheep.server.application import Application 13 | from blacksheep.server.authentication.oidc import ( 14 | CookiesOpenIDTokensHandler, 15 | OpenIDSettings, 16 | TokensStore, 17 | use_openid_connect, 18 | ) 19 | from dotenv import load_dotenv 20 | 21 | from common.routes import register_routes 22 | from common.secrets import Secrets 23 | 24 | load_dotenv() 25 | secrets = Secrets.from_env() 26 | app = Application(show_error_details=True) 27 | 28 | 29 | class TestTokensStore(TokensStore): 30 | def __init__(self) -> None: 31 | super().__init__() 32 | self._access_token: str | None = None 33 | self._refresh_token: str | None = None 34 | 35 | async def store_tokens( 36 | self, 37 | request: Request, 38 | response: Response, 39 | access_token: str, 40 | refresh_token: str | None, 41 | expires: datetime | None = None, 42 | ): 43 | """ 44 | Applies a strategy to store an access token and an optional refresh token for 45 | the given request and response. 46 | """ 47 | self._access_token = access_token 48 | self._refresh_token = refresh_token 49 | 50 | async def unset_tokens(self, request: Request): 51 | """ 52 | Optional method, to unset access tokens upon sign-out. 53 | """ 54 | self._access_token = None 55 | self._refresh_token = None 56 | 57 | async def restore_tokens(self, request: Request) -> None: 58 | """ 59 | Applies a strategy to restore an access token and an optional refresh token for 60 | the given request. 61 | """ 62 | assert request.identity is not None 63 | request.identity.access_token = self._access_token 64 | request.identity.refresh_token = self._refresh_token 65 | 66 | 67 | # AAD with custom scope 68 | handler = use_openid_connect( 69 | app, 70 | OpenIDSettings( 71 | authority="https://login.microsoftonline.com/b62b317a-19c2-40c0-8650-2d9672324ac4/v2.0/", 72 | client_id="499adb65-5e26-459e-bc35-b3e1b5f71a9d", 73 | client_secret=secrets.aad_client_secret, 74 | scope="openid profile offline_access email " 75 | "api://65d21481-4f1a-4731-9508-ad965cb4d59f/example", 76 | ), 77 | ) 78 | 79 | assert isinstance(handler.auth_handler, CookiesOpenIDTokensHandler) 80 | handler.auth_handler.tokens_store = TestTokensStore() 81 | 82 | register_routes(app) 83 | 84 | 85 | if __name__ == "__main__": 86 | uvicorn.run(app, host="127.0.0.1", port=5000, log_level="debug") 87 | -------------------------------------------------------------------------------- /otel/README.md: -------------------------------------------------------------------------------- 1 | # Examples for OpenTelemetry integration 2 | 3 | This folder contains examples to use [OpenTelemetry](https://opentelemetry.io/) 4 | integration with [Grafana](https://grafana.com/), and with [Azure Application 5 | Insights](https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview). 6 | 7 | > [!IMPORTANT] 8 | > This example includes code that can be used to integrate OpenTelemetry telemetries with 9 | > versions of the framework before `2.3.2`. Since `2.3.2`, vendor-agnostic code from this 10 | > example have been included in the BlackSheep framework. 11 | > For more information, refer to the documentation at https://www.neoteroi.dev/blacksheep/opentelemetry/ 12 | 13 | ## Requirements 14 | 15 | ```bash 16 | pip install opentelemetry-distro opentelemetry-exporter-otlp 17 | 18 | opentelemetry-bootstrap --action=install 19 | ``` 20 | 21 | For *Azure Application Insights*, also install: 22 | 23 | ```bash 24 | pip install azure-monitor-opentelemetry-exporter 25 | ``` 26 | 27 | To test, install also `blacksheep` and an ASGI server of your choice. For instance, `uvicorn`. 28 | 29 | ```bash 30 | pip install blacksheep uvicorn 31 | ``` 32 | 33 | ### Running the Azure example 34 | 35 | 1. Install the dependencies like documented above, including 36 | `azure-monitor-opentelemetry-exporter`. 37 | 2. Configure an environment variable `APP_INSIGHTS_CONNECTION_STRING` containing 38 | the connection string of an Azure Application Insights service. 39 | 3. Run the application with `uvicorn azureexample:app`. 40 | 4. Generate some web requests to the example endpoints `/`, `/bad-request`, 41 | `/crash`, `/example`. 42 | 5. Observe how logs appear in the Azure Application Insights service. 43 | 44 | ![image](https://github.com/user-attachments/assets/f1fac2db-228c-4573-bbe1-e9f544bd3065) 45 | 46 | 47 | ### Running the Grafana example 48 | 49 | 1. Install the dependencies like documented above, including 50 | `azure-monitor-opentelemetry-exporter`. 51 | 2. Obtain the necessary environment variables from the Grafana interface. 52 | 3. Configure the environment variables. These variables can also be configured 53 | in a `.env` file. 54 | 4. Run the application with `uvicorn grafanaexample:app`. 55 | 5. Generate some web requests to the example endpoints `/`, `/bad-request`, 56 | `/crash`, `/example`. 57 | 6. Observe how logs appear in the Azure Application Insights service. 58 | 59 | Environment variables look like the following: 60 | 61 | ``` 62 | OTEL_RESOURCE_ATTRIBUTES="service.name=my-app,service.namespace=my-application-group,deployment.environment=production" 63 | OTEL_EXPORTER_OTLP_ENDPOINT="https://otlp-gateway-prod-eu-north-0.grafana.net/otlp" 64 | OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic%20******" 65 | OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf" 66 | ``` 67 | 68 | ![image](https://github.com/user-attachments/assets/2e4722e4-eb14-49ad-b5dc-a542c47f0e48) 69 | 70 | ## Folder structure 71 | 72 | - The `otel` package contains reusable code. 73 | - `otel.otlp` contains generic code that can be used with many services adhering to the OpenTelemetry standard, including Grafana. 74 | -------------------------------------------------------------------------------- /oauth2-password-provider/src/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from datetime import datetime 4 | import hashlib 5 | import os 6 | from .db import User 7 | 8 | from pydantic import UUID4 9 | 10 | from .db import TOKEN_DB, USER_DB, Token 11 | 12 | 13 | def hashpw(password: str): 14 | """Hash a password for storing.""" 15 | 16 | salt = os.urandom(32) 17 | digest = hashlib.pbkdf2_hmac("sha512", password.encode("utf8"), salt, 1024) 18 | hashed_password = f"{salt.hex()}${digest.hex()}" 19 | return hashed_password 20 | 21 | 22 | def checkpw(password: str, hashed_password: str): 23 | """Check a hashed password.""" 24 | 25 | salt_hex, digest_hex = hashed_password.split("$") 26 | return ( 27 | digest_hex 28 | == hashlib.pbkdf2_hmac( 29 | "sha512", 30 | password.encode("utf8"), 31 | bytes.fromhex(salt_hex), 32 | 1024, 33 | ).hex() 34 | ) 35 | 36 | 37 | class UserDAL: 38 | def register(self, username: str, password: str): 39 | """Register a user. 40 | 41 | Save user to storage and return user data. 42 | """ 43 | 44 | hashed_password = hashpw(password) 45 | new_user = User(username=username, password=hashed_password) 46 | USER_DB[username] = new_user 47 | return new_user.dict() 48 | 49 | async def get_authuser_by_username(self, username): 50 | """Get user by username.""" 51 | 52 | return USER_DB.get(username) 53 | 54 | async def save_refresh_token( 55 | self, 56 | user_id: UUID4, 57 | jti: UUID4, 58 | session_id: UUID4, 59 | expired_at: datetime, 60 | ): 61 | """Save refresh token to storage.""" 62 | 63 | TOKEN_DB[jti] = Token( 64 | id=jti, 65 | user_id=user_id, 66 | session_id=session_id, 67 | expired_at=expired_at, 68 | ) 69 | 70 | async def get_user_by_refresh_token( 71 | self, 72 | user_id: UUID4, 73 | refresh_jti: UUID4, 74 | session_id: UUID4, 75 | ) -> User | None: 76 | """Get user by refresh token.""" 77 | 78 | token = TOKEN_DB.get(refresh_jti) 79 | if not token: 80 | return None 81 | if token.expired_at < datetime.utcnow(): 82 | return None 83 | if token.user_id != user_id: 84 | return None 85 | if token.session_id != session_id: 86 | return None 87 | 88 | user = USER_DB.get(user_id) 89 | if not user: 90 | return None 91 | 92 | return user 93 | 94 | async def revoke_refresh_token(self, user_id: UUID4): 95 | """Revoke all refresh tokens for user.""" 96 | 97 | for token in TOKEN_DB.values(): 98 | if token.user_id == user_id: 99 | del TOKEN_DB[token.id] 100 | 101 | async def remove_used_refresh_token(self, token_id: UUID4): 102 | """Remove used refresh token from storage.""" 103 | 104 | del TOKEN_DB[token_id] 105 | -------------------------------------------------------------------------------- /websocket-chat/app/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BlackSheep WebSocket Example 6 | 7 | 13 | 14 | 15 |
16 |

Your client ID is {{ CLIENT_ID }}

17 |

Status: {{ status }}

18 |

Messages:

19 | 26 |
27 | 28 | 29 | 30 | 31 |
32 |
33 | 99 | 100 | -------------------------------------------------------------------------------- /aad-machine-to-machine/README.md: -------------------------------------------------------------------------------- 1 | [![Video tutorial](https://gist.githubusercontent.com/RobertoPrevato/b9f5162bfe6082876ec2d9811cc554b0/raw/299af557b36c9495253eab93dbd4e7afa8421699/video-tutorial.svg)](https://youtu.be/-SPEcQxgOOQ) 2 | 3 | # Machine to machine (M2M) communication using
Azure Active Directory 4 | 5 | This example shows: 6 | * how to configure an API to require access tokens issued by Azure Active Directory 7 | * how to obtain access tokens for a confidential client (meaning an application that is 8 | able to handle secrets), running as a background worker or daemon, without user interaction 9 | 10 | `server.py` contains the server definition that requires and validates access tokens. 11 | `client_using_secret.py` contains a client definition that, using [MSAL for Python](https://github.com/AzureAD/microsoft-authentication-library-for-python), obtains access 12 | tokens using the `client credentials flow` **with a secret**, and calls the server. 13 | 14 | `client_using_certificate.py` contains a client definition that, using [MSAL for Python](https://github.com/AzureAD/microsoft-authentication-library-for-python), obtains access 15 | tokens using the `client credentials flow` **with a certificate**, and calls the server. 16 | Refer to the information under `certs` folder to have a reference on how to generate valid 17 | certificates for Azure Active Directory. 18 | 19 | `client_http_example.py` shows an example using the client credentials flow 20 | with secret with an HTTP POST request to the token endpoint, without using MSAL for Python. 21 | 22 | The following scheme describes the flow of this example. 23 | 24 | ![Scheme](https://gist.githubusercontent.com/RobertoPrevato/38a0598b515a2f7257c614938843b99b/raw/7ccbef683b18379ccf003ae9c7823ee03f3dc9f5/client-credentials-flow.png) 25 | 26 | * Client is the application running as daemon, connecting to the API 27 | * AAD is Azure Active Directory 28 | * API is the web application exposing an API and requiring access tokens 29 | 30 | ## How to run this example 31 | 32 | To run the example using the secret: 33 | 34 | 1. configure app registrations in a Azure Active Directory tenant 35 | 2. create a `.env` file with appropriate values, like in the example below, 36 | or in alternative, configure the environmental variables as in the same 37 | example 38 | 3. create a Python virtual environment, install the dependencies in `requirements.txt` 39 | 4. activate the virtual environment in two terminals, then: 40 | 5. run the server in one terminal `python server.py` 41 | 6. run the client file in another terminal `python client_using_secret.py` 42 | 43 | The client file should display that an access token is obtained successfully 44 | from Azure Active Directory and a call to the running server was successful. 45 | 46 | `http_example.py` shows an example of how the client credentials flow with secret can be 47 | used with HTTP, without using MSAL for Python. 48 | 49 | ## Example .env file 50 | 51 | To configure application settings to run these examples, create an `.env` file 52 | with contents like in the following block: 53 | 54 | ```bash 55 | # Server configuration 56 | API_ISSUER="https://sts.windows.net//" 57 | API_AUDIENCE="" 58 | 59 | # Client configuration 60 | AAD_AUTHORITY="https://login.microsoftonline.com//" 61 | APP_CLIENT_ID="" 62 | APP_CLIENT_SECRET="" 63 | APP_CLIENT_SCOPE="/.default" 64 | 65 | # For the example using a certificate: 66 | APP_CLIENT_CERT_THUMBPRINT="" 67 | ``` 68 | 69 | The `.env` file is read using `python-dotenv` when the examples are run. 70 | -------------------------------------------------------------------------------- /oidc/common/routes.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent 2 | 3 | import jwt 4 | from blacksheep.messages import Request 5 | from blacksheep.server.application import Application 6 | from blacksheep.server.authorization import allow_anonymous 7 | from blacksheep.server.responses import html, pretty_json, redirect, unauthorized 8 | from essentials.json import dumps 9 | from guardpost.authentication import Identity 10 | 11 | 12 | def _render_access_token(user: Identity) -> str: 13 | if not user.access_token: 14 | return "" 15 | 16 | # parse without validating the access token 17 | # (the id_token was validated upon sign-in!) 18 | claims = jwt.decode(user.access_token, options={"verify_signature": False}) 19 | 20 | return dedent( 21 | f""" 22 |

You also have an access token for an API

23 |

These are the claims, from your access_token:

24 |
{dumps(claims, indent=4)}
25 | """ 26 | ) 27 | 28 | 29 | def register_routes(app: Application, static_home: bool = False) -> None: 30 | @allow_anonymous() 31 | @app.route("/sign-in-error") 32 | async def error_handler(request: Request, error: str): 33 | if error == "access_denied": 34 | # the user declined consents to the app 35 | return html("

OK, but you won't be able to use our wonderful app.

") 36 | return html(f"

Oh, no! {error}

") 37 | 38 | if static_home: 39 | # for advanced examples using JWT Bearer authentication 40 | app.serve_files("static") 41 | else: 42 | # for examples using Cookie based authentication 43 | @app.route("/") 44 | async def home(request: Request, user: Identity): 45 | host = request.get_first_header(b"Host") 46 | if b"localhost" not in host: 47 | return redirect("http://localhost:5000/") 48 | 49 | if user.is_authenticated(): 50 | id_claims = dumps(user.claims, indent=4) 51 | 52 | return html( 53 | dedent( 54 | f""" 55 | 56 | 57 | 58 | 64 | 65 | 66 |

Welcome!

67 |

68 | These are your claims, from your 69 | id_token:

70 |
{id_claims}
71 | {_render_access_token(user)} 72 | 73 | 74 | """ 75 | ) 76 | ) 77 | 78 | return html( 79 | dedent( 80 | """ 81 | 82 | 83 | 84 | 85 | 86 |

You are not authenticated!

87 | Sign in here.
88 | 89 | 90 | """ 91 | ) 92 | ) 93 | 94 | @app.route("/auth/me") 95 | async def user_info(user: Identity): 96 | if not user.is_authenticated(): 97 | return unauthorized("Unauthorized") 98 | return pretty_json(user.claims) 99 | -------------------------------------------------------------------------------- /dependency-injector/docs/example1.py: -------------------------------------------------------------------------------- 1 | from typing import Type, TypeVar, get_type_hints 2 | 3 | from dependency_injector import containers, providers 4 | 5 | from blacksheep import Application, get 6 | 7 | T = TypeVar("T") 8 | 9 | 10 | class APIClient: ... 11 | 12 | 13 | class SomeService: 14 | 15 | def __init__(self, api_client: APIClient) -> None: 16 | self.api_client = api_client 17 | 18 | 19 | # Define the Dependency Injector container 20 | class AppContainer(containers.DeclarativeContainer): 21 | APIClient = providers.Singleton(APIClient) 22 | SomeService = providers.Factory(SomeService, api_client=APIClient) 23 | 24 | 25 | # Create the container instance 26 | container = AppContainer() 27 | 28 | 29 | class DependencyInjectorConnector: 30 | """ 31 | This class connects a Dependency Injector container with a 32 | BlackSheep application. 33 | Dependencies are registered using the code API offered by 34 | Dependency Injector. The BlackSheep application activates services 35 | using the container when needed. 36 | """ 37 | 38 | def __init__(self, container: containers.Container) -> None: 39 | self._container = container 40 | 41 | def register(self, obj_type: Type[T]) -> None: 42 | """ 43 | Registers a type with the container. 44 | The code below inspects the object's constructor's types annotations to 45 | automatically configure the provider to activate the type. 46 | 47 | It is not necessary to use @inject or Provide core on the __init__ method. This 48 | helps reducing code verbosity and keeping the source code not polluted by DI 49 | specific code. 50 | """ 51 | constructor = getattr(obj_type, "__init__", None) 52 | 53 | if not constructor: 54 | raise ValueError( 55 | f"Type {obj_type.__name__} does not have an __init__ method." 56 | ) 57 | 58 | # Get the type hints for the constructor parameters 59 | type_hints = get_type_hints(constructor) 60 | 61 | # Exclude 'self' from the parameters 62 | dependencies = { 63 | param_name: getattr(self._container, param_type.__name__) 64 | for param_name, param_type in type_hints.items() 65 | if param_name not in {"self", "return"} 66 | and hasattr(self._container, param_type.__name__) 67 | } 68 | 69 | # Create a provider for the type with its dependencies 70 | provider = providers.Factory(obj_type, **dependencies) 71 | setattr(self._container, obj_type.__name__, provider) 72 | 73 | def resolve(self, obj_type: Type[T], _) -> T: 74 | """Resolves an instance of the given type.""" 75 | provider = getattr(self._container, obj_type.__name__, None) 76 | if provider is None: 77 | raise TypeError( 78 | f"Type {obj_type.__name__} is not registered in the container." 79 | ) 80 | return provider() 81 | 82 | def __contains__(self, item: Type[T]) -> bool: 83 | """Checks if a type is registered in the container.""" 84 | return hasattr(self._container, item.__name__) 85 | 86 | 87 | app = Application( 88 | services=DependencyInjectorConnector(container), show_error_details=True 89 | ) 90 | 91 | 92 | @get("/") 93 | def home(service: SomeService): 94 | print(service) 95 | # DependencyInjector resolved the dependencies 96 | assert isinstance(service, SomeService) 97 | assert isinstance(service.api_client, APIClient) 98 | return id(service) 99 | 100 | 101 | if __name__ == "__main__": 102 | import uvicorn 103 | 104 | uvicorn.run(app, port=44777) 105 | -------------------------------------------------------------------------------- /oidc/common/redis.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import datetime 3 | from json import JSONDecodeError, dumps, loads 4 | from typing import Optional 5 | from uuid import uuid4 6 | 7 | import redis.asyncio as redis 8 | from blacksheep import Cookie, Request, Response 9 | from blacksheep.server.authentication.oidc import TokensStore, logger 10 | from guardpost import Identity 11 | 12 | logger.setLevel(logging.DEBUG) 13 | logger.addHandler(logging.StreamHandler()) 14 | 15 | 16 | class RedisTokensStore(TokensStore): 17 | """ 18 | A tokens store that can stores access tokens and refresh tokens using Redis, 19 | with the given `redis.asyncio.redis` client. This tokens store configures a trace_id 20 | """ 21 | 22 | def __init__( 23 | self, 24 | client: redis.Redis, 25 | trace_cookie_name: str = "tokenstraceid", 26 | expiration_seconds: int = 60 * 120, 27 | ) -> None: 28 | super().__init__() 29 | self._client = client 30 | self._trace_id_cookie_name = trace_cookie_name 31 | self._expiration_seconds = expiration_seconds 32 | 33 | def get_trace_id(self, request: Request) -> str: 34 | trace_id = request.cookies.get(self._trace_id_cookie_name) 35 | 36 | if trace_id: 37 | return trace_id 38 | return str(uuid4()) 39 | 40 | def set_cookie( 41 | self, 42 | response: Response, 43 | cookie_name: str, 44 | value: str, 45 | secure: bool, 46 | expires: Optional[datetime] = None, 47 | path: str = "/", 48 | ) -> None: 49 | response.set_cookie( 50 | Cookie( 51 | cookie_name, 52 | value, 53 | domain=None, 54 | path=path, 55 | http_only=True, 56 | secure=secure, 57 | expires=expires, 58 | ) 59 | ) 60 | 61 | async def store_tokens( 62 | self, 63 | request: Request, 64 | response: Response, 65 | access_token: str, 66 | refresh_token: str | None, 67 | ): 68 | """ 69 | Applies a strategy to store an access token and an optional refresh token for 70 | the given request and response. 71 | """ 72 | trace_id = self.get_trace_id(request) 73 | secure = request.scheme == "https" 74 | self.set_cookie( 75 | response, 76 | self._trace_id_cookie_name, 77 | trace_id, 78 | secure=secure, 79 | expires=None, 80 | ) 81 | await self._client.set( 82 | trace_id, 83 | dumps({"access_token": access_token, "refresh_token": refresh_token}), 84 | ex=self._expiration_seconds, 85 | ) 86 | 87 | async def restore_tokens(self, request: Request) -> None: 88 | """ 89 | Applies a strategy to restore an access token and an optional refresh token for 90 | the given request. 91 | """ 92 | trace_id = request.cookies.get(self._trace_id_cookie_name) 93 | 94 | if not trace_id: 95 | return 96 | 97 | value = await self._client.get(trace_id) 98 | if not value: 99 | return 100 | 101 | try: 102 | data = loads(value) 103 | except JSONDecodeError as json_error: 104 | logger.debug( 105 | "Ignoring tokens because the cached value cannot be parsed. " 106 | "Trace id %s", 107 | trace_id, 108 | exc_info=json_error, 109 | ) 110 | else: 111 | if request.identity is None: 112 | request.identity = Identity({}) 113 | request.identity.access_token = data.get("access_token") 114 | request.identity.refresh_token = data.get("refresh_token") 115 | 116 | async def unset_tokens(self, request: Request): 117 | """ 118 | Unsets access tokens upon sign-out. 119 | """ 120 | cookie = request.cookies.get(self._trace_id_cookie_name) 121 | 122 | if cookie: 123 | await self._client.delete(cookie) 124 | -------------------------------------------------------------------------------- /proxy-1/blacksheep_proxy/server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import uvicorn 4 | from blacksheep import Application, Request, Response, StreamedContent 5 | from blacksheep.client import ClientSession 6 | from blacksheep.client.pool import ClientConnectionPools 7 | from blacksheep.headers import Headers 8 | 9 | app = Application(show_error_details=True) 10 | 11 | 12 | @app.lifespan 13 | async def register_http_client(): 14 | async with ClientSession( 15 | # This is the URL of the application to which we are proxying, 16 | # set this value in the request handler if you want to support dynamic proxies 17 | base_url="http://localhost:44777", 18 | pools=ClientConnectionPools(asyncio.get_running_loop()), 19 | ) as client: 20 | print("HTTP client created and registered as singleton") 21 | app.services.add_instance(client) 22 | yield 23 | 24 | print("HTTP client disposed") 25 | 26 | 27 | def get_content_length(headers: Headers) -> int: 28 | content_length_header = headers.get_first(b"content-length") 29 | return int(content_length_header) if content_length_header else -1 30 | 31 | 32 | def _get_proxied_request(request: Request) -> Request: 33 | """ 34 | Gets a Request for the destination server, from a request of a source client. 35 | 36 | Note: the code should probably set X-Forwarded-* headers, and related headers. 37 | This is left as an exercise! 38 | """ 39 | 40 | async def read_request_stream(): 41 | async for chunk in request.stream(): 42 | yield chunk 43 | 44 | content_length = get_content_length(request.headers) 45 | content_type = request.headers.get_first(b"Content-Type") 46 | content = ( 47 | None 48 | if content_type is None 49 | else StreamedContent( 50 | content_type or b"application/octet-stream", 51 | read_request_stream, 52 | content_length, 53 | ) 54 | ) 55 | headers = [ 56 | (key, value) 57 | for key, value in request.headers 58 | if key.lower() 59 | not in { 60 | b"content-type", 61 | b"content-length", 62 | b"transfer-encoding", 63 | } 64 | ] 65 | new_request = Request( 66 | request.method, 67 | request.url.value, 68 | headers, 69 | ) 70 | 71 | return new_request if content is None else new_request.with_content(content) 72 | 73 | 74 | def _get_proxied_response(response: Response) -> Response: 75 | """ 76 | Gets a Response for the source client, from a Response obtained from the back-end 77 | for which requests are proxied. 78 | """ 79 | 80 | # The above line completes when the original server sends the headers, but 81 | # we need to wait for the response content, too! 82 | async def response_content_reader(): 83 | async for chunk in response.stream(): 84 | yield chunk 85 | 86 | content_type = response.headers.get_first(b"Content-Type") 87 | response_headers = [ 88 | (key, value) 89 | for key, value in response.headers 90 | if key.lower() 91 | not in { 92 | b"date", 93 | b"server", 94 | b"content-type", 95 | b"content-length", 96 | b"transfer-encoding", 97 | } 98 | ] 99 | 100 | content_length = get_content_length(response.headers) 101 | 102 | content = ( 103 | StreamedContent( 104 | content_type or b"application/octet-stream", 105 | response_content_reader, 106 | content_length, 107 | ) 108 | if content_type 109 | else None 110 | ) 111 | 112 | return Response( 113 | response.status, 114 | response_headers, 115 | content=content, 116 | ) 117 | 118 | 119 | @app.route("*", methods="HEAD OPTIONS GET PATCH POST PUT DELETE".split()) 120 | async def proxy_all(request: Request, http_client: ClientSession) -> Response: 121 | proxied_request = _get_proxied_request(request) 122 | response = await http_client.send(proxied_request) 123 | return _get_proxied_response(response) 124 | 125 | 126 | uvicorn.run(app, host="localhost", port=44555, lifespan="on") # , http="h11" 127 | -------------------------------------------------------------------------------- /proxy-2/blacksheep_proxy/server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import uvicorn 4 | from blacksheep import Application, Request, Response, StreamedContent 5 | from blacksheep.client import ClientSession 6 | from blacksheep.client.pool import ClientConnectionPools 7 | from blacksheep.headers import Headers 8 | 9 | app = Application(show_error_details=True) 10 | 11 | 12 | @app.lifespan 13 | async def register_http_client(): 14 | async with ClientSession( 15 | # This is the URL of the application to which we are proxying, 16 | # set this value in the request handler if you want to support dynamic proxies 17 | base_url="http://localhost:44777", 18 | pools=ClientConnectionPools(asyncio.get_running_loop()), 19 | ) as client: 20 | print("HTTP client created and registered as singleton") 21 | app.services.add_instance(client) 22 | yield 23 | 24 | print("HTTP client disposed") 25 | 26 | 27 | def get_content_length(headers: Headers) -> int: 28 | content_length_header = headers.get_first(b"content-length") 29 | return int(content_length_header) if content_length_header else -1 30 | 31 | 32 | def _get_proxied_request(request: Request) -> Request: 33 | """ 34 | Gets a Request for the destination server, from a request of a source client. 35 | 36 | Note: the code should probably set X-Forwarded-* headers, and related headers. 37 | This is left as an exercise! 38 | """ 39 | 40 | async def read_request_stream(): 41 | async for chunk in request.stream(): 42 | yield chunk 43 | 44 | content_length = get_content_length(request.headers) 45 | content_type = request.headers.get_first(b"Content-Type") 46 | content = ( 47 | None 48 | if content_type is None 49 | else StreamedContent( 50 | content_type or b"application/octet-stream", 51 | read_request_stream, 52 | content_length, 53 | ) 54 | ) 55 | headers = [ 56 | (key, value) 57 | for key, value in request.headers 58 | if key.lower() 59 | not in { 60 | b"content-type", 61 | b"content-length", 62 | b"transfer-encoding", 63 | } 64 | ] 65 | new_request = Request( 66 | request.method, 67 | request.url.value, 68 | headers, 69 | ) 70 | 71 | return new_request if content is None else new_request.with_content(content) 72 | 73 | 74 | def _get_proxied_response(response: Response) -> Response: 75 | """ 76 | Gets a Response for the source client, from a Response obtained from the back-end 77 | for which requests are proxied. 78 | """ 79 | 80 | # The above line completes when the original server sends the headers, but 81 | # we need to wait for the response content, too! 82 | async def response_content_reader(): 83 | async for chunk in response.stream(): 84 | yield chunk 85 | 86 | content_type = response.headers.get_first(b"Content-Type") 87 | response_headers = [ 88 | (key, value) 89 | for key, value in response.headers 90 | if key.lower() 91 | not in { 92 | b"date", 93 | b"server", 94 | b"content-type", 95 | b"content-length", 96 | b"transfer-encoding", 97 | } 98 | ] 99 | 100 | content_length = get_content_length(response.headers) 101 | 102 | content = ( 103 | StreamedContent( 104 | content_type or b"application/octet-stream", 105 | response_content_reader, 106 | content_length, 107 | ) 108 | if content_type 109 | else None 110 | ) 111 | 112 | return Response( 113 | response.status, 114 | response_headers, 115 | content=content, 116 | ) 117 | 118 | 119 | @app.route("*", methods="HEAD OPTIONS GET PATCH POST PUT DELETE".split()) 120 | async def proxy_all(request: Request, http_client: ClientSession) -> Response: 121 | proxied_request = _get_proxied_request(request) 122 | response = await http_client.send(proxied_request) 123 | return _get_proxied_response(response) 124 | 125 | 126 | uvicorn.run(app, host="localhost", port=44555, lifespan="on") # , http="h11" 127 | -------------------------------------------------------------------------------- /dependency-injector/docs/example2.py: -------------------------------------------------------------------------------- 1 | from typing import Type, TypeVar, get_type_hints 2 | 3 | from dependency_injector import containers, providers 4 | 5 | from blacksheep import Application, get 6 | 7 | T = TypeVar("T") 8 | 9 | 10 | class APIClient: ... 11 | 12 | 13 | class SomeService: 14 | 15 | def __init__(self, api_client: APIClient) -> None: 16 | self.api_client = api_client 17 | 18 | 19 | # Define the Dependency Injector container 20 | class AppContainer(containers.DeclarativeContainer): 21 | api_client = providers.Singleton(APIClient) 22 | some_service = providers.Factory(SomeService, api_client=api_client) 23 | 24 | 25 | # Create the container instance 26 | container = AppContainer() 27 | 28 | 29 | class DependencyInjectorConnector: 30 | """ 31 | This class connects a Dependency Injector container with a 32 | BlackSheep application. 33 | Dependencies are registered using the code API offered by 34 | Dependency Injector. The BlackSheep application activates services 35 | using the container when needed. 36 | """ 37 | 38 | def __init__(self, container: containers.Container) -> None: 39 | self._container = container 40 | 41 | def register(self, obj_type: Type[T]) -> None: 42 | """ 43 | Registers a type with the container. 44 | The code below inspects the object's constructor's types annotations to 45 | automatically configure the provider to activate the type. 46 | 47 | It is not necessary to use @inject or Provide core on the __init__ method. This 48 | helps reducing code verbosity and keeping the source code not polluted by DI 49 | specific code. 50 | """ 51 | constructor = getattr(obj_type, "__init__", None) 52 | 53 | if not constructor: 54 | raise ValueError( 55 | f"Type {obj_type.__name__} does not have an __init__ method." 56 | ) 57 | 58 | # Get the type hints for the constructor parameters 59 | type_hints = get_type_hints(constructor) 60 | 61 | # Exclude 'self' from the parameters 62 | dependencies = { 63 | param_name: getattr(self._container, self._get_provider_name(param_type)) 64 | for param_name, param_type in type_hints.items() 65 | if param_name not in {"self", "return"} 66 | and hasattr(self._container, self._get_provider_name(param_type)) 67 | } 68 | 69 | # Create a provider for the type with its dependencies 70 | provider = providers.Factory(obj_type, **dependencies) 71 | setattr(self._container, self._get_provider_name(obj_type), provider) 72 | 73 | def resolve(self, obj_type: Type[T], _) -> T: 74 | """Resolves an instance of the given type.""" 75 | provider = getattr(self._container, self._get_provider_name(obj_type), None) 76 | if provider is None: 77 | raise TypeError( 78 | f"Type {obj_type.__name__} is not registered in the container." 79 | ) 80 | return provider() 81 | 82 | def __contains__(self, item: Type[T]) -> bool: 83 | """Checks if a type is registered in the container.""" 84 | return hasattr(self._container, item.__name__) 85 | 86 | def _get_provider_name(self, obj_type) -> str: 87 | """ 88 | Gets a provider name by object type. 89 | """ 90 | return self._to_snake_case(obj_type.__name__) 91 | 92 | def _to_snake_case(self, name: str) -> str: 93 | """ 94 | Converts a PascalCase or camelCase string to snake_case. 95 | 96 | Args: 97 | name (str): The string to convert. 98 | 99 | Returns: 100 | str: The converted string in snake_case. 101 | """ 102 | return re.sub(r"(? None: 25 | self.closing = False 26 | self._active_requests: list[ActiveRequest] = [] 27 | self._timeout: float = 60 28 | 29 | @property 30 | def active_requests(self) -> list[ActiveRequest]: 31 | return self._active_requests 32 | 33 | async def subscribe(self, request: Request): 34 | if self.closing: 35 | return text("") 36 | 37 | request_queue = asyncio.Queue() 38 | task = asyncio.create_task(self.wait_for_message(request, request_queue)) 39 | active_request = ActiveRequest(request, task, request_queue) 40 | self._active_requests.append(active_request) 41 | 42 | try: 43 | response = await task 44 | except asyncio.CancelledError: 45 | # Tasks are cancelled when the application stops, or periodically when 46 | # a request is disconnected 47 | print("Task cancelled...") 48 | return no_content() 49 | else: 50 | return response 51 | finally: 52 | try: 53 | self._active_requests.remove(active_request) 54 | except ValueError: 55 | # All is good, the item was already removed 56 | pass 57 | 58 | async def wait_for_message(self, request, queue): 59 | try: 60 | async with asyncio.timeout(self._timeout): 61 | message = await queue.get() 62 | 63 | # Note: here it is possible to check if the request is 64 | # disconnected using: if await request.is_disconnected() 65 | # 66 | # This can be useful to avoid consuming operations from this point, 67 | # or to cancel tasks. 68 | if await request.is_disconnected(): 69 | print("🔥🔥🔥 Request is disconnected!") 70 | return 71 | return text(message) 72 | except TimeoutError: 73 | # Waited for the timeout period, now closing a Long-Polling request. 74 | # The client must create a new request. 75 | return text("") 76 | 77 | async def add_message(self, message): 78 | for item in self._active_requests: 79 | await item.queue.put(message) 80 | 81 | def cancel_all_tasks(self): 82 | self.closing = True # Stop processing new requests 83 | for item in self._active_requests: 84 | item.task.cancel() 85 | 86 | def __len__(self): 87 | return len(self._active_requests) 88 | 89 | 90 | manager = MessageManager() 91 | 92 | 93 | async def periodic_check(): 94 | """ 95 | Periodically checks if active long-polling requests are disconnected, and cancels 96 | them if needed. 97 | """ 98 | while True: 99 | await asyncio.sleep(5) # Example: check every 5 seconds 100 | 101 | print("Checking active connections...") 102 | 103 | for item in manager.active_requests: 104 | request = item.request 105 | if await request.is_disconnected(): 106 | print(f"Request {id(request)} is disconnected, cancelling its task...") 107 | item.task.cancel() 108 | 109 | 110 | @app.on_start 111 | async def start_periodic_check(): 112 | asyncio.create_task(periodic_check()) 113 | 114 | 115 | @app.on_start 116 | async def configure_sigint_handler(): 117 | # See the conversation here: 118 | # https://github.com/encode/uvicorn/issues/1579#issuecomment-1419635974 119 | default_sigint_handler = signal.getsignal(signal.SIGINT) 120 | 121 | def terminate_now(signum, frame): 122 | print(f"Cancelling the tasks ({len(manager)})...") 123 | manager.cancel_all_tasks() 124 | default_sigint_handler(signum, frame) # type: ignore 125 | 126 | signal.signal(signal.SIGINT, terminate_now) 127 | 128 | 129 | @get("/subscribe") 130 | async def on_subscribe(request): 131 | return await manager.subscribe(request) 132 | 133 | 134 | @get("/stats") 135 | def get_stats(): 136 | return json({"active_requests": len(manager._active_requests)}) 137 | 138 | 139 | @post("/publish") 140 | async def publish_message(data: MessageInput): 141 | await manager.add_message(data.text) 142 | return ok() 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BlackSheep-Examples 2 | 3 | Various examples for BlackSheep. 4 | 5 | | Example | Description | 6 | | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 7 | | [./testing-api/](./testing-api) | Shows how to test a BlackSheep API using `pytest` and the provided `TestClient` (see also [testing](https://www.neoteroi.dev/blacksheep/testing/)). | 8 | | [./piccolo-admin/](./piccolo-admin) | Shows how to use the mount feature to use [Piccolo Admin](https://github.com/piccolo-orm/piccolo_admin) in BlackSheep. | 9 | | [./jwt-validation](./jwt-validation) | Shows how to configure a BlackSheep API that uses JWTs to implement authentication and authorization for users. | 10 | | [./oidc](./oidc) | Shows how to configure a BlackSheep app to use OpenID Connect and integrate with: Azure Active Directory, Auth0, Google, or Okta. | 11 | | [./aad-machine-to-machine](./aad-machine-to-machine) | Shows how to configure an API that requires access tokens issued by Azure Active Directory, and how to obtain access tokens using [MSAL for Python](https://github.com/AzureAD/microsoft-authentication-library-for-python) for a confidential client (machine to machine communication). | 12 | | [./oauth2-password-provider](./oauth2-password-provider) | Shows an implementation of OAuth2 Server with password authentication. | 13 | | [./websocket-chat](./websocket-chat) | Shows how to use WebSocket with BlackSheep, the example consists of a simple chat application built using WebSocket and VueJS. | 14 | | [./proxy-1](./proxy-1) | Shows how to create a proxy server with BlackSheep, to proxy all requests to a back-end, and return responses from that back-end. (Proxying to a Flask back-end). | 15 | | [./proxy-2](./proxy-2) | Shows how to create a proxy server with BlackSheep, to proxy all requests to a back-end, and return responses from that back-end. (Proxying to a BlackSheep back-end). | 16 | | [./max-body-size](./max-body-size) | Shows a way to control the maximum body size when reading requests streams. | 17 | | [./long-polling](./long-polling) | Shows an example of long polling implemented using BlackSheep. | 18 | | [./server-sent-events](./server-sent-events) | Shows a basic example of how to use [Server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) with BlackSheep (>=2.0.6). | 19 | | [./dependency-injector](./dependency-injector/) | Shows how to use [Dependency Injector](https://python-dependency-injector.ets-labs.org/) instead of [Rodi](https://www.neoteroi.dev/rodi/). | 20 | | [./otel](./otel/) | Shows how to use [OpenTelemetry](https://opentelemetry.io/) integration with [Grafana](https://grafana.com/), and with [Azure Application Insights](https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview). | 21 | -------------------------------------------------------------------------------- /oidc/static/jwt-decode.js: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2015 Auth0, Inc. (http://auth0.com) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | https://github.com/auth0/jwt-decode/tree/master 25 | */ 26 | (function (factory) { 27 | typeof define === 'function' && define.amd ? define(factory) : 28 | factory(); 29 | })((function () { 'use strict'; 30 | 31 | /** 32 | * The code was extracted from: 33 | * https://github.com/davidchambers/Base64.js 34 | */ 35 | 36 | var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; 37 | 38 | function InvalidCharacterError(message) { 39 | this.message = message; 40 | } 41 | 42 | InvalidCharacterError.prototype = new Error(); 43 | InvalidCharacterError.prototype.name = "InvalidCharacterError"; 44 | 45 | function polyfill(input) { 46 | var str = String(input).replace(/=+$/, ""); 47 | if (str.length % 4 == 1) { 48 | throw new InvalidCharacterError( 49 | "'atob' failed: The string to be decoded is not correctly encoded." 50 | ); 51 | } 52 | for ( 53 | // initialize result and counters 54 | var bc = 0, bs, buffer, idx = 0, output = ""; 55 | // get next character 56 | (buffer = str.charAt(idx++)); 57 | // character found in table? initialize bit storage and add its ascii value; 58 | ~buffer && 59 | ((bs = bc % 4 ? bs * 64 + buffer : buffer), 60 | // and if not first of each 4 characters, 61 | // convert the first 8 bits to one ascii character 62 | bc++ % 4) ? 63 | (output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6)))) : 64 | 0 65 | ) { 66 | // try to find character in table (0-63, not found => -1) 67 | buffer = chars.indexOf(buffer); 68 | } 69 | return output; 70 | } 71 | 72 | var atob = (typeof window !== "undefined" && 73 | window.atob && 74 | window.atob.bind(window)) || 75 | polyfill; 76 | 77 | function b64DecodeUnicode(str) { 78 | return decodeURIComponent( 79 | atob(str).replace(/(.)/g, function(m, p) { 80 | var code = p.charCodeAt(0).toString(16).toUpperCase(); 81 | if (code.length < 2) { 82 | code = "0" + code; 83 | } 84 | return "%" + code; 85 | }) 86 | ); 87 | } 88 | 89 | function base64_url_decode(str) { 90 | var output = str.replace(/-/g, "+").replace(/_/g, "/"); 91 | switch (output.length % 4) { 92 | case 0: 93 | break; 94 | case 2: 95 | output += "=="; 96 | break; 97 | case 3: 98 | output += "="; 99 | break; 100 | default: 101 | throw new Error("base64 string is not of the correct length"); 102 | } 103 | 104 | try { 105 | return b64DecodeUnicode(output); 106 | } catch (err) { 107 | return atob(output); 108 | } 109 | } 110 | 111 | function InvalidTokenError(message) { 112 | this.message = message; 113 | } 114 | 115 | InvalidTokenError.prototype = new Error(); 116 | InvalidTokenError.prototype.name = "InvalidTokenError"; 117 | 118 | function jwtDecode(token, options) { 119 | if (typeof token !== "string") { 120 | throw new InvalidTokenError("Invalid token specified: must be a string"); 121 | } 122 | 123 | options = options || {}; 124 | var pos = options.header === true ? 0 : 1; 125 | 126 | var part = token.split(".")[pos]; 127 | if (typeof part !== "string") { 128 | throw new InvalidTokenError("Invalid token specified: missing part #" + (pos + 1)); 129 | } 130 | 131 | try { 132 | var decoded = base64_url_decode(part); 133 | } catch (e) { 134 | throw new InvalidTokenError("Invalid token specified: invalid base64 for part #" + (pos + 1) + ' (' + e.message + ')'); 135 | } 136 | 137 | try { 138 | return JSON.parse(decoded); 139 | } catch (e) { 140 | throw new InvalidTokenError("Invalid token specified: invalid json for part #" + (pos + 1) + ' (' + e.message + ')'); 141 | } 142 | } 143 | 144 | /* 145 | * Expose the function on the window object 146 | */ 147 | 148 | //use amd or just through the window object. 149 | if (window) { 150 | if (typeof window.define == "function" && window.define.amd) { 151 | window.define("jwt_decode", function() { 152 | return jwtDecode; 153 | }); 154 | } else if (window) { 155 | window.jwt_decode = jwtDecode; 156 | } 157 | } 158 | 159 | })); 160 | -------------------------------------------------------------------------------- /oidc/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OIDC Example with JWT Authentication 5 | 6 | 7 | 8 | 9 |

Example OIDC integration with JWT Authentication

10 |

11 | This example avoids using Cookie based authentication: session 12 | tokens are not shared with the client using cookies, but using HTML documents 13 | that write information using the HTML5 Storage API. 14 |

15 |
16 |
17 | 18 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /otel/otel/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides middleware, context managers, and helper functions to enable 3 | distributed tracing and logging using OpenTelemetry. 4 | 5 | Features: 6 | - OTELMiddleware: Middleware for automatic tracing of HTTP requests. 7 | - Environment-based configuration for OpenTelemetry resource attributes. 8 | - Logging and tracing setup using user-provided exporters. 9 | - Context manager and decorator utilities for tracing custom operations and function calls. 10 | 11 | Usage: 12 | from otel import use_open_telemetry 13 | use_open_telemetry(app, log_exporter, span_exporter) 14 | """ 15 | 16 | import logging 17 | import os 18 | from contextlib import contextmanager 19 | from functools import wraps 20 | from typing import Awaitable, Callable, Dict 21 | 22 | from blacksheep import Application, Response 23 | from blacksheep.messages import Request, Response 24 | from blacksheep.server.env import get_env 25 | from opentelemetry import trace 26 | from opentelemetry.instrumentation.logging import LoggingInstrumentor 27 | from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler 28 | from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, LogExporter 29 | from opentelemetry.sdk.trace import TracerProvider 30 | from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter 31 | from opentelemetry.trace import SpanKind 32 | 33 | 34 | ExceptionHandler = Callable[[Request, Exception], Awaitable[Response]] 35 | 36 | 37 | class OTELMiddleware: 38 | """ 39 | Middleware configuring OpenTelemetry for all web requests. 40 | """ 41 | 42 | def __init__(self, exc_handler: ExceptionHandler) -> None: 43 | self._exc_handler = exc_handler 44 | self._tracer = trace.get_tracer(__name__) 45 | 46 | async def __call__(self, request: Request, handler): 47 | path = request.url.path.decode("utf8") 48 | method = request.method 49 | with self._tracer.start_as_current_span( 50 | f"{method} {path}", kind=SpanKind.SERVER 51 | ) as span: 52 | try: 53 | response = await handler(request) 54 | except Exception as exc: 55 | # This approach is correct because it supports controlling the response 56 | # using exceptions. Unhandled exceptions are handled by the Span. 57 | response = await self._exc_handler(request, exc) 58 | 59 | self.set_span_attributes(span, request, response, path) 60 | return response 61 | 62 | def set_span_attributes( 63 | self, span: trace.Span, request: Request, response: Response, path: str 64 | ) -> None: 65 | """ 66 | Configure the attributes on the span for a given request-response cycle. 67 | """ 68 | # To reduce cardinality, update the span name to use the 69 | # route that matched the request 70 | route = request.route # type: ignore 71 | span.update_name(f"{request.method} {route}") 72 | 73 | span.set_attribute("http.status_code", response.status) 74 | span.set_attribute("http.method", request.method) 75 | span.set_attribute("http.path", path) 76 | span.set_attribute("http.url", request.url.value.decode()) 77 | span.set_attribute("http.route", route) 78 | span.set_attribute("http.status_code", response.status) 79 | span.set_attribute("client.ip", request.original_client_ip) 80 | 81 | if response.status >= 400: 82 | span.set_status(trace.Status(trace.StatusCode.ERROR)) 83 | 84 | 85 | def _configure_logging(log_exporter: LogExporter, span_exporter: SpanExporter): 86 | log_provider = LoggerProvider() 87 | log_provider.add_log_record_processor(BatchLogRecordProcessor(log_exporter)) 88 | logging.getLogger("opentelemetry").setLevel(logging.WARNING) 89 | logging.getLogger().addHandler( 90 | LoggingHandler(level=logging.NOTSET, logger_provider=log_provider) 91 | ) 92 | 93 | logging.basicConfig( 94 | level=logging.INFO, 95 | format="%(asctime)s %(levelname)s [trace_id=%(otelTraceID)s span_id=%(otelSpanID)s] %(message)s", 96 | ) 97 | 98 | LoggingInstrumentor().instrument(set_logging_format=True) 99 | 100 | trace.set_tracer_provider(TracerProvider()) 101 | trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(span_exporter)) # type: ignore 102 | 103 | 104 | def set_attributes( 105 | service_name: str, 106 | service_namespace: str = "default", 107 | env: str = "", 108 | ): 109 | """ 110 | Sets the OTEL_RESOURCE_ATTRIBUTES environment variable with service metadata 111 | for OpenTelemetry. 112 | 113 | Args: 114 | service_name (str): The name of the service. 115 | service_namespace (str, optional): The namespace of the service. Defaults to 116 | "default". 117 | env (str, optional): The deployment environment. If not provided, it is 118 | determined from the environment. 119 | 120 | Returns: 121 | None 122 | """ 123 | if not env: 124 | env = get_env() 125 | os.environ["OTEL_RESOURCE_ATTRIBUTES"] = ( 126 | f"service.name={service_name}," 127 | f"service.namespace={service_namespace}," 128 | f"deployment.environment={env}" 129 | ) 130 | 131 | 132 | def use_open_telemetry( 133 | app: Application, 134 | log_exporter: LogExporter, 135 | span_exporter: SpanExporter, 136 | ): 137 | if os.getenv("OTEL_RESOURCE_ATTRIBUTES") is None: 138 | # set a default value 139 | set_attributes("blacksheep-app") 140 | 141 | _configure_logging(log_exporter, span_exporter) 142 | 143 | app.middlewares.append(OTELMiddleware(app.handle_request_handler_exception)) 144 | 145 | @app.on_start 146 | async def on_start(app): 147 | # Patch the router to keep track of the route pattern that matched the request, 148 | # if any 149 | # https://www.neoteroi.dev/blacksheep/routing/#how-to-track-routes-that-matched-a-request 150 | def wrap_get_route_match(fn): 151 | @wraps(fn) 152 | def get_route_match(request): 153 | match = fn(request) 154 | request.route = match.pattern.decode() if match else "Not Found" # type: ignore 155 | return match 156 | 157 | return get_route_match 158 | 159 | app.router.get_match = wrap_get_route_match(app.router.get_match) # type: ignore 160 | 161 | @app.on_stop 162 | async def on_stop(app): 163 | # Try calling shutdown() on app stop to flush all remaining spans. 164 | try: 165 | trace.get_tracer_provider().shutdown() # type: ignore 166 | except TypeError: 167 | pass 168 | 169 | 170 | @contextmanager 171 | def client_span_context( 172 | operation_name: str, attributes: Dict[str, str], *args, **kwargs 173 | ): 174 | tracer = trace.get_tracer(__name__) 175 | with tracer.start_as_current_span(operation_name, kind=SpanKind.CLIENT) as span: 176 | span.set_attributes(attributes) 177 | for i, value in enumerate(args): 178 | span.set_attribute(f"@arg{i}", str(value)) 179 | for key, value in kwargs.items(): 180 | span.set_attribute(f"@{key}", str(value)) 181 | try: 182 | yield 183 | except Exception as ex: 184 | span.record_exception(ex) 185 | span.set_attribute("ERROR", str(ex)) 186 | span.set_attribute("http.status_code", 500) 187 | span.set_status(trace.Status(trace.StatusCode.ERROR)) 188 | raise 189 | 190 | 191 | def logcall(component="Service"): 192 | """ 193 | Wraps a function to log each call using OpenTelemetry, as SpanKind.CLIENT. 194 | """ 195 | 196 | def log_decorator(fn): 197 | @wraps(fn) 198 | async def wrapper(*args, **kwargs): 199 | with client_span_context( 200 | fn.__name__, {"component": component}, *args, **kwargs 201 | ): 202 | return await fn(*args, **kwargs) 203 | 204 | return wrapper 205 | 206 | return log_decorator 207 | -------------------------------------------------------------------------------- /piccolo-admin/piccoloexample.py: -------------------------------------------------------------------------------- 1 | """ 2 | An example of how to configure and run the admin. 3 | Can be run from the command line using `python -m piccolo_admin.example`, 4 | or `admin_demo`. 5 | 6 | Refer to Piccolo-Admin-LICENSE for this file, and to the source repository: 7 | https://github.com/piccolo-orm/piccolo_admin 8 | """ 9 | import asyncio 10 | import datetime 11 | import decimal 12 | import enum 13 | import os 14 | import random 15 | import typing as t 16 | 17 | from hypercorn.asyncio import serve 18 | from hypercorn.config import Config 19 | from piccolo_api.session_auth.tables import SessionsBase 20 | from piccolo.engine.sqlite import SQLiteEngine 21 | from piccolo.engine.postgres import PostgresEngine 22 | from piccolo.apps.user.tables import BaseUser 23 | from piccolo.table import Table 24 | from piccolo.columns import ( 25 | Array, 26 | BigInt, 27 | Varchar, 28 | Integer, 29 | ForeignKey, 30 | Boolean, 31 | Interval, 32 | Text, 33 | Timestamp, 34 | Numeric, 35 | Real, 36 | SmallInt, 37 | ) 38 | from piccolo.columns.readable import Readable 39 | import targ 40 | 41 | from piccolo_admin.endpoints import create_admin 42 | from piccolo_admin.example_data import DIRECTORS, MOVIES, MOVIE_WORDS 43 | 44 | 45 | class Sessions(SessionsBase): 46 | pass 47 | 48 | 49 | class User(BaseUser, tablename="piccolo_user"): 50 | pass 51 | 52 | 53 | class Director(Table, help_text="The main director for a movie."): 54 | class Gender(enum.Enum): 55 | male = "m" 56 | female = "f" 57 | non_binary = "n" 58 | 59 | name = Varchar(length=300, null=False) 60 | years_nominated = Array( 61 | base_column=Integer(), 62 | help_text=( 63 | "Which years this director was nominated for a best director " "Oscar." 64 | ), 65 | ) 66 | gender = Varchar(length=1, choices=Gender) 67 | 68 | @classmethod 69 | def get_readable(cls): 70 | return Readable(template="%s", columns=[cls.name]) 71 | 72 | 73 | class Movie(Table): 74 | class Genre(int, enum.Enum): 75 | fantasy = 1 76 | sci_fi = 2 77 | documentary = 3 78 | horror = 4 79 | action = 5 80 | comedy = 6 81 | romance = 7 82 | musical = 8 83 | 84 | name = Varchar(length=300) 85 | rating = Real(help_text="The rating on IMDB.") 86 | duration = Interval() 87 | director = ForeignKey(references=Director) 88 | oscar_nominations = Integer() 89 | won_oscar = Boolean() 90 | description = Text() 91 | release_date = Timestamp() 92 | box_office = Numeric(digits=(5, 1), help_text="In millions of US dollars.") 93 | tags = Array(base_column=Varchar()) 94 | barcode = BigInt(default=0) 95 | genre = SmallInt(choices=Genre, null=True) 96 | 97 | 98 | TABLE_CLASSES: t.Tuple[t.Type[Table]] = (Director, Movie, User, Sessions) 99 | APP = create_admin([Director, Movie], auth_table=User, session_table=Sessions) 100 | 101 | 102 | def set_engine(engine: str = "sqlite"): 103 | if engine == "postgres": 104 | db = PostgresEngine(config={"database": "piccolo_admin"}) 105 | else: 106 | sqlite_path = os.path.join(os.path.dirname(__file__), "example.sqlite") 107 | db = SQLiteEngine(path=sqlite_path) 108 | 109 | for table_class in TABLE_CLASSES: 110 | table_class._meta._db = db 111 | 112 | 113 | def create_schema(persist: bool = False): 114 | if not persist: 115 | for table_class in reversed(TABLE_CLASSES): 116 | table_class.alter().drop_table(if_exists=True).run_sync() 117 | 118 | for table_class in TABLE_CLASSES: 119 | table_class.create_table(if_not_exists=True).run_sync() 120 | 121 | 122 | def populate_data(inflate: int = 0, engine: str = "sqlite"): 123 | """ 124 | Populate the database with some example data. 125 | :param inflate: 126 | If set, this number of extra rows are inserted containing dummy data. 127 | This is useful for testing. 128 | """ 129 | # Add some rows 130 | Director.insert(*[Director(**d) for d in DIRECTORS]).run_sync() 131 | Movie.insert(*[Movie(**m) for m in MOVIES]).run_sync() 132 | 133 | if engine == "postgres": 134 | # We need to update the sequence, as we explicitly set the IDs for the 135 | # directors we just inserted 136 | Director.raw( 137 | "SELECT setval('director_id_seq', max(id)) FROM director" 138 | ).run_sync() 139 | 140 | # Create a user for testing login 141 | user = User( 142 | username="piccolo", 143 | password="piccolo123", 144 | admin=True, 145 | email="admin@test.com", 146 | active=True, 147 | ) 148 | user.save().run_sync() 149 | 150 | if inflate: 151 | try: 152 | import faker 153 | except ImportError: 154 | print( 155 | "Install faker to use this feature: " 156 | "`pip install piccolo_admin[faker]`" 157 | ) 158 | else: 159 | fake = faker.Faker() 160 | remaining = inflate 161 | chunk_size = 100 162 | 163 | while remaining > 0: 164 | if remaining < chunk_size: 165 | chunk_size = remaining 166 | remaining = 0 167 | else: 168 | remaining = remaining - chunk_size 169 | 170 | directors = [] 171 | genders = ["m", "f", "n"] 172 | for _ in range(chunk_size): 173 | gender = random.choice(genders) 174 | if gender == "m": 175 | name = fake.name_male() 176 | elif gender == "f": 177 | name = fake.name_female() 178 | else: 179 | name = fake.name_nonbinary() 180 | directors.append(Director(name=name, gender=gender)) 181 | 182 | Director.insert(*directors).run_sync() 183 | 184 | director_ids = ( 185 | Director.select(Director.id) 186 | .order_by(Director.id, ascending=False) 187 | .limit(chunk_size) 188 | .output(as_list=True) 189 | .run_sync() 190 | ) 191 | 192 | movies = [] 193 | genres = [i.value for i in Movie.Genre] 194 | for _ in range(chunk_size): 195 | oscar_nominations = random.sample([0, 0, 0, 0, 0, 1, 1, 3, 5], 1)[0] 196 | won_oscar = oscar_nominations > 0 197 | rating = ( 198 | random.randint(80, 100) if won_oscar else random.randint(1, 100) 199 | ) / 10 200 | 201 | movie = Movie( 202 | name="{} {}".format( 203 | fake.word().title(), 204 | fake.word(ext_word_list=MOVIE_WORDS), 205 | ), 206 | rating=rating, 207 | duration=datetime.timedelta(minutes=random.randint(60, 210)), 208 | director=random.sample(director_ids, 1)[0], 209 | oscar_nominations=oscar_nominations, 210 | won_oscar=won_oscar, 211 | description=fake.sentence(30), 212 | release_date=fake.date_time(), 213 | box_office=decimal.Decimal(str(random.randint(10, 1500) / 10)), 214 | barcode=random.randint(1_000_000_000, 9_999_999_999), 215 | genre=random.choice(genres), 216 | ) 217 | movies.append(movie) 218 | 219 | Movie.insert(*movies).run_sync() 220 | 221 | 222 | def run(persist: bool = False, engine: str = "sqlite", inflate: int = 0): 223 | """ 224 | Start the Piccolo admin. 225 | :param persist: 226 | If True, we don't rebuild all of the data each time. 227 | :param engine: 228 | Options are sqlite and postgres. By default sqlite is used. 229 | :param inflate: 230 | If set, this number of extra rows are inserted containing dummy data. 231 | This is useful when you need to test with lots of data. Example 232 | `--inflate=10000`. 233 | """ 234 | set_engine(engine) 235 | create_schema(persist=persist) 236 | 237 | if not persist: 238 | populate_data(inflate=inflate, engine=engine) 239 | 240 | # Server 241 | class CustomConfig(Config): 242 | use_reloader = True 243 | accesslog = "-" 244 | 245 | asyncio.run(serve(APP, CustomConfig())) 246 | 247 | 248 | def main(): 249 | cli = targ.CLI(description="Piccolo Admin") 250 | cli.register(run) 251 | cli.run(solo=True) 252 | 253 | 254 | if __name__ == "__main__": 255 | main() 256 | -------------------------------------------------------------------------------- /oauth2-password-provider/src/password_auth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | from dataclasses import asdict, dataclass 5 | from datetime import datetime, timedelta 6 | from enum import Enum 7 | from json import JSONEncoder 8 | from typing import Any, Iterable, List, Literal, Optional, Union 9 | from uuid import UUID, uuid4 10 | 11 | import jwt 12 | from blacksheep import Application 13 | from blacksheep.messages import Request, Response 14 | from blacksheep.server.authorization import allow_anonymous 15 | from blacksheep.server.headers.cache import cache_control 16 | from blacksheep.server.responses import json, no_content 17 | from essentials.exceptions import UnauthorizedException 18 | from guardpost.asynchronous.authentication import AuthenticationHandler 19 | from guardpost.authentication import Identity 20 | from guardpost.authorization import Policy 21 | from guardpost.common import AuthenticatedRequirement 22 | from jwt import InvalidTokenError 23 | from pydantic import UUID4, BaseModel 24 | 25 | from .user import UserDAL, checkpw 26 | 27 | logger = logging.getLogger(__name__) 28 | 29 | 30 | class UUIDJSONEncode(JSONEncoder): 31 | def default(self, obj: Any): 32 | if isinstance(obj, UUID): 33 | return obj.hex 34 | return JSONEncoder.default(self, obj) 35 | 36 | 37 | class TokenPayload(BaseModel): 38 | """Token model.""" 39 | 40 | sub: UUID4 41 | jti: UUID4 42 | sid: UUID4 43 | iat: datetime 44 | nbf: datetime 45 | exp: datetime 46 | typ: Literal["access", "refresh"] 47 | 48 | 49 | class HMACAlgorithm(Enum): 50 | """HMAC algorithms.""" 51 | 52 | HS256 = "HS256" 53 | HS384 = "HS384" 54 | HS512 = "HS512" 55 | 56 | 57 | @dataclass 58 | class OAuth2PasswordAuthData: 59 | """Dataclass for OAuth2 Password flow.""" 60 | 61 | username: Optional[str] = None 62 | password: Optional[str] = None 63 | 64 | 65 | @dataclass 66 | class OAuth2PasswordRefreshData: 67 | """Dataclass for OAuth2 Password flow.""" 68 | 69 | refresh_token: Optional[str] = None 70 | 71 | 72 | @dataclass 73 | class OAuth2PasswordSettings: 74 | """Settings for OAuth2 Password flow.""" 75 | 76 | secret: str 77 | algorithm: HMACAlgorithm = HMACAlgorithm.HS256 78 | issuer: Optional[str] = None 79 | audience: Optional[Union[str, Iterable[str]]] = None 80 | token_path: str = "/token" 81 | refresh_path: str = "/refresh" 82 | revoke_path: str = "/revoke" 83 | access_token_ttl: int = 60 * 60 84 | refresh_token_ttl: int = 60 * 60 * 24 * 30 85 | username_field: str = "username" 86 | password_field: str = "password" 87 | 88 | 89 | class FailedTokenDecode(Exception): 90 | """Raised when a token cannot be decoded.""" 91 | 92 | 93 | class HMACJWTSerializerBase: 94 | """Base class for JWT token generatoration/extraction.""" 95 | 96 | def __init__( 97 | self, 98 | secret: str, 99 | algorithm: HMACAlgorithm = HMACAlgorithm.HS256, 100 | issuer: Optional[str] = None, 101 | audience: Optional[Union[str, Iterable[str]]] = None, 102 | verify_options: Optional[dict[str, Any]] = None, 103 | ): 104 | self.secret = secret 105 | self.algorithm = algorithm 106 | self.issuer = issuer 107 | self.audience = audience 108 | self.verify_options = verify_options 109 | 110 | def encode(self, payload: dict) -> str: 111 | """Encode a payload into a JWT token.""" 112 | raise NotImplementedError() 113 | 114 | def decode(self, token: str) -> dict: 115 | """Decode a JWT token into a payload.""" 116 | raise NotImplementedError() 117 | 118 | 119 | class HMACJWTSerializer(HMACJWTSerializerBase): 120 | """JWT token encode/decode using HMAC.""" 121 | 122 | def encode(self, payload: dict) -> str: 123 | """Encode a payload into a JWT token.""" 124 | return jwt.encode( 125 | payload=payload, 126 | key=self.secret, 127 | algorithm=self.algorithm.value, 128 | json_encoder=UUIDJSONEncode, 129 | ) 130 | 131 | def decode(self, token: str) -> dict: 132 | """Decode a JWT token into a payload.""" 133 | try: 134 | return jwt.decode( 135 | jwt=token, 136 | key=self.secret, 137 | algorithms=[self.algorithm.value], 138 | issuer=self.issuer, 139 | audience=self.audience, 140 | options=self.verify_options, 141 | ) 142 | except InvalidTokenError as e: 143 | logger.debug("Failed to decode token", exc_info=e) 144 | raise FailedTokenDecode from e 145 | 146 | 147 | class BearerAuthentication(AuthenticationHandler): 148 | """Authentication handler for Bearer tokens with dynamic validation.""" 149 | 150 | token_type = b"Bearer" 151 | header_name = b"Authorization" 152 | 153 | def __init__(self, serializer: HMACJWTSerializerBase): 154 | self.serializer = serializer 155 | 156 | async def authenticate(self, request: Request) -> Optional[Identity]: 157 | raw_token = self._get_request_token(request) 158 | if not raw_token: 159 | request.identity = Identity({}) 160 | return None 161 | 162 | try: 163 | token = self.serializer.decode(raw_token) 164 | TokenPayload.parse_obj(token) 165 | except FailedTokenDecode: 166 | request.identity = Identity({}) 167 | return None 168 | 169 | if token["typ"] != "access": 170 | request.identity = Identity({}) 171 | return None 172 | 173 | request.identity = Identity(token, self.token_type.decode()) 174 | 175 | return request.identity 176 | 177 | def _get_request_token(self, request: Request) -> str | None: 178 | auth_header = request.headers.get_first(self.header_name) 179 | if not auth_header: 180 | return None 181 | if auth_header.startswith(self.token_type + b" ") is False: 182 | return None 183 | 184 | try: 185 | token = auth_header[7:].decode() 186 | except UnicodeDecodeError: 187 | return None 188 | 189 | return token 190 | 191 | 192 | class AuthProviderBase: 193 | """Base class for authentication storage. 194 | 195 | Verifies creadentials, refresh and revoke tokens. 196 | """ 197 | 198 | async def authenticate(self, username: str, password: str) -> Identity: 199 | """Authenticate a user. 200 | 201 | Check if the user exists and the password is correct. Returns an identity with access and refresh tokens. 202 | """ 203 | raise NotImplementedError() 204 | 205 | async def refresh_token(self, raw_token: str) -> Identity: 206 | """Verify a refresh token. 207 | 208 | Check if the refresh token is valid and not expired. Returns an identity with access and refresh tokens. 209 | """ 210 | raise NotImplementedError() 211 | 212 | async def revoke_refresh_token(self, session_id: Any) -> None: 213 | """Revoke a refresh token. 214 | 215 | Remove the refresh token from the storage for a given session id. 216 | """ 217 | raise NotImplementedError() 218 | 219 | 220 | class AppAuthProvider(AuthProviderBase): 221 | def __init__( 222 | self, 223 | storage: UserDAL, 224 | serializer: HMACJWTSerializerBase, 225 | settings: OAuth2PasswordSettings, 226 | ): 227 | self.storage = storage 228 | self.serializer = serializer 229 | self.settings = settings 230 | 231 | async def authenticate(self, username: str, password: str) -> Identity: 232 | user = await self.storage.get_authuser_by_username(username) 233 | if not user: 234 | raise UnauthorizedException("User or password is invalid") 235 | if not checkpw(password, user.password): 236 | raise UnauthorizedException("User or password is invalid") 237 | if not user.active: 238 | raise UnauthorizedException("User or password is invalid") 239 | 240 | access_payload, refresh_payload = self._get_tokens_pair(user.id) 241 | access_token, refresh_token = self._encode_tokens( 242 | access_payload, 243 | refresh_payload, 244 | ) 245 | await self._store_refresh_token(refresh_payload) 246 | 247 | identity = self._make_identity(access_payload, access_token, refresh_token) 248 | return identity 249 | 250 | def _make_identity( 251 | self, payload: TokenPayload, access_token: str, refresh_token: str 252 | ) -> Identity: 253 | identity = Identity(payload.dict()) 254 | identity.access_token = access_token 255 | identity.refresh_token = refresh_token 256 | return identity 257 | 258 | def _get_tokens_pair( 259 | self, sub: UUID4, sid: UUID4 | None = None 260 | ) -> tuple[TokenPayload, TokenPayload]: 261 | sid = sid or uuid4() 262 | jti = uuid4() 263 | now = datetime.utcnow() 264 | access = TokenPayload( 265 | sub=sub, 266 | jti=jti, 267 | sid=sid, 268 | iat=now, 269 | nbf=now, 270 | exp=now + timedelta(minutes=self.settings.access_token_ttl), 271 | typ="access", 272 | ) 273 | refresh = TokenPayload( 274 | sub=sub, 275 | jti=jti, 276 | sid=sid, 277 | iat=now, 278 | nbf=now, 279 | exp=now + timedelta(minutes=self.settings.refresh_token_ttl), 280 | typ="refresh", 281 | ) 282 | return access, refresh 283 | 284 | def _encode_tokens( 285 | self, 286 | access_payload: TokenPayload, 287 | refresh_payload: TokenPayload, 288 | ) -> tuple[str, str]: 289 | access_token = self.serializer.encode(access_payload.dict()) 290 | refresh_token = self.serializer.encode(refresh_payload.dict()) 291 | return access_token, refresh_token 292 | 293 | async def _store_refresh_token(self, token_payload: TokenPayload) -> None: 294 | await self.storage.save_refresh_token( 295 | token_payload.sub, 296 | token_payload.jti, 297 | token_payload.sid, 298 | token_payload.exp, 299 | ) 300 | 301 | async def _remove_used_refresh_token(self, jti: UUID4): 302 | await self.storage.remove_used_refresh_token(jti) 303 | 304 | async def refresh_token(self, raw_token: str) -> Identity: 305 | try: 306 | used_refresh_token = self.serializer.decode(raw_token) 307 | except FailedTokenDecode as e: 308 | raise UnauthorizedException( 309 | f"Invalid refresh token: {FailedTokenDecode}", 310 | ) from e 311 | 312 | if used_refresh_token["typ"] != "refresh": 313 | raise UnauthorizedException("Invalid type of refresh token") 314 | 315 | user = await self.storage.get_user_by_refresh_token( 316 | UUID(used_refresh_token["sub"]), 317 | UUID(used_refresh_token["jti"]), 318 | UUID(used_refresh_token["sid"]), 319 | ) 320 | if not user: 321 | # someone is trying to use already used refresh token. revoke all refresh tokens for this user 322 | await self.revoke_refresh_token(used_refresh_token["sub"]) 323 | raise UnauthorizedException("Invalid refresh token") 324 | 325 | # TODO: wrap delete old and store new refresh token to transaction 326 | access_payload, refresh_payload = self._get_tokens_pair( 327 | sub=user.id, 328 | sid=used_refresh_token["sid"], 329 | ) 330 | access_token, refresh_token = self._encode_tokens( 331 | access_payload, refresh_payload 332 | ) 333 | await self._store_refresh_token(refresh_payload) 334 | await self._remove_used_refresh_token(UUID(used_refresh_token["jti"])) 335 | identity = self._make_identity(access_payload, access_token, refresh_token) 336 | return identity 337 | 338 | async def revoke_refresh_token(self, user_id: str) -> None: 339 | await self.storage.revoke_refresh_token(UUID(user_id)) 340 | 341 | 342 | class OAuth2PasswordHandler: 343 | def __init__( 344 | self, 345 | *, 346 | settings: OAuth2PasswordSettings, 347 | auth_provider: AuthProviderBase, 348 | ): 349 | self.settings = settings 350 | self.auth_provider = auth_provider 351 | 352 | async def token_handler(self, request: Request) -> Response: 353 | """Handler for OAuth2 Password flow. 354 | 355 | Issue access and refresh tokens when user logs in. 356 | 357 | Response headers should contain: 358 | Cache-Control: no-store 359 | Pragma: no-cache 360 | """ 361 | userdata = await self._fetch_credentials(request) 362 | identity = await self.auth_provider.authenticate(**asdict(userdata)) 363 | if not identity: 364 | raise UnauthorizedException("Invalid username or password") 365 | return json( 366 | dict( 367 | access_token=identity.access_token, 368 | refresh_token=identity.refresh_token, 369 | token_type="Bearer", 370 | ) 371 | ) 372 | 373 | async def _fetch_credentials(self, request: Request) -> OAuth2PasswordAuthData: 374 | """Extract user credentials from request.""" 375 | content_type = request.headers.get_first(b"Content-Type") 376 | if content_type == b"application/x-www-form-urlencoded": 377 | userdata = await self._form_userdata(request) 378 | elif content_type == b"application/json": 379 | userdata = await self._json_userdata(request) 380 | else: 381 | raise UnauthorizedException( 382 | "Unsupported Content-Type. Supported: application/x-www-form-urlencoded, application/json" 383 | ) 384 | 385 | return userdata 386 | 387 | async def _form_userdata(self, request: Request) -> OAuth2PasswordAuthData: 388 | """Extract user credentials from form payload.""" 389 | form = await request.form() 390 | if not form: 391 | raise ValueError("Cannot parse form data") 392 | username = form.get(self.settings.username_field, None) 393 | if username is None or not isinstance(username, str): 394 | raise ValueError("Username filed is required and must be a string") 395 | password = form.get(self.settings.password_field, None) 396 | if password is None or not isinstance(password, str): 397 | raise ValueError("Password filed is required and must be a string") 398 | return OAuth2PasswordAuthData( 399 | username=username, 400 | password=password, 401 | ) 402 | 403 | async def _json_userdata(self, request: Request) -> OAuth2PasswordAuthData: 404 | """Extract user credentials from JSON payload.""" 405 | data = await request.json() 406 | username = data.get(self.settings.username_field) 407 | if username is None or not isinstance(username, str): 408 | raise ValueError("Username filed is required and must be a string") 409 | password = data.get(self.settings.password_field) 410 | if password is None or not isinstance(password, str): 411 | raise ValueError("Password filed is required and must be a string") 412 | return OAuth2PasswordAuthData( 413 | username=username, 414 | password=password, 415 | ) 416 | 417 | async def refresh_handler(self, request: Request) -> Response: 418 | """Handler for OAuth2 Refresh flow. 419 | 420 | Issue new access and refresh tokens when user make request with refresh token. 421 | 422 | Response headers should contain: 423 | Cache-Control: no-store 424 | Pragma: no-cache 425 | """ 426 | refreshdata = await self._fetch_refresh_token(request) 427 | identity = await self.auth_provider.refresh_token(**asdict(refreshdata)) 428 | if not identity: 429 | raise UnauthorizedException("Invalid refresh token") 430 | return json( 431 | dict( 432 | access_token=identity.access_token, 433 | refresh_token=identity.refresh_token, 434 | token_type="Bearer", 435 | ) 436 | ) 437 | 438 | async def _fetch_refresh_token(self, request: Request) -> OAuth2PasswordRefreshData: 439 | """Extract refresh token from request.""" 440 | content_type = request.headers.get_first(b"Content-Type") 441 | if content_type == b"application/x-www-form-urlencoded": 442 | refreshdata = await self._form_refreshdata(request) 443 | elif content_type == b"application/json": 444 | refreshdata = await self._json_refreshdata(request) 445 | else: 446 | raise ValueError( 447 | "Unsupported Content-Type. Supported: application/x-www-form-urlencoded, application/json" 448 | ) 449 | 450 | return refreshdata 451 | 452 | async def _form_refreshdata(self, request: Request) -> OAuth2PasswordRefreshData: 453 | """Extract refresh token from form payload.""" 454 | form = await request.form() 455 | if not form: 456 | raise ValueError("Cannot parse form data") 457 | refresh_token = form.get("refresh_token", None) 458 | if refresh_token is None or not isinstance(refresh_token, str): 459 | raise ValueError("Refresh token filed is required and must be a string") 460 | return OAuth2PasswordRefreshData( 461 | refresh_token=refresh_token, 462 | ) 463 | 464 | async def _json_refreshdata(self, request: Request) -> OAuth2PasswordRefreshData: 465 | """Extract refresh token from JSON payload.""" 466 | data = await request.json() 467 | refresh_token = data.get("refresh_token") 468 | if refresh_token is None or not isinstance(refresh_token, str): 469 | raise ValueError("Refresh token field is required and must be a string") 470 | return OAuth2PasswordRefreshData( 471 | refresh_token=refresh_token, 472 | ) 473 | 474 | async def revoke_handler(self, request: Request) -> Response: 475 | """Handler for revoke token. 476 | 477 | Revoke refresh tokens and/or access tokens during logout. 478 | """ 479 | if not request.identity or request.identity.is_authenticated() is False: 480 | raise UnauthorizedException("Authentication required") 481 | session_id = request.identity["session_id"] 482 | if session_id is None or not isinstance(session_id, str): 483 | raise ValueError("Session ID filed is required and must be a string") 484 | await self.auth_provider.revoke_refresh_token(session_id) 485 | 486 | return no_content() 487 | 488 | 489 | def use_oauth2_password( 490 | app: Application, 491 | settings: OAuth2PasswordSettings, 492 | auth_provider: Optional[AuthProviderBase] = None, 493 | auth_handler: Optional[AuthenticationHandler] = None, 494 | authz_policies: Optional[List[Policy]] = None, 495 | ): 496 | """Register OAuth2 Password handlers.""" 497 | 498 | serializer = HMACJWTSerializer( 499 | secret=settings.secret, 500 | algorithm=settings.algorithm, 501 | issuer=settings.issuer, 502 | audience=settings.audience, 503 | verify_options={ 504 | "verify_exp": True, 505 | "verify_iat": True, 506 | "verify_nbf": True, 507 | "verify_signature": True, 508 | }, 509 | ) 510 | 511 | auth_handler = auth_handler or BearerAuthentication( 512 | serializer, 513 | ) 514 | 515 | authz_policies = authz_policies or [ 516 | Policy("authenticated", AuthenticatedRequirement()) 517 | ] 518 | 519 | auth_provider = auth_provider or AppAuthProvider( 520 | storage=UserDAL(), 521 | serializer=serializer, 522 | settings=settings, 523 | ) 524 | handler = OAuth2PasswordHandler( 525 | settings=settings, 526 | auth_provider=auth_provider, 527 | ) 528 | 529 | @allow_anonymous() 530 | @app.router.post(settings.token_path) 531 | @cache_control(no_cache=True, no_store=True) 532 | async def token_handler(request: Request): 533 | return await handler.token_handler(request) 534 | 535 | @allow_anonymous() 536 | @app.router.post(settings.refresh_path) 537 | @cache_control(no_cache=True, no_store=True) 538 | async def refresh_handler(request: Request): 539 | return await handler.refresh_handler(request) 540 | 541 | @app.router.get(settings.revoke_path) 542 | @cache_control(no_cache=True, no_store=True) 543 | async def revoke_handler(request: Request): 544 | return await handler.revoke_handler(request) 545 | 546 | authentication = app.use_authentication() 547 | authentication.add(auth_handler) 548 | 549 | authorization = app.use_authorization() 550 | for policy in authz_policies: 551 | authorization.add(policy) 552 | --------------------------------------------------------------------------------