def test_that_user_with_info_dependency_works_authenticated(app, client, caplog): import logging caplog.set_level(logging.DEBUG) security = FastAPISecurity() @app.get("/users/me") def get_user_info(user: User = Depends(security.user_with_info)): return user.without_access_token() security.init_oauth2_through_oidc(dummy_oidc_url, audiences=[dummy_audience]) with aioresponses() as mock: mock.get( dummy_oidc_url, payload={ "userinfo_endpoint": dummy_userinfo_endpoint_url, "jwks_uri": dummy_jwks_uri, }, ) mock.get(dummy_jwks_uri, payload=dummy_jwks_response_data) mock.get(dummy_userinfo_endpoint_url, payload={"nickname": "jacobsvante"}) token = make_access_token(sub="GMqBbybGfBQeR6NgCY4NyXKnpFzaaTAn@clients") resp = client.get("/users/me", headers={"Authorization": f"Bearer {token}"}) assert resp.status_code == 200 data = resp.json() info = data["info"] assert info["nickname"] == "jacobsvante"
def test_that_auth_can_be_enabled_through_oidc(app, client): security = FastAPISecurity() @app.get("/") def get_products(user: User = Depends(security.authenticated_user_or_401)): return [] security.init_oauth2_through_oidc(dummy_oidc_url, audiences=[dummy_audience]) access_token = make_access_token(sub="test-subject") with aioresponses() as mock: mock.get( dummy_oidc_url, payload={ "userinfo_endpoint": dummy_userinfo_endpoint_url, "jwks_uri": dummy_jwks_uri, }, ) mock.get(dummy_jwks_uri, payload=dummy_jwks_response_data) mock.get(dummy_userinfo_endpoint_url, payload={"nickname": "jacobsvante"}) unauthenticated_resp = client.get("/") assert unauthenticated_resp.status_code == 401 authenticated_resp = client.get( "/", headers={"Authorization": f"Bearer {access_token}"}) assert authenticated_resp.status_code == 200
def test_that_user_must_have_all_permissions(app, client): security = FastAPISecurity() can_list = security.user_permission("users:list") # noqa can_view = security.user_permission("users:view") # noqa @app.get("/users") def get_user_list(user: User = Depends(security.user_holding(can_list, can_view))): return [user] security.init_oauth2_through_jwks(dummy_jwks_uri, audiences=[dummy_audience]) bad_token = make_access_token(sub="test-user", permissions=["users:list"]) valid_token = make_access_token( sub="JaneDoe", permissions=["users:list", "users:view"], ) with aioresponses() as mock: mock.get(dummy_jwks_uri, payload=dummy_jwks_response_data) resp = client.get("/users", headers={"Authorization": f"Bearer {bad_token}"}) assert resp.status_code == 403 assert resp.json() == {"detail": "Missing required permission users:view"} resp = client.get("/users", headers={"Authorization": f"Bearer {valid_token}"}) assert resp.status_code == 200 (user1,) = resp.json() assert user1["auth"]["subject"] == "JaneDoe"
def test_that_nonexisting_permissions_are_ignored(app, client): security = FastAPISecurity() @app.get("/users/me") def get_user_info(user: User = Depends(security.authenticated_user_or_401)): return user.without_access_token() security.init_oauth2_through_jwks(dummy_jwks_uri, audiences=[dummy_audience]) access_token = make_access_token( sub="test-subject", permissions=["users:list"], ) with aioresponses() as mock: mock.get(dummy_jwks_uri, payload=dummy_jwks_response_data) resp = client.get( "/users/me", headers={"Authorization": f"Bearer {access_token}"} ) assert resp.status_code == 200 data = resp.json()["auth"] del data["expires_at"] del data["issued_at"] assert data == { "audience": ["https://some-resource"], "auth_method": "oauth2", "issuer": "https://identity-provider/", "permissions": [], "scopes": [], "subject": "test-subject", }
def test_that_oidc_info_is_returned(app, client): security = FastAPISecurity() @app.get("/users/me") async def get_user_details(user: User = Depends(security.user_with_info)): """Return user details, regardless of whether user is authenticated or not""" return user.without_access_token() security.init_oauth2_through_oidc(dummy_oidc_url, audiences=[dummy_audience]) access_token = make_access_token(sub="test-subject") with aioresponses() as mock: mock.get( dummy_oidc_url, payload={ "userinfo_endpoint": dummy_userinfo_endpoint_url, "jwks_uri": dummy_jwks_uri, }, ) mock.get(dummy_jwks_uri, payload=dummy_jwks_response_data) mock.get(dummy_userinfo_endpoint_url, payload={"nickname": "jacobsvante"}) resp = client.get("/users/me", headers={"Authorization": f"Bearer {access_token}"}) assert resp.status_code == 200 data = resp.json() assert data["info"]["nickname"] == "jacobsvante"
def test_that_header_is_returned_for_basic_auth(app, client): security = FastAPISecurity() security.init_basic_auth([{"username": "******", "password": "******"}]) @app.get("/") def get_products(user: User = Depends(security.authenticated_user_or_401)): return [] resp = client.get("/") assert resp.headers["WWW-Authenticate"] == "Basic"
def test_that_basic_auth_accepts_correct_credentials(app, client): security = FastAPISecurity() @app.get("/") def get_products(user: User = Depends(security.authenticated_user_or_401)): return [] credentials = [{"username": "******", "password": "******"}] security.init_basic_auth(credentials) resp = client.get("/", auth=("user", "pass")) assert resp.status_code == 200
def test_that_header_is_returned_for_oauth2(app, client): security = FastAPISecurity() security.init_oauth2_through_jwks(dummy_jwks_uri, audiences=[dummy_audience]) @app.get("/") def get_products(user: User = Depends(security.authenticated_user_or_401)): return [] with aioresponses() as mock: mock.get(dummy_jwks_uri, payload=dummy_jwks_response_data) resp = client.get("/") assert resp.headers["WWW-Authenticate"] == "Bearer"
def test_that_user_with_info_dependency_works_unauthenticated(app, client): security = FastAPISecurity() @app.get("/users/me") def get_user_info(user: User = Depends(security.user_with_info)): return user.without_access_token() security.init_basic_auth([{"username": "******", "password": "******"}]) resp = client.get("/users/me") assert resp.status_code == 200 info = resp.json()["info"] assert info["nickname"] is None
def test_that_uninitialized_basic_auth_doesnt_accept_any_credentials( app, client): security = FastAPISecurity() @app.get("/") def get_products(user: User = Depends(security.authenticated_user_or_401)): return [] # NOTE: Not passing basic_auth_credentials, which means Basic Auth will be disabled # NOTE: We are passing security.init_oauth2_through_jwks(dummy_jwks_uri, audiences=[dummy_audience]) resp = client.get("/") assert resp.status_code == 401 resp = client.get("/", auth=("username", "password")) assert resp.status_code == 401
def test_that_oauth2_rejects_expired_token(app, client): security = FastAPISecurity() @app.get("/") def get_products(user: User = Depends(security.authenticated_user_or_401)): return [] security.init(app, jwks_url=dummy_jwks_url, audiences=[dummy_audience]) access_token = make_access_token(sub="test-subject", expire_in=-1) with aioresponses() as mock: mock.get(dummy_jwks_url, payload=dummy_jwks_response_data) resp = client.get("/", headers={"Authorization": f"Bearer {access_token}"}) assert resp.status_code == 401
def test_that_endpoints_raise_exception_when_auth_is_unconfigured(app, client): security = FastAPISecurity() @app.get("/") def get_products(user: User = Depends(security.authenticated_user_or_401)): return [] with pytest.raises(AuthNotConfigured): client.get("/")
def test_that_oauth2_rejects_incorrect_token(app, client): security = FastAPISecurity() @app.get("/") def get_products(user: User = Depends(security.authenticated_user_or_401)): return [] security.init(app, jwks_url=dummy_jwks_url, audiences=[dummy_audience]) resp = client.get("/") assert resp.status_code == 401 resp = client.get("/", headers={"Authorization": "Bearer abc"}) assert resp.status_code == 401 resp = client.get("/", headers={"Authorization": "Bearer abc.xyz.def"}) assert resp.status_code == 401
def test_that_missing_permission_results_in_403(app, client): security = FastAPISecurity() can_list = security.user_permission("users:list") # noqa @app.get("/users") def get_user_list(user: User = Depends(security.user_holding(can_list))): return [user] security.init_oauth2_through_jwks(dummy_jwks_uri, audiences=[dummy_audience]) access_token = make_access_token(sub="test-user", permissions=[]) with aioresponses() as mock: mock.get(dummy_jwks_uri, payload=dummy_jwks_response_data) resp = client.get("/users", headers={"Authorization": f"Bearer {access_token}"}) assert resp.status_code == 403 assert resp.json() == {"detail": "Missing required permission users:list"}
def test_that_assigned_permission_result_in_200(app, client): security = FastAPISecurity() can_list = security.user_permission("users:list") # noqa @app.get("/users") def get_user_list(user: User = Depends(security.user_holding(can_list))): return [user] security.init_oauth2_through_jwks(dummy_jwks_uri, audiences=[dummy_audience]) access_token = make_access_token(sub="test-user", permissions=["users:list"]) with aioresponses() as mock: mock.get(dummy_jwks_uri, payload=dummy_jwks_response_data) resp = client.get("/users", headers={"Authorization": f"Bearer {access_token}"}) assert resp.status_code == 200 (user1,) = resp.json() assert user1["auth"]["subject"] == "test-user"
def test_that_user_dependency_works_authenticated_or_not(app, client): security = FastAPISecurity() @app.get("/users/me") def get_user_info(user: User = Depends(security.user)): return user.without_access_token() security.init_basic_auth([{"username": "******", "password": "******"}]) # Anonymous resp = client.get("/users/me") assert resp.status_code == 200 data = resp.json()["auth"] del data["expires_at"] del data["issued_at"] assert data == { "audience": [], "auth_method": "none", "issuer": None, "permissions": [], "scopes": [], "subject": "anonymous", } # Authenticated resp = client.get("/users/me", auth=("JaneDoe", "abc123")) assert resp.status_code == 200 data = resp.json()["auth"] del data["expires_at"] del data["issued_at"] assert data == { "audience": [], "auth_method": "basic_auth", "issuer": None, "permissions": [], "scopes": [], "subject": "JaneDoe", }
def test_that_headers_are_returned_for_oauth2_and_basic_auth(app, client): security = FastAPISecurity() security.init_basic_auth([{"username": "******", "password": "******"}]) security.init_oauth2_through_jwks(dummy_jwks_uri, audiences=[dummy_audience]) @app.get("/") def get_products(user: User = Depends(security.authenticated_user_or_401)): return [] with aioresponses() as mock: mock.get(dummy_jwks_uri, payload=dummy_jwks_response_data) resp = client.get("/") # NOTE: They are actually set as separate headers assert resp.headers["WWW-Authenticate"] == "Basic, Bearer"
def test_that_explicit_permission_overrides_are_applied(app, client): cred = HTTPBasicCredentials(username="******", password="******") security = FastAPISecurity() create_product_perm = security.user_permission("products:create") security.init_basic_auth([cred]) security.add_permission_overrides({"johndoe": ["products:create"]}) @app.post("/products") def create_product( user: User = Depends(security.user_holding(create_product_perm)), ): return {"ok": True} resp = client.post("/products", auth=("johndoe", "123")) assert resp.status_code == 200 assert resp.json() == {"ok": True}
def test_that_permission_overrides_can_be_an_exhaustable_iterator(app, client): cred = HTTPBasicCredentials(username="******", password="******") security = FastAPISecurity() create_product_perm = security.user_permission("products:create") security.init_basic_auth([cred]) overrides = iter(["products:create"]) security.add_permission_overrides({"johndoe": overrides}) @app.post("/products") def create_product( user: User = Depends(security.user_holding(create_product_perm)), ): return {"ok": True} # NOTE: Before v0.3.1, the second iteration would give a HTTP403, as the overrides # iterator had been exhausted on the first try. for _ in range(2): resp = client.post("/products", auth=("johndoe", "123")) assert resp.status_code == 200 assert resp.json() == {"ok": True}
def test_that_authenticated_user_with_info_or_401_works_as_expected(app, client): security = FastAPISecurity() @app.get("/users/me") def get_user_info( user: User = Depends(security.authenticated_user_with_info_or_401), ): return user.without_access_token() security.init_oauth2_through_oidc(dummy_oidc_url, audiences=[dummy_audience]) security.init_basic_auth([{"username": "******", "password": "******"}]) with aioresponses() as mock: mock.get( dummy_oidc_url, payload={ "userinfo_endpoint": dummy_userinfo_endpoint_url, "jwks_uri": dummy_jwks_uri, }, ) mock.get(dummy_jwks_uri, payload=dummy_jwks_response_data) mock.get(dummy_userinfo_endpoint_url, payload={"nickname": "jacobsvante"}) token = make_access_token(sub="GMqBbybGfBQeR6NgCY4NyXKnpFzaaTAn@clients") resp = client.get("/users/me", headers={"Authorization": f"Bearer {token}"}) assert resp.status_code == 200 info = resp.json()["info"] assert info["nickname"] == "jacobsvante" # Basic auth resp = client.get("/users/me", auth=("a", "b")) assert resp.status_code == 200 info = resp.json()["info"] assert info["nickname"] is None # Unauthenticated resp = client.get("/users/me") assert resp.status_code == 401 assert resp.json() == {"detail": "Could not validate credentials"}
import logging from typing import List from fastapi import Depends, FastAPI from fastapi_security import FastAPISecurity, User from . import db from .models import Product from .settings import get_settings app = FastAPI() settings = get_settings() security = FastAPISecurity() if settings.basic_auth_credentials: security.init_basic_auth(settings.basic_auth_credentials) if settings.oidc_discovery_url: security.init_oauth2_through_oidc( settings.oidc_discovery_url, audiences=settings.oauth2_audiences, ) elif settings.oauth2_jwks_url: security.init_oauth2_through_jwks( settings.oauth2_jwks_url, audiences=settings.oauth2_audiences, )
import logging from typing import List from fastapi import Depends, FastAPI from fastapi_security import FastAPISecurity, User, UserPermission from . import db from .models import Product from .settings import get_settings app = FastAPI() settings = get_settings() security = FastAPISecurity() security.init( app, basic_auth_credentials=settings.basic_auth_credentials, jwks_url=settings.oauth2_jwks_url, audiences=settings.oauth2_audiences, oidc_discovery_url=settings.oidc_discovery_url, permission_overrides=settings.permission_overrides, ) logger = logging.getLogger(__name__) create_product_perm = UserPermission("products:create")