mirror of
https://github.com/lnbits/lnbits.git
synced 2025-06-18 04:40:56 +02:00
fix: better differentiation between UNAUTHORIZED
and FORBIDDEN
(#3139)
This commit is contained in:
parent
6a9089fd98
commit
3529f9152f
@ -74,7 +74,7 @@ async def get_auth_user(user: User = Depends(check_user_exists)) -> User:
|
|||||||
async def login(data: LoginUsernamePassword) -> JSONResponse:
|
async def login(data: LoginUsernamePassword) -> JSONResponse:
|
||||||
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.UNAUTHORIZED, "Login by 'Username and Password' not allowed."
|
HTTPStatus.FORBIDDEN, "Login by 'Username and Password' not allowed."
|
||||||
)
|
)
|
||||||
account = await get_account_by_username_or_email(data.username)
|
account = await get_account_by_username_or_email(data.username)
|
||||||
if not account or not account.verify_password(data.password):
|
if not account or not account.verify_password(data.password):
|
||||||
@ -85,9 +85,7 @@ async def login(data: LoginUsernamePassword) -> JSONResponse:
|
|||||||
@auth_router.post("/nostr", description="Login via Nostr")
|
@auth_router.post("/nostr", description="Login via Nostr")
|
||||||
async def nostr_login(request: Request) -> JSONResponse:
|
async def nostr_login(request: Request) -> JSONResponse:
|
||||||
if not settings.is_auth_method_allowed(AuthMethods.nostr_auth_nip98):
|
if not settings.is_auth_method_allowed(AuthMethods.nostr_auth_nip98):
|
||||||
raise HTTPException(
|
raise HTTPException(HTTPStatus.FORBIDDEN, "Login with Nostr Auth not allowed.")
|
||||||
HTTPStatus.UNAUTHORIZED, "Login with Nostr Auth not allowed."
|
|
||||||
)
|
|
||||||
event = _nostr_nip98_event(request)
|
event = _nostr_nip98_event(request)
|
||||||
account = await get_account_by_pubkey(event["pubkey"])
|
account = await get_account_by_pubkey(event["pubkey"])
|
||||||
if not account:
|
if not account:
|
||||||
@ -104,7 +102,7 @@ async def nostr_login(request: Request) -> JSONResponse:
|
|||||||
async def login_usr(data: LoginUsr) -> JSONResponse:
|
async def login_usr(data: LoginUsr) -> JSONResponse:
|
||||||
if not settings.is_auth_method_allowed(AuthMethods.user_id_only):
|
if not settings.is_auth_method_allowed(AuthMethods.user_id_only):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.UNAUTHORIZED,
|
HTTPStatus.FORBIDDEN,
|
||||||
"Login by 'User ID' not allowed.",
|
"Login by 'User ID' not allowed.",
|
||||||
)
|
)
|
||||||
account = await get_account(data.usr)
|
account = await get_account(data.usr)
|
||||||
@ -112,7 +110,7 @@ async def login_usr(data: LoginUsr) -> JSONResponse:
|
|||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User ID does not exist.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User ID does not exist.")
|
||||||
if account.is_admin:
|
if account.is_admin:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.UNAUTHORIZED, "Admin users cannot login with user id only."
|
HTTPStatus.FORBIDDEN, "Admin users cannot login with user id only."
|
||||||
)
|
)
|
||||||
return _auth_success_response(account.username, account.id, account.email)
|
return _auth_success_response(account.username, account.id, account.email)
|
||||||
|
|
||||||
@ -242,7 +240,7 @@ async def login_with_sso_provider(
|
|||||||
provider_sso = _new_sso(provider)
|
provider_sso = _new_sso(provider)
|
||||||
if not provider_sso:
|
if not provider_sso:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.UNAUTHORIZED,
|
HTTPStatus.FORBIDDEN,
|
||||||
f"Login by '{provider}' not allowed.",
|
f"Login by '{provider}' not allowed.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -257,7 +255,7 @@ async def handle_oauth_token(request: Request, provider: str) -> RedirectRespons
|
|||||||
provider_sso = _new_sso(provider)
|
provider_sso = _new_sso(provider)
|
||||||
if not provider_sso:
|
if not provider_sso:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.UNAUTHORIZED,
|
HTTPStatus.FORBIDDEN,
|
||||||
f"Login by '{provider}' not allowed.",
|
f"Login by '{provider}' not allowed.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -285,7 +283,7 @@ async def logout() -> JSONResponse:
|
|||||||
async def register(data: RegisterUser) -> JSONResponse:
|
async def register(data: RegisterUser) -> JSONResponse:
|
||||||
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.UNAUTHORIZED,
|
HTTPStatus.FORBIDDEN,
|
||||||
"Register by 'Username and Password' not allowed.",
|
"Register by 'Username and Password' not allowed.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -375,7 +373,7 @@ async def update_password(
|
|||||||
async def reset_password(data: ResetUserPassword) -> JSONResponse:
|
async def reset_password(data: ResetUserPassword) -> JSONResponse:
|
||||||
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.UNAUTHORIZED, "Auth by 'Username and Password' not allowed."
|
HTTPStatus.FORBIDDEN, "Auth by 'Username and Password' not allowed."
|
||||||
)
|
)
|
||||||
|
|
||||||
assert data.password == data.password_repeat, "Passwords do not match."
|
assert data.password == data.password_repeat, "Passwords do not match."
|
||||||
@ -449,7 +447,7 @@ async def update(
|
|||||||
@auth_router.put("/first_install")
|
@auth_router.put("/first_install")
|
||||||
async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
|
async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
|
||||||
if not settings.first_install:
|
if not settings.first_install:
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "This is not your first install")
|
raise HTTPException(HTTPStatus.FORBIDDEN, "This is not your first install")
|
||||||
account = await get_account(settings.super_user)
|
account = await get_account(settings.super_user)
|
||||||
if not account:
|
if not account:
|
||||||
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Superuser not found.")
|
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Superuser not found.")
|
||||||
@ -472,10 +470,10 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: Optional[str] =
|
|||||||
|
|
||||||
if verified_user_id:
|
if verified_user_id:
|
||||||
if account:
|
if account:
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Email already used.")
|
raise HTTPException(HTTPStatus.FORBIDDEN, "Email already used.")
|
||||||
account = await get_account(verified_user_id)
|
account = await get_account(verified_user_id)
|
||||||
if not account:
|
if not account:
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Cannot verify user email.")
|
raise HTTPException(HTTPStatus.FORBIDDEN, "Cannot verify user email.")
|
||||||
redirect_path = "/account"
|
redirect_path = "/account"
|
||||||
|
|
||||||
if account:
|
if account:
|
||||||
@ -580,10 +578,10 @@ def _find_auth_provider_class(provider: str) -> Callable:
|
|||||||
def _nostr_nip98_event(request: Request) -> dict:
|
def _nostr_nip98_event(request: Request) -> dict:
|
||||||
auth_header = request.headers.get("Authorization")
|
auth_header = request.headers.get("Authorization")
|
||||||
if not auth_header:
|
if not auth_header:
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Nostr Auth header missing.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Nostr Auth header missing.")
|
||||||
scheme, token = auth_header.split()
|
scheme, token = auth_header.split()
|
||||||
if scheme.lower() != "nostr":
|
if scheme.lower() != "nostr":
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid Authorization scheme.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid Authorization scheme.")
|
||||||
event = None
|
event = None
|
||||||
try:
|
try:
|
||||||
event_json = base64.b64decode(token.encode("ascii"))
|
event_json = base64.b64decode(token.encode("ascii"))
|
||||||
|
@ -155,7 +155,7 @@ async def api_payments_daily_stats(
|
|||||||
|
|
||||||
if not user.admin:
|
if not user.admin:
|
||||||
exc = HTTPException(
|
exc = HTTPException(
|
||||||
status_code=HTTPStatus.UNAUTHORIZED,
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
detail="Missing wallet id.",
|
detail="Missing wallet id.",
|
||||||
)
|
)
|
||||||
wallet_filter = next(
|
wallet_filter = next(
|
||||||
@ -322,7 +322,7 @@ async def api_payments_create(
|
|||||||
return await _api_payments_create_invoice(invoice_data, wallet.wallet)
|
return await _api_payments_create_invoice(invoice_data, wallet.wallet)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.UNAUTHORIZED,
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
detail="Invoice (or Admin) key required.",
|
detail="Invoice (or Admin) key required.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -102,7 +102,7 @@ class KeyChecker(SecurityBase):
|
|||||||
request.scope["user_id"] = wallet.user
|
request.scope["user_id"] = wallet.user
|
||||||
if self.expected_key_type is KeyType.admin and wallet.adminkey != key_value:
|
if self.expected_key_type is KeyType.admin and wallet.adminkey != key_value:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.UNAUTHORIZED,
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
detail="Invalid adminkey.",
|
detail="Invalid adminkey.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -165,7 +165,7 @@ async def check_user_exists(
|
|||||||
|
|
||||||
r.scope["user_id"] = account.id
|
r.scope["user_id"] = account.id
|
||||||
if not settings.is_user_allowed(account.id):
|
if not settings.is_user_allowed(account.id):
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User not allowed.")
|
raise HTTPException(HTTPStatus.FORBIDDEN, "User not allowed.")
|
||||||
|
|
||||||
user = await get_user_from_account(account)
|
user = await get_user_from_account(account)
|
||||||
if not user:
|
if not user:
|
||||||
@ -201,7 +201,7 @@ async def access_token_payload(
|
|||||||
async def check_admin(user: Annotated[User, Depends(check_user_exists)]) -> User:
|
async def check_admin(user: Annotated[User, Depends(check_user_exists)]) -> User:
|
||||||
if user.id != settings.super_user and user.id not in settings.lnbits_admin_users:
|
if user.id != settings.super_user and user.id not in settings.lnbits_admin_users:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.UNAUTHORIZED, "User not authorized. No admin privileges."
|
HTTPStatus.FORBIDDEN, "User not authorized. No admin privileges."
|
||||||
)
|
)
|
||||||
if not user.has_password:
|
if not user.has_password:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@ -214,7 +214,7 @@ async def check_admin(user: Annotated[User, Depends(check_user_exists)]) -> User
|
|||||||
async def check_super_user(user: Annotated[User, Depends(check_user_exists)]) -> User:
|
async def check_super_user(user: Annotated[User, Depends(check_user_exists)]) -> User:
|
||||||
if user.id != settings.super_user:
|
if user.id != settings.super_user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.UNAUTHORIZED, "User not authorized. No super user privileges."
|
HTTPStatus.FORBIDDEN, "User not authorized. No super user privileges."
|
||||||
)
|
)
|
||||||
if not user.has_password:
|
if not user.has_password:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
@ -65,19 +65,15 @@
|
|||||||
goToExtension() {
|
goToExtension() {
|
||||||
window.location.href = `/extensions#${this.extension}`
|
window.location.href = `/extensions#${this.extension}`
|
||||||
},
|
},
|
||||||
logOut() {
|
async logOut() {
|
||||||
LNbits.utils
|
|
||||||
.confirmDialog(
|
|
||||||
'Do you really want to logout?'
|
|
||||||
)
|
|
||||||
.onOk( async () => {
|
|
||||||
try {
|
try {
|
||||||
await LNbits.api.logout()
|
await LNbits.api.logout()
|
||||||
window.location = '/'
|
window.location = '/'
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
LNbits.utils.notifyApiError(e)
|
LNbits.utils.notifyApiError(e)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -86,13 +82,13 @@
|
|||||||
if (this.message.startsWith('Extension ')) return true
|
if (this.message.startsWith('Extension ')) return true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
async created() {
|
||||||
this.err = '{{ err }}'
|
this.err = '{{ err }}'
|
||||||
const statusCode = '{{ status_code }}' || 404
|
const statusCode = '{{ status_code }}' || 404
|
||||||
this.message = String({{ message | tojson }}) || 'Page not found'
|
this.message = String({{ message | tojson }}) || 'Page not found'
|
||||||
if (statusCode == 401) {
|
if (statusCode == 401) {
|
||||||
console.warn(`Unauthorized: ${this.message}`)
|
console.warn(`Unauthorized: ${this.message}`)
|
||||||
this.goHome()
|
this.logOut()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.statusCode = statusCode
|
this.statusCode = statusCode
|
||||||
|
@ -7,7 +7,7 @@ from lnbits.settings import Settings
|
|||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_admin_get_settings_permission_denied(client, from_user):
|
async def test_admin_get_settings_permission_denied(client, from_user):
|
||||||
response = await client.get(f"/admin/api/v1/settings?usr={from_user.id}")
|
response = await client.get(f"/admin/api/v1/settings?usr={from_user.id}")
|
||||||
assert response.status_code == 401
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
|
@ -102,7 +102,7 @@ async def test_login_usr_not_allowed_for_admin_without_credentials(
|
|||||||
# Attempt to login with user ID for admin
|
# Attempt to login with user ID for admin
|
||||||
response = await http_client.post("/api/v1/auth/usr", json=login_data.dict())
|
response = await http_client.post("/api/v1/auth/usr", json=login_data.dict())
|
||||||
|
|
||||||
assert response.status_code == 401
|
assert response.status_code == 403
|
||||||
assert (
|
assert (
|
||||||
response.json().get("detail") == "Admin users cannot login with user id only."
|
response.json().get("detail") == "Admin users cannot login with user id only."
|
||||||
)
|
)
|
||||||
@ -135,7 +135,7 @@ async def test_login_usr_not_allowed(
|
|||||||
|
|
||||||
response = await http_client.post("/api/v1/auth/usr", json={"usr": user_alan.id})
|
response = await http_client.post("/api/v1/auth/usr", json={"usr": user_alan.id})
|
||||||
|
|
||||||
assert response.status_code == 401, "Login method not allowed."
|
assert response.status_code == 403, "Login method not allowed."
|
||||||
assert response.json().get("detail") == "Login by 'User ID' not allowed."
|
assert response.json().get("detail") == "Login by 'User ID' not allowed."
|
||||||
|
|
||||||
settings.auth_allowed_methods = AuthMethods.all()
|
settings.auth_allowed_methods = AuthMethods.all()
|
||||||
@ -217,7 +217,7 @@ async def test_login_username_password_not_allowed(
|
|||||||
"/api/v1/auth", json={"username": user_alan.username, "password": "secret1234"}
|
"/api/v1/auth", json={"username": user_alan.username, "password": "secret1234"}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 401, "Login method not allowed."
|
assert response.status_code == 403, "Login method not allowed."
|
||||||
assert (
|
assert (
|
||||||
response.json().get("detail") == "Login by 'Username and Password' not allowed."
|
response.json().get("detail") == "Login by 'Username and Password' not allowed."
|
||||||
)
|
)
|
||||||
@ -597,7 +597,7 @@ async def test_register_nostr_not_allowed(http_client: AsyncClient, settings: Se
|
|||||||
json={},
|
json={},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 401, "User not authenticated."
|
assert response.status_code == 403, "User not authenticated."
|
||||||
assert response.json().get("detail") == "Login with Nostr Auth not allowed."
|
assert response.json().get("detail") == "Login with Nostr Auth not allowed."
|
||||||
|
|
||||||
settings.auth_allowed_methods = AuthMethods.all()
|
settings.auth_allowed_methods = AuthMethods.all()
|
||||||
@ -607,7 +607,7 @@ async def test_register_nostr_not_allowed(http_client: AsyncClient, settings: Se
|
|||||||
async def test_register_nostr_bad_header(http_client: AsyncClient):
|
async def test_register_nostr_bad_header(http_client: AsyncClient):
|
||||||
response = await http_client.post("/api/v1/auth/nostr")
|
response = await http_client.post("/api/v1/auth/nostr")
|
||||||
|
|
||||||
assert response.status_code == 401, "Missing header."
|
assert response.status_code == 400, "Missing header."
|
||||||
assert response.json().get("detail") == "Nostr Auth header missing."
|
assert response.json().get("detail") == "Nostr Auth header missing."
|
||||||
|
|
||||||
response = await http_client.post(
|
response = await http_client.post(
|
||||||
@ -615,7 +615,7 @@ async def test_register_nostr_bad_header(http_client: AsyncClient):
|
|||||||
headers={"Authorization": "Bearer xyz"},
|
headers={"Authorization": "Bearer xyz"},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 401, "Non nostr header."
|
assert response.status_code == 400, "Non nostr header."
|
||||||
assert response.json().get("detail") == "Invalid Authorization scheme."
|
assert response.json().get("detail") == "Invalid Authorization scheme."
|
||||||
|
|
||||||
response = await http_client.post(
|
response = await http_client.post(
|
||||||
@ -1028,7 +1028,7 @@ async def test_reset_username_password_not_allowed(
|
|||||||
)
|
)
|
||||||
settings.auth_allowed_methods = AuthMethods.all()
|
settings.auth_allowed_methods = AuthMethods.all()
|
||||||
|
|
||||||
assert response.status_code == 401, "Login method not allowed."
|
assert response.status_code == 403, "Login method not allowed."
|
||||||
assert (
|
assert (
|
||||||
response.json().get("detail") == "Auth by 'Username and Password' not allowed."
|
response.json().get("detail") == "Auth by 'Username and Password' not allowed."
|
||||||
)
|
)
|
||||||
|
@ -76,7 +76,7 @@ async def test_check_user_exists_with_user_not_allowed(user_alan: User):
|
|||||||
settings.lnbits_allowed_users = ["only_this_user_id"]
|
settings.lnbits_allowed_users = ["only_this_user_id"]
|
||||||
with pytest.raises(HTTPException) as exc_info:
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
await check_user_exists(request, access_token=None, usr=UUID4(user_alan.id))
|
await check_user_exists(request, access_token=None, usr=UUID4(user_alan.id))
|
||||||
assert exc_info.value.status_code == 401
|
assert exc_info.value.status_code == 403
|
||||||
assert exc_info.value.detail == "User not allowed."
|
assert exc_info.value.detail == "User not allowed."
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user