Blind authentication (#675)

* auth server

* cleaning up

* auth ledger class

* class variables -> instance variables

* annotations

* add models and api route

* custom amount and api prefix

* add auth db

* blind auth token working

* jwt working

* clean up

* JWT works

* using openid connect server

* use oauth server with password flow

* new realm

* add keycloak docker

* hopefully not garbage

* auth works

* auth kinda working

* fix cli

* auth works for send and receive

* pass auth_db to Wallet

* auth in info

* refactor

* fix supported

* cache mint info

* fix settings and endpoints

* add description to .env.example

* track changes for openid connect client

* store mint in db

* store credentials

* clean up v1_api.py

* load mint info into auth wallet

* fix first login

* authenticate if refresh token fails

* clear auth also middleware

* use regex

* add cli command

* pw works

* persist keyset amounts

* add errors.py

* do not start auth server if disabled in config

* upadte poetry

* disvoery url

* fix test

* support device code flow

* adopt latest spec changes

* fix code flow

* mint max bat dynamic

* mypy ignore

* fix test

* do not serialize amount in authproof

* all auth flows working

* fix tests

* submodule

* refactor

* test

* dont sleep

* test

* add wallet auth tests

* test differently

* test only keycloak for now

* fix creds

* daemon

* fix test

* install everything

* install jinja

* delete wallet for every test

* auth: use global rate limiter

* test auth rate limit

* keycloak hostname

* move keycloak test data

* reactivate all tests

* add readme

* load proofs

* remove unused code

* remove unused code

* implement change suggestions by ok300

* add error codes

* test errors
This commit is contained in:
callebtc
2025-01-29 22:48:51 -06:00
committed by GitHub
parent b67ffd8705
commit a0ef44dba0
58 changed files with 8188 additions and 701 deletions

View File

@@ -75,16 +75,26 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
db_read: DbReadHelper
invoice_listener_tasks: List[asyncio.Task] = []
disable_melt: bool = False
pubkey: PublicKey
def __init__(
self,
*,
db: Database,
seed: str,
backends: Mapping[Method, Mapping[Unit, LightningBackend]],
seed_decryption_key: Optional[str] = None,
derivation_path="",
amounts: Optional[List[int]] = None,
backends: Optional[Mapping[Method, Mapping[Unit, LightningBackend]]] = None,
seed_decryption_key: Optional[str] = None,
crud=LedgerCrudSqlite(),
):
) -> None:
self.keysets: Dict[str, MintKeyset] = {}
self.backends: Mapping[Method, Mapping[Unit, LightningBackend]] = {}
self.events = LedgerEventManager()
self.db_read: DbReadHelper
self.locks: Dict[str, asyncio.Lock] = {} # holds multiprocessing locks
self.invoice_listener_tasks: List[asyncio.Task] = []
if not seed:
raise Exception("seed not set")
@@ -103,24 +113,33 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
self.db = db
self.crud = crud
self.backends = backends
if backends:
self.backends = backends
if amounts:
self.amounts = amounts
else:
self.amounts = [2**n for n in range(settings.max_order)]
self.pubkey = derive_pubkey(self.seed)
self.db_read = DbReadHelper(self.db, self.crud)
self.db_write = DbWriteHelper(self.db, self.crud, self.events, self.db_read)
# ------- STARTUP -------
async def startup_ledger(self):
await self._startup_ledger()
async def startup_ledger(self) -> None:
await self._startup_keysets()
await self._check_backends()
await self._check_pending_proofs_and_melt_quotes()
self.invoice_listener_tasks = await self.dispatch_listeners()
async def _startup_ledger(self):
async def _startup_keysets(self) -> None:
await self.init_keysets()
for derivation_path in settings.mint_derivation_path_list:
await self.activate_keyset(derivation_path=derivation_path)
async def _check_backends(self) -> None:
for method in self.backends:
for unit in self.backends[method]:
logger.info(
@@ -139,7 +158,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
logger.info(f"Data dir: {settings.cashu_dir}")
async def shutdown_ledger(self):
async def shutdown_ledger(self) -> None:
await self.db.engine.dispose()
for task in self.invoice_listener_tasks:
task.cancel()
@@ -169,57 +188,65 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe
version: Optional[str] = None,
autosave=True,
) -> MintKeyset:
"""Load the keyset for a derivation path if it already exists. If not generate new one and store in the db.
"""
Load an existing keyset for the specified derivation path or generate a new one if it doesn't exist.
Optionally store the newly created keyset in the database.
Args:
derivation_path (_type_): Derivation path from which the keyset is generated.
autosave (bool, optional): Store newly-generated keyset if not already in database. Defaults to True.
derivation_path (str): Derivation path for keyset generation.
seed (Optional[str], optional): Seed value. Defaults to None.
version (Optional[str], optional): Version identifier. Defaults to None.
autosave (bool, optional): Whether to store the keyset if newly created. Defaults to True.
Returns:
MintKeyset: Keyset
MintKeyset: The activated keyset.
"""
if not derivation_path:
raise Exception("derivation path not set")
raise ValueError("Derivation path must be provided.")
seed = seed or self.seed
tmp_keyset_local = MintKeyset(
version = version or settings.version
# Initialize a temporary keyset to derive the ID
temp_keyset = MintKeyset(
seed=seed,
derivation_path=derivation_path,
version=version or settings.version,
version=version,
amounts=self.amounts,
)
logger.debug(
f"Activating keyset for derivation path {derivation_path} with id"
f" {tmp_keyset_local.id}."
f"Activating keyset for derivation path '{derivation_path}' with ID '{temp_keyset.id}'."
)
# load the keyset from db
logger.trace(f"crud: loading keyset for {derivation_path}")
tmp_keysets_local: List[MintKeyset] = await self.crud.get_keyset(
id=tmp_keyset_local.id, db=self.db
# Attempt to retrieve existing keysets from the database
existing_keysets: List[MintKeyset] = await self.crud.get_keyset(
id=temp_keyset.id, db=self.db
)
logger.trace(f"crud: loaded {len(tmp_keysets_local)} keysets")
if tmp_keysets_local:
# we have a keyset with this derivation path in the database
keyset = tmp_keysets_local[0]
logger.trace(
f"Retrieved {len(existing_keysets)} keyset(s) for derivation path '{derivation_path}'."
)
if existing_keysets:
keyset = existing_keysets[0]
else:
# no keyset for this derivation path yet
# we create a new keyset (keys will be generated at instantiation)
# Create a new keyset if none exists
keyset = MintKeyset(
seed=seed or self.seed,
seed=seed,
derivation_path=derivation_path,
version=version or settings.version,
amounts=self.amounts,
version=version,
input_fee_ppk=settings.mint_input_fee_ppk,
)
logger.debug(f"Generated new keyset {keyset.id}.")
logger.debug(f"Generated new keyset with ID '{keyset.id}'.")
if autosave:
logger.debug(f"crud: storing new keyset {keyset.id}.")
logger.debug(f"Storing new keyset with ID '{keyset.id}'.")
await self.crud.store_keyset(keyset=keyset, db=self.db)
logger.trace(f"crud: stored new keyset {keyset.id}.")
# activate this keyset
# Activate the keyset
keyset.active = True
# load the new keyset in self.keysets
self.keysets[keyset.id] = keyset
logger.debug(f"Keyset with ID '{keyset.id}' is now active.")
logger.debug(f"Loaded keyset {keyset.id}")
return keyset
async def init_keysets(self, autosave: bool = True) -> None: