From 2426e0675c37d4513ce2cddb9ba9b7af0c930407 Mon Sep 17 00:00:00 2001 From: Johan Lundberg Date: Mon, 16 Feb 2026 13:24:54 +0100 Subject: [PATCH] feat: add idpyoidc server initialization --- src/fastapi_oidc_op/oidc/provider.py | 136 +++++++++++++++++++++++++++ tests/test_oidc/test_provider.py | 56 +++++++++++ 2 files changed, 192 insertions(+) create mode 100644 src/fastapi_oidc_op/oidc/provider.py create mode 100644 tests/test_oidc/test_provider.py diff --git a/src/fastapi_oidc_op/oidc/provider.py b/src/fastapi_oidc_op/oidc/provider.py new file mode 100644 index 0000000..fc481c1 --- /dev/null +++ b/src/fastapi_oidc_op/oidc/provider.py @@ -0,0 +1,136 @@ +"""idpyoidc Server initialization.""" + +from pathlib import Path + +from idpyoidc.server import Server + +from fastapi_oidc_op.config import Settings +from fastapi_oidc_op.oidc.claims import PorchlightUserInfo + + +def _build_server_config(settings: Settings) -> dict: + """Build the idpyoidc configuration dict from application settings.""" + key_path = Path(settings.signing_key_path) + key_path.mkdir(parents=True, exist_ok=True) + + return { + "issuer": settings.issuer, + "key_conf": { + "key_defs": [ + {"type": "RSA", "use": ["sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]}, + ], + "private_path": str(key_path / "private_jwks.json"), + "public_path": str(key_path / "public_jwks.json"), + "uri_path": "jwks", + "read_only": False, + }, + "endpoint": { + "provider_config": { + "path": ".well-known/openid-configuration", + "class": "idpyoidc.server.oidc.provider_config.ProviderConfiguration", + "kwargs": {}, + }, + "authorization": { + "path": "authorization", + "class": "idpyoidc.server.oidc.authorization.Authorization", + "kwargs": {}, + }, + "token": { + "path": "token", + "class": "idpyoidc.server.oidc.token.Token", + "kwargs": {}, + }, + "userinfo": { + "path": "userinfo", + "class": "idpyoidc.server.oidc.userinfo.UserInfo", + "kwargs": {}, + }, + }, + "userinfo": { + "class": PorchlightUserInfo, + "kwargs": {}, + }, + "authz": { + "class": "idpyoidc.server.authz.AuthzHandling", + "kwargs": { + "grant_config": { + "usage_rules": { + "authorization_code": { + "supports_minting": ["access_token", "refresh_token", "id_token"], + "max_usage": 1, + "expires_in": 120, + }, + "access_token": { + "expires_in": 3600, + }, + "refresh_token": { + "supports_minting": ["access_token", "refresh_token", "id_token"], + "expires_in": 86400, + }, + }, + "expires_in": 2592000, + }, + }, + }, + "token_handler_args": { + "jwks_def": { + "private_path": str(key_path / "token_jwks.json"), + "read_only": False, + "key_defs": [{"type": "oct", "bytes": "24", "use": ["enc"], "kid": "code"}], + }, + "code": { + "kwargs": {"lifetime": 600}, + }, + "token": { + "class": "idpyoidc.server.token.jwt_token.JWTToken", + "kwargs": { + "lifetime": 3600, + "add_claims_by_scope": True, + "aud": [settings.issuer], + }, + }, + "refresh": { + "class": "idpyoidc.server.token.jwt_token.JWTToken", + "kwargs": { + "lifetime": 86400, + "aud": [settings.issuer], + }, + }, + "id_token": { + "class": "idpyoidc.server.token.id_token.IDToken", + "kwargs": { + "lifetime": 3600, + }, + }, + }, + "scopes_to_claims": { + "openid": ["sub"], + "profile": [ + "name", + "given_name", + "family_name", + "middle_name", + "nickname", + "profile", + "picture", + "website", + "gender", + "birthdate", + "zoneinfo", + "locale", + "updated_at", + "preferred_username", + ], + "email": ["email", "email_verified"], + "phone": ["phone_number", "phone_number_verified"], + }, + "authentication": {}, + } + + +def create_oidc_server(settings: Settings) -> Server: + """Create and configure an idpyoidc Server instance.""" + config = _build_server_config(settings) + server = Server(conf=config) + return server diff --git a/tests/test_oidc/test_provider.py b/tests/test_oidc/test_provider.py new file mode 100644 index 0000000..8b73e17 --- /dev/null +++ b/tests/test_oidc/test_provider.py @@ -0,0 +1,56 @@ +import shutil +from pathlib import Path + +from fastapi_oidc_op.config import Settings +from fastapi_oidc_op.oidc.provider import create_oidc_server + + +def test_create_server_has_endpoints() -> None: + key_path = Path("test_keys_provider") + key_path.mkdir(exist_ok=True) + try: + settings = Settings(issuer="http://localhost:8000", sqlite_path=":memory:", signing_key_path=str(key_path)) + server = create_oidc_server(settings) + assert "authorization" in server.endpoint + assert "token" in server.endpoint + assert "userinfo" in server.endpoint + assert "provider_config" in server.endpoint + finally: + shutil.rmtree(key_path, ignore_errors=True) + + +def test_create_server_has_issuer() -> None: + key_path = Path("test_keys_issuer") + key_path.mkdir(exist_ok=True) + try: + settings = Settings(issuer="http://localhost:8000", sqlite_path=":memory:", signing_key_path=str(key_path)) + server = create_oidc_server(settings) + assert server.context.issuer == "http://localhost:8000" + finally: + shutil.rmtree(key_path, ignore_errors=True) + + +def test_create_server_jwks_available() -> None: + key_path = Path("test_keys_jwks") + key_path.mkdir(exist_ok=True) + try: + settings = Settings(issuer="http://localhost:8000", sqlite_path=":memory:", signing_key_path=str(key_path)) + server = create_oidc_server(settings) + keys = server.keyjar.export_jwks() + assert "keys" in keys + assert len(keys["keys"]) > 0 + finally: + shutil.rmtree(key_path, ignore_errors=True) + + +def test_create_server_userinfo_is_porchlight() -> None: + key_path = Path("test_keys_userinfo") + key_path.mkdir(exist_ok=True) + try: + settings = Settings(issuer="http://localhost:8000", sqlite_path=":memory:", signing_key_path=str(key_path)) + server = create_oidc_server(settings) + from fastapi_oidc_op.oidc.claims import PorchlightUserInfo + + assert isinstance(server.context.userinfo, PorchlightUserInfo) + finally: + shutil.rmtree(key_path, ignore_errors=True)