From 7391056daf49d055bf3a8c812cbb765fe05e4a28 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 9 Apr 2019 23:29:04 +0400 Subject: [PATCH] :sparkles: Add OAuth2 scopes with SecurityScopes, upgrade Security (#141) * :sparkles: Upgrade OAuth2 Security with scopes handling * :memo: Update Security tutorial with OAuth2 and JWT * :sparkles: Add tutorial code for OAuth2 with scopes (and JWT) * :white_check_mark: Add tests for tutorial/OAuth2 with scopes * :bug: Fix security_scopes type declaration * :sparkles: Add docs and tests for SecurityScopes --- docs/img/tutorial/security/image11.png | Bin 0 -> 80863 bytes docs/src/security/tutorial004.py | 29 +- docs/src/security/tutorial005.py | 162 +++++++++ docs/tutorial/security/oauth2-jwt.md | 55 ++- docs/tutorial/security/oauth2-scopes.md | 191 +++++++++++ fastapi/dependencies/models.py | 4 + fastapi/dependencies/utils.py | 38 ++- fastapi/security/__init__.py | 7 +- fastapi/security/oauth2.py | 7 +- mkdocs.yml | 1 + .../test_security/test_tutorial005.py | 313 ++++++++++++++++++ 11 files changed, 773 insertions(+), 34 deletions(-) create mode 100644 docs/img/tutorial/security/image11.png create mode 100644 docs/src/security/tutorial005.py create mode 100644 docs/tutorial/security/oauth2-scopes.md create mode 100644 tests/test_tutorial/test_security/test_tutorial005.py diff --git a/docs/img/tutorial/security/image11.png b/docs/img/tutorial/security/image11.png new file mode 100644 index 0000000000000000000000000000000000000000..3e1a0ce9195bce6a9471e536dc71d5ce6f2581de GIT binary patch literal 80863 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://127.0.0.1:8000/docs. @@ -158,7 +179,7 @@ Password: `secret` -Call the endpoint `/users/me`, you will get the response as: +Call the endpoint `/users/me/`, you will get the response as: ```JSON { @@ -180,15 +201,17 @@ If you open the developer tools, you could see how the data sent and received is ## Advanced usage with `scopes` -We didn't use it in this example, but `Security` can receive a parameter `scopes`, as a list of strings. +OAuth2 has the notion of "scopes". + +You can use them to add a specific set of permissions to a JWT token. -It would describe the scopes required for a specific path operation, as different path operations might require different security scopes, even while using the same `OAuth2PasswordBearer` (or any of the other tools). +Then you can give this token to a user directly or a third party, to interact with your API with a set of restrictions. -This only applies to OAuth2, and it might be, more or less, an advanced feature, but it is there, if you need to use it. +You can learn how to use them and how they are integrated into **FastAPI** in the next section. ## Recap -This concludes our tour for the security features of **FastAPI**. +With what you have seen up to now, you can set up a secure **FastAPI** application using standards like OAuth2 and JWT. In almost any framework handling the security becomes a rather complex subject quite quickly. @@ -205,3 +228,7 @@ And you can use directly many well maintained and widely used packages like `pas But it provides you the tools to simplify the process as much as possible without compromising flexibility, robustness or security. And you can use secure, standard protocols like OAuth2 in a relatively simple way. + +In the next (optional) section you can see how to extend this even further, using OAuth2 "scopes", for a more fine-grained permission system following standards. + +OAuth2 with scopes (explained in the next section) is the mechanism used by many big authentication providers, like Facebook, Google, GitHub, Microsoft, Twitter, etc. diff --git a/docs/tutorial/security/oauth2-scopes.md b/docs/tutorial/security/oauth2-scopes.md new file mode 100644 index 0000000000..51e28daafd --- /dev/null +++ b/docs/tutorial/security/oauth2-scopes.md @@ -0,0 +1,191 @@ +You can use OAuth2 scopes directly with **FastAPI**, they are integrated to work seamlessly. + +This would allow you to have a more fine-grained permission system, following standards like OAuth2, integrated into your OpenAPI application (and the API docs). + +OAuth2 with scopes is the mechanism used by many big authentication providers, like Facebook, Google, GitHub, Microsoft, Twitter, etc. They use it to provide specific permissions to users and applications. + +Every time you "log in with" Facebook, Google, GitHub, Microsoft, Twitter, that application is using OAuth2 with scopes. + +In this section you will see how to manage authentication and authorization with the same OAuth2 with scopes in your **FastAPI** application. + +!!! warning + This is a more or less advanced section. If you are just starting, you can skip it. + + You don't necessarily need OAuth2 scopes, you can handle authentication and authorization however you want. + + But OAuth2 with scopes can be nicely integrated into your API (with OpenAPI) and your API docs. + + Nevertheless, you still enforce those scopes or any other security/authorization requirement however you need in your code. + + In many cases, OAuth2 with scopes can be an overkill. + + But if you know you need it, or you are curious, keep reading. + +## OAuth2 scopes and OpenAPI + +The OAuth2 specification defines "scopes" as a list of strings separated by spaces. + +The content of each of these strings can have any format, but should not contain spaces. + +These scopes represent "permissions". + +In OpenAPI (e.g. the API docs), you can define "security schemes", the same as you saw in the previous sections. + +When one of these security schemes uses OAuth2, you can also declare and use scopes. + +## Global view + +First, let's quickly see the parts that change from the previous section about OAuth2 and JWT. Now using OAuth2 scopes: + +```Python hl_lines="2 5 9 13 48 66 106 115 116 117 122 123 124 125 126 131 145 158" +{!./src/security/tutorial005.py!} +``` + +Now let's review those changes step by step. + +## OAuth2 Security scheme + +The first change is that now we are declaring the OAuth2 security scheme with two available scopes, `me` and `items`. + +The `scopes` parameter receives a `dict` with each scope as a key and the description as the value: + +```Python hl_lines="64 65 66 67" +{!./src/security/tutorial005.py!} +``` + +Because we are now declaring those scopes,they will show up in the API docs when you log-in/authorize. + +And you will be able to select which scopes you want to give access to: `me` and `items`. + +This is the same mechanism used when you give permissions while logging in with Facebook, Google, GitHub, etc: + + + +## JWT token with scopes + +Now, modify the token *path operation* to return the scopes requested. + +We are still using the same `OAuth2PasswordRequestForm`. It includes a property `scopes` with each scope it received. + +And we return the scopes as part of the JWT token. + +!!! danger + For simplicity, here we are just adding the scopes received directly to the token. + + But in your application, for security, you should make sure you only add the scopes that the user is actually able to have, or the ones you have predefined. + +```Python hl_lines="145" +{!./src/security/tutorial005.py!} +``` + +## Declare scopes in *path operations* and dependencies + +Now we declare that the *path operation* for `/users/me/items/` requires the scope `items`. + +For this, we import and use `Security` from `fastapi`. + +You can use `Security` to declare dependencies (just like `Depends`), but `Security` also receives a parameter `scopes` with a list of scopes (strings). + +In this case, we pass a dependency function `get_current_active_user` to `Security` (the same way we would do with `Depends`). + +But we also pass a `list` of scopes, in this case with just one scope: `items` (it could have more). + +And the dependency function `get_current_active_user` can also declare sub-dependencies, not only with `Depends` but also with `Security`. Declaring its own sub-dependency function (`get_current_user`), and more scope requirements. + +In this case, it requires the scope `me` (it could require more than one scope). + +!!! note + You don't necessarily need to add different scopes in different places. + + We are doing it here to demonstrate how **FastAPI** handles scopes declared at different levels. + +```Python hl_lines="5 131 158" +{!./src/security/tutorial005.py!} +``` + +## Use `SecurityScopes` + +Now update the dependency `get_current_user`. + +This is the one used by the dependencies above. + +Here's were we are declaring the same OAuth2 scheme we created above as a dependency: `oauth2_scheme`. + +Because this dependency function doesn't have any scope requirements itself, we can use `Depends` with `oauth2_scheme`, we don't have to use `Security`. + +We also declare a special parameter of type `SecurityScopes`, imported from `fastapi.security`. + +This `SecurityScopes` class is similar to `Request` (`Request` was used to get the request object directly). + +The parameter `security_scopes` will be of type `SecurityScopes`. It will have a property `scopes` with a list containing all the scopes required by itself and all the dependencies that use this as a sub-dependency. That means, all the "dependants" or all the super-dependencies (the contrary of sub-dependencies). + +We verify that all the scopes required, by this dependency and all the dependants (including *path operations*), are included in the scopes provided in the token received, otherwise raise an `HTTPException`. + +We also check that the token data is validated with the Pydantic model (catching the `ValidationError` exception), and if we get an error reading the JWT token or validating the data with Pydantic, we also raise an `HTTPException`. + +By validating the data with Pydantic we can make sure that we have, for example, exactly a `list` of `str` with the scopes and a `str` with the `username`. Instead of, for example, a `dict`, or something else, as it could break the application at some point later. + + +```Python hl_lines="9 13 106 48 106 115 116 117 122 123" +{!./src/security/tutorial005.py!} +``` + +So, as the other dependency `get_current_active_user` has as a sub-dependency this `get_current_user`, the scope `"me"` declared at `get_current_active_user` will be included in the `security_scopes.scopes` `list` inside of `get_current_user`. + +And as the *path operation* itself also declares a scope `"items"`, it will also be part of this `list` `security_scopes.scopes` in `get_current_user`. + +Here's how the hierarchy of dependencies and scopes looks like: + +* The *path operation* `read_own_items` has: + * Required scopes `["items"]` with the dependency: + * `get_current_active_user`: + * The dependency function `get_current_active_user` has: + * Required scopes `["me"]` with the dependency: + * `get_current_user`: + * The dependency function `get_current_user` has: + * No scopes required by itself. + * A dependency using `oauth2_scheme`. + * A `security_scopes` parameter of type `SecurityScopes`: + * This `security_scopes` parameter has a property `scopes` with a `list` containing all these scopes declared above, so: + * `security_scopes.scopes` will contain `["me", "items"]` + +## More details about `SecurityScopes` + +You can use `SecurityScopes` at any point, and in multiple places, it doesn't have to be at the "root" dependency. + +It will always have the security scopes declared in the current `Security` dependencies and all the super-dependencies/dependants. + +Because the `SecurityScopes` will have all the scopes declared by super-dependencies/dependants, you can use it to verify that a token has the required scopes in a central dependency function, and then declare different scope requirements in different *path operations*. + +## Check it + +If you open the API docs, you can authenticate and specify which scopes you want to authorize. + + + +If you don't select any scope, you will be "authenticated", but when you try to access `/users/me/` or `/users/me/items/` you will get an error saying that you don't have enough permissions. + +And if you select the scope `me` but not the scope `items`, you will be able to access `/users/me/` but not `/users/me/items/`. + +That's what would happen to a third party application that tried to access one of these *path operations* with a token provided by a user, depending on how many permissions the user gave the application. + +## About third party integrations + +In this example we are using the OAuth2 "password" flow. + +This is appropriate when we are logging in to our own application, probably with our own frontend. + +Because we can trust it to receive the `username` and `password`, as we control it. + +But if you are building an OAuth2 application that others would connect to (i.e., if you are building an authentication provider equivalent to Facebook, Google, GitHub, etc.) you should use one of the other flows. + +The most common is the implicit flow. + +The most secure is the code flow, but is more complex to implement as it requires more steps. As it is more cumbersome, many providers end up suggesting the implicit flow. + +!!! note + It's common that each authentication provider names their flows in a different way, to make it part of their brand. + + But in the end, they are implementing the same OAuth2 standard. + +**FastAPI** includes utilities for all these OAuth2 authentication flows in `fastapi.security.oauth2`. diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index d7fbf853d1..8bba5e369c 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -27,6 +27,8 @@ class Dependant: call: Callable = None, request_param_name: str = None, background_tasks_param_name: str = None, + security_scopes_param_name: str = None, + security_scopes: List[str] = None, ) -> None: self.path_params = path_params or [] self.query_params = query_params or [] @@ -37,5 +39,7 @@ class Dependant: self.security_requirements = security_schemes or [] self.request_param_name = request_param_name self.background_tasks_param_name = background_tasks_param_name + self.security_scopes = security_scopes + self.security_scopes_param_name = security_scopes_param_name self.name = name self.call = call diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 86b4ca0058..4cf737d670 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -20,6 +20,8 @@ from uuid import UUID from fastapi import params from fastapi.dependencies.models import Dependant, SecurityRequirement from fastapi.security.base import SecurityBase +from fastapi.security.oauth2 import OAuth2, SecurityScopes +from fastapi.security.open_id_connect_url import OpenIdConnect from fastapi.utils import UnconstrainedConfig, get_path_param_names from pydantic import Schema, create_model from pydantic.error_wrappers import ErrorWrapper @@ -46,18 +48,32 @@ param_supported_types = ( ) -def get_sub_dependant(*, param: inspect.Parameter, path: str) -> Dependant: +def get_sub_dependant( + *, param: inspect.Parameter, path: str, security_scopes: List[str] = None +) -> Dependant: depends: params.Depends = param.default if depends.dependency: dependency = depends.dependency else: dependency = param.annotation - sub_dependant = get_dependant(path=path, call=dependency, name=param.name) - if isinstance(depends, params.Security) and isinstance(dependency, SecurityBase): + security_requirement = None + security_scopes = security_scopes or [] + if isinstance(depends, params.Security): + dependency_scopes = depends.scopes + security_scopes.extend(dependency_scopes) + if isinstance(dependency, SecurityBase): + use_scopes = [] + if isinstance(dependency, (OAuth2, OpenIdConnect)): + use_scopes = security_scopes security_requirement = SecurityRequirement( - security_scheme=dependency, scopes=depends.scopes + security_scheme=dependency, scopes=use_scopes ) + sub_dependant = get_dependant( + path=path, call=dependency, name=param.name, security_scopes=security_scopes + ) + if security_requirement: sub_dependant.security_requirements.append(security_requirement) + sub_dependant.security_scopes = security_scopes return sub_dependant @@ -81,7 +97,9 @@ def get_flat_dependant(dependant: Dependant) -> Dependant: return flat_dependant -def get_dependant(*, path: str, call: Callable, name: str = None) -> Dependant: +def get_dependant( + *, path: str, call: Callable, name: str = None, security_scopes: List[str] = None +) -> Dependant: path_param_names = get_path_param_names(path) endpoint_signature = inspect.signature(call) signature_params = endpoint_signature.parameters @@ -89,7 +107,9 @@ def get_dependant(*, path: str, call: Callable, name: str = None) -> Dependant: for param_name in signature_params: param = signature_params[param_name] if isinstance(param.default, params.Depends): - sub_dependant = get_sub_dependant(param=param, path=path) + sub_dependant = get_sub_dependant( + param=param, path=path, security_scopes=security_scopes + ) dependant.dependencies.append(sub_dependant) for param_name in signature_params: param = signature_params[param_name] @@ -138,6 +158,8 @@ def get_dependant(*, path: str, call: Callable, name: str = None) -> Dependant: dependant.request_param_name = param_name elif lenient_issubclass(param.annotation, BackgroundTasks): dependant.background_tasks_param_name = param_name + elif lenient_issubclass(param.annotation, SecurityScopes): + dependant.security_scopes_param_name = param_name elif not isinstance(param.default, params.Depends): add_param_to_body_fields(param=param, dependant=dependant) return dependant @@ -282,6 +304,10 @@ async def solve_dependencies( if background_tasks is None: background_tasks = BackgroundTasks() values[dependant.background_tasks_param_name] = background_tasks + if dependant.security_scopes_param_name: + values[dependant.security_scopes_param_name] = SecurityScopes( + scopes=dependant.security_scopes + ) return values, errors, background_tasks diff --git a/fastapi/security/__init__.py b/fastapi/security/__init__.py index 32d69e74d3..de88d8f151 100644 --- a/fastapi/security/__init__.py +++ b/fastapi/security/__init__.py @@ -6,5 +6,10 @@ from .http import ( HTTPBearer, HTTPDigest, ) -from .oauth2 import OAuth2, OAuth2PasswordBearer, OAuth2PasswordRequestForm +from .oauth2 import ( + OAuth2, + OAuth2PasswordBearer, + OAuth2PasswordRequestForm, + SecurityScopes, +) from .open_id_connect_url import OpenIdConnect diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index d779bcae1e..31ddc946df 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import List, Optional from fastapi.openapi.models import OAuth2 as OAuth2Model, OAuthFlows as OAuthFlowsModel from fastapi.params import Form @@ -159,3 +159,8 @@ class OAuth2PasswordBearer(OAuth2): else: return None return param + + +class SecurityScopes: + def __init__(self, scopes: List[str] = None): + self.scopes = scopes or [] diff --git a/mkdocs.yml b/mkdocs.yml index 811d3c197d..d7cf5f2422 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -56,6 +56,7 @@ nav: - Get Current User: 'tutorial/security/get-current-user.md' - Simple OAuth2 with Password and Bearer: 'tutorial/security/simple-oauth2.md' - OAuth2 with Password (and hashing), Bearer with JWT tokens: 'tutorial/security/oauth2-jwt.md' + - OAuth2 scopes: 'tutorial/security/oauth2-scopes.md' - Using the Request Directly: 'tutorial/using-request-directly.md' - SQL (Relational) Databases: 'tutorial/sql-databases.md' - Async SQL (Relational) Databases: 'tutorial/async-sql-databases.md' diff --git a/tests/test_tutorial/test_security/test_tutorial005.py b/tests/test_tutorial/test_security/test_tutorial005.py new file mode 100644 index 0000000000..d0f0bdf044 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial005.py @@ -0,0 +1,313 @@ +from starlette.testclient import TestClient + +from security.tutorial005 import ( + app, + create_access_token, + fake_users_db, + get_password_hash, + verify_password, +) + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "Fast API", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Token"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Route Login Access Token Post", + "operationId": "route_login_access_token_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_route_login_access_token" + } + } + }, + "required": True, + }, + } + }, + "/users/me/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + "summary": "Read Users Me Get", + "operationId": "read_users_me_users_me__get", + "security": [{"OAuth2PasswordBearer": ["me"]}], + } + }, + "/users/me/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Own Items Get", + "operationId": "read_own_items_users_me_items__get", + "security": [{"OAuth2PasswordBearer": ["items", "me"]}], + } + }, + }, + "components": { + "schemas": { + "Body_route_login_access_token": { + "title": "Body_route_login_access_token", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant_Type", + "pattern": "password", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": {"title": "Client_Id", "type": "string"}, + "client_secret": {"title": "Client_Secret", "type": "string"}, + }, + }, + "Token": { + "title": "Token", + "required": ["access_token", "token_type"], + "type": "object", + "properties": { + "access_token": {"title": "Access_Token", "type": "string"}, + "token_type": {"title": "Token_Type", "type": "string"}, + }, + }, + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": {"title": "Email", "type": "string"}, + "full_name": {"title": "Full_Name", "type": "string"}, + "disabled": {"title": "Disabled", "type": "boolean"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "me": "Read information about the current user.", + "items": "Read items.", + }, + "tokenUrl": "/token", + } + }, + } + }, + }, +} + + +def get_access_token(username="johndoe", password="secret", scope=None): + data = {"username": username, "password": password} + if scope: + data["scope"] = scope + response = client.post("/token", data=data) + content = response.json() + access_token = content.get("access_token") + return access_token + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == openapi_schema + + +def test_login(): + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200 + content = response.json() + assert "access_token" in content + assert content["token_type"] == "bearer" + + +def test_login_incorrect_password(): + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Incorrect username or password"} + + +def test_login_incorrect_username(): + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 400 + assert response.json() == {"detail": "Incorrect username or password"} + + +def test_no_token(): + response = client.get("/users/me") + assert response.status_code == 403 + assert response.json() == {"detail": "Not authenticated"} + + +def test_token(): + access_token = get_access_token(scope="me") + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + print(response.json()) + assert response.status_code == 200 + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "disabled": False, + } + + +def test_incorrect_token(): + response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) + assert response.status_code == 403 + assert response.json() == {"detail": "Could not validate credentials"} + + +def test_incorrect_token_type(): + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 403 + assert response.json() == {"detail": "Not authenticated"} + + +def test_verify_password(): + assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) + + +def test_get_password_hash(): + assert get_password_hash("secretalice") + + +def test_create_access_token(): + access_token = create_access_token(data={"data": "foo"}) + assert access_token + + +def test_token_no_sub(): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" + }, + ) + assert response.status_code == 403 + assert response.json() == {"detail": "Could not validate credentials"} + + +def test_token_no_username(): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" + }, + ) + assert response.status_code == 403 + assert response.json() == {"detail": "Could not validate credentials"} + + +def test_token_no_scope(): + access_token = get_access_token() + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 403 + assert response.json() == {"detail": "Not enough permissions"} + + +def test_token_inexistent_user(): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" + }, + ) + assert response.status_code == 403 + assert response.json() == {"detail": "Could not validate credentials"} + + +def test_token_inactive_user(): + access_token = get_access_token( + username="alice", password="secretalice", scope="me" + ) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + print(response.json()) + assert response.status_code == 400 + assert response.json() == {"detail": "Inactive user"} + + +def test_read_items(): + access_token = get_access_token(scope="me items") + response = client.get( + "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200 + assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] -- 2.47.3