예제 #1
0
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"
예제 #2
0
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
예제 #3
0
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"
예제 #4
0
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",
        }
예제 #5
0
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"
예제 #9
0
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
예제 #10
0
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
예제 #11
0
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("/")
예제 #13
0
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
예제 #14
0
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"}
예제 #15
0
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"
예제 #16
0
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}
예제 #20
0
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"}
예제 #21
0
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,
    )
예제 #22
0
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")