mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 10:34:20 +01:00
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:
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import asyncio
|
||||
import getpass
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
@@ -41,6 +42,7 @@ from ...wallet.crud import (
|
||||
)
|
||||
from ...wallet.wallet import Wallet as Wallet
|
||||
from ..api.api_server import start_api_server
|
||||
from ..auth.auth import WalletAuth
|
||||
from ..cli.cli_helpers import (
|
||||
get_mint_wallet,
|
||||
get_unit_wallet,
|
||||
@@ -84,6 +86,49 @@ def coro(f):
|
||||
return wrapper
|
||||
|
||||
|
||||
def init_auth_wallet(func):
|
||||
"""Decorator to pass auth_db and auth_keyset_id to the Wallet object."""
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
ctx = args[0] # Assuming the first argument is 'ctx'
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
db_location = wallet.db.db_location
|
||||
|
||||
auth_wallet = await WalletAuth.with_db(
|
||||
url=ctx.obj["HOST"],
|
||||
db=db_location,
|
||||
)
|
||||
|
||||
requires_auth = await auth_wallet.init_auth_wallet(wallet.mint_info)
|
||||
|
||||
if not requires_auth:
|
||||
logger.debug("Mint does not require clear auth.")
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
# Pass auth_db and auth_keyset_id to the wallet object
|
||||
wallet.auth_db = auth_wallet.db
|
||||
wallet.auth_keyset_id = auth_wallet.keyset_id
|
||||
# pass the mint_info so the wallet doesn't need to re-fetch it
|
||||
wallet.mint_info = auth_wallet.mint_info
|
||||
|
||||
# Pass the auth_wallet to context
|
||||
args[0].obj["AUTH_WALLET"] = auth_wallet
|
||||
|
||||
# Proceed to the original function
|
||||
ret = await func(*args, **kwargs)
|
||||
|
||||
if settings.debug:
|
||||
await auth_wallet.load_proofs(reload=True)
|
||||
logger.debug(
|
||||
f"Auth balance: {auth_wallet.unit.str(auth_wallet.available_balance)}"
|
||||
)
|
||||
|
||||
return ret
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@click.group(cls=NaturalOrderGroup)
|
||||
@click.option(
|
||||
"--host",
|
||||
@@ -176,6 +221,10 @@ async def cli(ctx: Context, host: str, walletname: str, unit: str, tests: bool):
|
||||
ctx.obj["HOST"], db_path, name=walletname, unit=unit
|
||||
)
|
||||
|
||||
# if we have never seen this mint before, we load its information
|
||||
if not wallet.mint_info:
|
||||
await wallet.load_mint()
|
||||
|
||||
assert wallet, "Wallet not found."
|
||||
ctx.obj["WALLET"] = wallet
|
||||
|
||||
@@ -205,6 +254,7 @@ async def cli(ctx: Context, host: str, walletname: str, unit: str, tests: bool):
|
||||
)
|
||||
@click.pass_context
|
||||
@coro
|
||||
@init_auth_wallet
|
||||
async def pay(
|
||||
ctx: Context, invoice: str, amount: Optional[int] = None, yes: bool = False
|
||||
):
|
||||
@@ -291,6 +341,7 @@ async def pay(
|
||||
)
|
||||
@click.pass_context
|
||||
@coro
|
||||
@init_auth_wallet
|
||||
async def invoice(
|
||||
ctx: Context,
|
||||
amount: float,
|
||||
@@ -451,6 +502,7 @@ async def invoice(
|
||||
@cli.command("swap", help="Swap funds between mints.")
|
||||
@click.pass_context
|
||||
@coro
|
||||
@init_auth_wallet
|
||||
async def swap(ctx: Context):
|
||||
print("Select the mint to swap from:")
|
||||
outgoing_wallet: Wallet = await get_mint_wallet(ctx, force_select=True)
|
||||
@@ -621,8 +673,9 @@ async def balance(ctx: Context, verbose):
|
||||
)
|
||||
@click.pass_context
|
||||
@coro
|
||||
@init_auth_wallet
|
||||
async def send_command(
|
||||
ctx,
|
||||
ctx: Context,
|
||||
amount: int,
|
||||
memo: str,
|
||||
nostr: str,
|
||||
@@ -668,6 +721,7 @@ async def send_command(
|
||||
)
|
||||
@click.pass_context
|
||||
@coro
|
||||
@init_auth_wallet
|
||||
async def receive_cli(
|
||||
ctx: Context,
|
||||
token: str,
|
||||
@@ -685,6 +739,8 @@ async def receive_cli(
|
||||
mint_url,
|
||||
os.path.join(settings.cashu_dir, wallet.name),
|
||||
unit=token_obj.unit,
|
||||
auth_db=wallet.auth_db.db_location if wallet.auth_db else None,
|
||||
auth_keyset_id=wallet.auth_keyset_id,
|
||||
)
|
||||
await verify_mint(mint_wallet, mint_url)
|
||||
receive_wallet = await receive(mint_wallet, token_obj)
|
||||
@@ -830,7 +886,7 @@ async def pending(ctx: Context, legacy, number: int, offset: int):
|
||||
@cli.command("lock", help="Generate receiving lock.")
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def lock(ctx):
|
||||
async def lock(ctx: Context):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
|
||||
pubkey = await wallet.create_p2pk_pubkey()
|
||||
@@ -851,7 +907,7 @@ async def lock(ctx):
|
||||
@cli.command("locks", help="Show unused receiving locks.")
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def locks(ctx):
|
||||
async def locks(ctx: Context):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
# P2PK lock
|
||||
pubkey = await wallet.create_p2pk_pubkey()
|
||||
@@ -899,7 +955,7 @@ async def locks(ctx):
|
||||
)
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def invoices(ctx, paid: bool, unpaid: bool, pending: bool, mint: bool):
|
||||
async def invoices(ctx: Context, paid: bool, unpaid: bool, pending: bool, mint: bool):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
|
||||
if paid and unpaid:
|
||||
@@ -1001,7 +1057,7 @@ async def invoices(ctx, paid: bool, unpaid: bool, pending: bool, mint: bool):
|
||||
@cli.command("wallets", help="List of all available wallets.")
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def wallets(ctx):
|
||||
async def wallets(ctx: Context):
|
||||
# list all directories
|
||||
wallets = [
|
||||
d for d in listdir(settings.cashu_dir) if isdir(join(settings.cashu_dir, d))
|
||||
@@ -1013,7 +1069,7 @@ async def wallets(ctx):
|
||||
for w in wallets:
|
||||
wallet = Wallet(ctx.obj["HOST"], os.path.join(settings.cashu_dir, w))
|
||||
try:
|
||||
await wallet.load_proofs()
|
||||
await wallet.load_proofs(reload=True, all_keysets=True)
|
||||
if wallet.proofs and len(wallet.proofs):
|
||||
active_wallet = False
|
||||
if w == ctx.obj["WALLET_NAME"]:
|
||||
@@ -1031,9 +1087,10 @@ async def wallets(ctx):
|
||||
@cli.command("info", help="Information about Cashu wallet.")
|
||||
@click.option("--mint", default=False, is_flag=True, help="Fetch mint information.")
|
||||
@click.option("--mnemonic", default=False, is_flag=True, help="Show your mnemonic.")
|
||||
@click.option("--reload", default=False, is_flag=True, help="Reload mint info.")
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def info(ctx: Context, mint: bool, mnemonic: bool):
|
||||
async def info(ctx: Context, mint: bool, mnemonic: bool, reload: bool):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
await wallet.load_keysets_from_db(unit=None)
|
||||
|
||||
@@ -1057,7 +1114,11 @@ async def info(ctx: Context, mint: bool, mnemonic: bool):
|
||||
if mint:
|
||||
wallet.url = mint_url
|
||||
try:
|
||||
mint_info: dict = (await wallet.load_mint_info()).dict()
|
||||
mint_info_obj = await wallet.load_mint_info(reload)
|
||||
if not mint_info_obj:
|
||||
print(" - Mint information not available.")
|
||||
continue
|
||||
mint_info = mint_info_obj.dict()
|
||||
if mint_info:
|
||||
print(f" - Mint name: {mint_info['name']}")
|
||||
if mint_info.get("description"):
|
||||
@@ -1126,6 +1187,7 @@ async def info(ctx: Context, mint: bool, mnemonic: bool):
|
||||
)
|
||||
@click.pass_context
|
||||
@coro
|
||||
@init_auth_wallet
|
||||
async def restore(ctx: Context, to: int, batch: int):
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
# check if there is already a mnemonic in the database
|
||||
@@ -1160,6 +1222,7 @@ async def restore(ctx: Context, to: int, batch: int):
|
||||
# @click.option("--all", default=False, is_flag=True, help="Execute on all available mints.")
|
||||
@click.pass_context
|
||||
@coro
|
||||
@init_auth_wallet
|
||||
async def selfpay(ctx: Context, all: bool = False):
|
||||
wallet = await get_mint_wallet(ctx, force_select=True)
|
||||
await wallet.load_mint()
|
||||
@@ -1183,3 +1246,46 @@ async def selfpay(ctx: Context, all: bool = False):
|
||||
print(token)
|
||||
token_obj = TokenV4.deserialize(token)
|
||||
await receive(wallet, token_obj)
|
||||
|
||||
|
||||
@cli.command("auth", help="Authenticate with mint.")
|
||||
@click.option("--mint", "-m", default=False, is_flag=True, help="Mint new auth tokens.")
|
||||
@click.option(
|
||||
"--force", "-f", default=False, is_flag=True, help="Force authentication."
|
||||
)
|
||||
@click.option(
|
||||
"--password",
|
||||
"-p",
|
||||
default=False,
|
||||
is_flag=True,
|
||||
help="Use username and password for authentication.",
|
||||
)
|
||||
@click.pass_context
|
||||
@coro
|
||||
async def auth(ctx: Context, mint: bool, force: bool, password: bool):
|
||||
# auth_wallet: WalletAuth = ctx.obj["AUTH_WALLET"]
|
||||
wallet: Wallet = ctx.obj["WALLET"]
|
||||
username = None
|
||||
password_str = None
|
||||
if password:
|
||||
username = input("Enter username: ")
|
||||
password_str = getpass.getpass("Enter password: ")
|
||||
auth_wallet = await WalletAuth.with_db(
|
||||
url=ctx.obj["HOST"],
|
||||
db=wallet.db.db_location,
|
||||
username=username,
|
||||
password=password_str,
|
||||
)
|
||||
|
||||
requires_auth = await auth_wallet.init_auth_wallet(
|
||||
wallet.mint_info, mint_auth_proofs=False, force_auth=force
|
||||
)
|
||||
if not requires_auth:
|
||||
print("Mint does not require authentication.")
|
||||
return
|
||||
|
||||
if mint:
|
||||
new_proofs = await auth_wallet.mint_blind_auth()
|
||||
print(f"Minted {auth_wallet.unit.str(sum_proofs(new_proofs))} auth tokens.")
|
||||
|
||||
print(f"Auth balance: {auth_wallet.unit.str(auth_wallet.available_balance)}")
|
||||
|
||||
Reference in New Issue
Block a user