Files
nutshell/cashu/core/mint_info.py
callebtc a0ef44dba0 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
2025-01-29 22:48:51 -06:00

126 lines
4.4 KiB
Python

import json
import re
from typing import Any, Dict, List, Optional
from pydantic import BaseModel
from .base import Method, Unit
from .models import MintInfoContact, MintInfoProtectedEndpoint, Nut15MppSupport
from .nuts.nuts import BLIND_AUTH_NUT, CLEAR_AUTH_NUT, MPP_NUT, WEBSOCKETS_NUT
class MintInfo(BaseModel):
name: Optional[str]
pubkey: Optional[str]
version: Optional[str]
description: Optional[str]
description_long: Optional[str]
contact: Optional[List[MintInfoContact]]
motd: Optional[str]
icon_url: Optional[str]
time: Optional[int]
nuts: Dict[int, Any]
def __str__(self):
return f"{self.name} ({self.description})"
@classmethod
def from_json_str(cls, json_str: str):
return cls.parse_obj(json.loads(json_str))
def supports_nut(self, nut: int) -> bool:
if self.nuts is None:
return False
return nut in self.nuts
def supports_mpp(self, method: str, unit: Unit) -> bool:
if not self.nuts:
return False
nut_15 = self.nuts.get(MPP_NUT)
if not nut_15 or not self.supports_nut(MPP_NUT) or not nut_15.get("methods"):
return False
for entry in nut_15["methods"]:
entry_obj = Nut15MppSupport.parse_obj(entry)
if entry_obj.method == method and entry_obj.unit == unit.name:
return True
return False
def supports_websocket_mint_quote(self, method: Method, unit: Unit) -> bool:
if not self.nuts or not self.supports_nut(WEBSOCKETS_NUT):
return False
websocket_settings = self.nuts[WEBSOCKETS_NUT]
if not websocket_settings or "supported" not in websocket_settings:
return False
websocket_supported = websocket_settings["supported"]
for entry in websocket_supported:
if entry["method"] == method.name and entry["unit"] == unit.name:
if "bolt11_mint_quote" in entry["commands"]:
return True
return False
def requires_clear_auth(self) -> bool:
return self.supports_nut(CLEAR_AUTH_NUT)
def oidc_discovery_url(self) -> str:
if not self.requires_clear_auth():
raise Exception(
"Could not get OIDC discovery URL. Mint info does not support clear auth."
)
return self.nuts[CLEAR_AUTH_NUT]["openid_discovery"]
def oidc_client_id(self) -> str:
if not self.requires_clear_auth():
raise Exception(
"Could not get client_id. Mint info does not support clear auth."
)
return self.nuts[CLEAR_AUTH_NUT]["client_id"]
def required_clear_auth_endpoints(self) -> List[MintInfoProtectedEndpoint]:
if not self.requires_clear_auth():
return []
return [
MintInfoProtectedEndpoint.parse_obj(e)
for e in self.nuts[CLEAR_AUTH_NUT]["protected_endpoints"]
]
def requires_clear_auth_path(self, method: str, path: str) -> bool:
if not self.requires_clear_auth():
return False
path = "/" + path if not path.startswith("/") else path
for endpoint in self.required_clear_auth_endpoints():
if method == endpoint.method and re.match(endpoint.path, path):
return True
return False
def requires_blind_auth(self) -> bool:
return self.supports_nut(BLIND_AUTH_NUT)
@property
def bat_max_mint(self) -> int:
if not self.requires_blind_auth():
raise Exception(
"Could not get max mint. Mint info does not support blind auth."
)
if not self.nuts[BLIND_AUTH_NUT].get("bat_max_mint"):
raise Exception("Could not get max mint. bat_max_mint not set.")
return self.nuts[BLIND_AUTH_NUT]["bat_max_mint"]
def required_blind_auth_paths(self) -> List[MintInfoProtectedEndpoint]:
if not self.requires_blind_auth():
return []
return [
MintInfoProtectedEndpoint.parse_obj(e)
for e in self.nuts[BLIND_AUTH_NUT]["protected_endpoints"]
]
def requires_blind_auth_path(self, method: str, path: str) -> bool:
if not self.requires_blind_auth():
return False
path = "/" + path if not path.startswith("/") else path
for endpoint in self.required_blind_auth_paths():
if method == endpoint.method and re.match(endpoint.path, path):
return True
return False