From 7391056daf49d055bf3a8c812cbb765fe05e4a28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?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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 literal 0 HcmV?d00001 diff --git a/docs/src/security/tutorial004.py b/docs/src/security/tutorial004.py index edbac86f6..bac77a4b1 100644 --- a/docs/src/security/tutorial004.py +++ b/docs/src/security/tutorial004.py @@ -1,7 +1,7 @@ from datetime import datetime, timedelta import jwt -from fastapi import Depends, FastAPI, HTTPException, Security +from fastapi import Depends, FastAPI, HTTPException from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from jwt import PyJWTError from passlib.context import CryptContext @@ -12,7 +12,6 @@ from starlette.status import HTTP_403_FORBIDDEN # openssl rand -hex 32 SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" ALGORITHM = "HS256" -TOKEN_SUBJECT = "access" ACCESS_TOKEN_EXPIRE_MINUTES = 30 @@ -32,7 +31,7 @@ class Token(BaseModel): token_type: str -class TokenPayload(BaseModel): +class TokenData(BaseModel): username: str = None @@ -83,20 +82,26 @@ def create_access_token(*, data: dict, expires_delta: timedelta = None): expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=15) - to_encode.update({"exp": expire, "sub": TOKEN_SUBJECT}) + to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt -async def get_current_user(token: str = Security(oauth2_scheme)): +async def get_current_user(token: str = Depends(oauth2_scheme)): + credentials_exception = HTTPException( + status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials" + ) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - token_data = TokenPayload(**payload) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_data = TokenData(username=username) except PyJWTError: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials" - ) + raise credentials_exception user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception return user @@ -110,15 +115,15 @@ async def get_current_active_user(current_user: User = Depends(get_current_user) async def route_login_access_token(form_data: OAuth2PasswordRequestForm = Depends()): user = authenticate_user(fake_users_db, form_data.username, form_data.password) if not user: - raise HTTPException(status_code=400, detail="Incorrect email or password") + raise HTTPException(status_code=400, detail="Incorrect username or password") access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( - data={"username": form_data.username}, expires_delta=access_token_expires + data={"sub": user.username}, expires_delta=access_token_expires ) return {"access_token": access_token, "token_type": "bearer"} -@app.get("/users/me", response_model=User) +@app.get("/users/me/", response_model=User) async def read_users_me(current_user: User = Depends(get_current_active_user)): return current_user diff --git a/docs/src/security/tutorial005.py b/docs/src/security/tutorial005.py new file mode 100644 index 000000000..f13e2310e --- /dev/null +++ b/docs/src/security/tutorial005.py @@ -0,0 +1,162 @@ +from datetime import datetime, timedelta +from typing import List + +import jwt +from fastapi import Depends, FastAPI, HTTPException, Security +from fastapi.security import ( + OAuth2PasswordBearer, + OAuth2PasswordRequestForm, + SecurityScopes, +) +from jwt import PyJWTError +from passlib.context import CryptContext +from pydantic import BaseModel, ValidationError +from starlette.status import HTTP_403_FORBIDDEN + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Chains", + "email": "alicechains@example.com", + "hashed_password": "$2b$12$gSvqqUPvlXP2tfVFaWK1Be7DlH.PKZbv5H8KnzzVgXXbVxpva.pFm", + "disabled": True, + }, +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: str = None + scopes: List[str] = [] + + +class User(BaseModel): + username: str + email: str = None + full_name: str = None + disabled: bool = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer( + tokenUrl="/token", + scopes={"me": "Read information about the current user.", "items": "Read items."}, +) + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(*, data: dict, expires_delta: timedelta = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user( + security_scopes: SecurityScopes, token: str = Depends(oauth2_scheme) +): + credentials_exception = HTTPException( + status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials" + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_scopes = payload.get("scopes", []) + token_data = TokenData(scopes=token_scopes, username=username) + except (PyJWTError, ValidationError): + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + for scope in security_scopes.scopes: + if scope not in token_data.scopes: + raise HTTPException( + status_code=HTTP_403_FORBIDDEN, detail="Not enough permissions" + ) + return user + + +async def get_current_active_user( + current_user: User = Security(get_current_user, scopes=["me"]) +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token", response_model=Token) +async def route_login_access_token(form_data: OAuth2PasswordRequestForm = Depends()): + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException(status_code=400, detail="Incorrect username or password") + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username, "scopes": form_data.scopes}, + expires_delta=access_token_expires, + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@app.get("/users/me/", response_model=User) +async def read_users_me(current_user: User = Depends(get_current_active_user)): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: User = Security(get_current_active_user, scopes=["items"]) +): + return [{"item_id": "Foo", "owner": current_user.username}] diff --git a/docs/tutorial/security/oauth2-jwt.md b/docs/tutorial/security/oauth2-jwt.md index a28f4e95f..4958d35f2 100644 --- a/docs/tutorial/security/oauth2-jwt.md +++ b/docs/tutorial/security/oauth2-jwt.md @@ -46,7 +46,7 @@ So, the thief won't be able to try to use that password in another system (as ma PassLib is a great Python package to handle password hashes. -It supports many secure hashing algorithms, and utilities to work with them. +It supports many secure hashing algorithms and utilities to work with them. The recommended algorithm is "Bcrypt". @@ -69,7 +69,7 @@ Import the tools we need from `passlib`. Create a PassLib "context". This is what will be used to hash and verify passwords. !!! tip - The PassLib context also has functionality to use different hashing algorithms, deprecate old ones, but allow verifying them, etc. + The PassLib context also has functionality to use different hashing algorithms, including deprecate old ones only to allow verifying them, etc. For example, you could use it to read and verify passwords generated by another system (like Django) but hash any new passwords with a different algorithm like Bcrypt. @@ -81,7 +81,7 @@ And another utility to verify if a received password matches the hash stored. And another one to authenticate and return a user. -```Python hl_lines="7 50 57 58 61 62 71 72 73 74 75 76 77" +```Python hl_lines="7 39 56 57 60 61 70 71 72 73 74 75 76" {!./src/security/tutorial004.py!} ``` @@ -94,7 +94,7 @@ Import the modules installed. Create a random secret key that will be used to sign the JWT tokens. -To generate a secure random secret, key use the command: +To generate a secure random secret key use the command: ```bash openssl rand -hex 32 @@ -104,15 +104,13 @@ And copy the output to the variable `SECRET_KEY` (don't use the one in the examp Create a variable `ALGORITHM` with the algorithm used to sign the JWT token and set it to `"HS256"`. -And another one for the `TOKEN_SUBJECT`, and set it to, for example, `"access"`. - Create a variable for the expiration of the token. Define a Pydantic Model that will be used in the token endpoint for the response. Create a utility function to generate a new access token. -```Python hl_lines="3 6 13 14 15 16 30 31 32 80 81 82 83 84 85 86 87 88" +```Python hl_lines="3 6 13 14 15 29 30 31 79 80 81 82 83 84 85 86 87" {!./src/security/tutorial004.py!} ``` @@ -124,7 +122,7 @@ Decode the received token, verify it, and return the current user. If the token is invalid, return an HTTP error right away. -```Python hl_lines="91 92 93 94 95 96 97 98 99 100" +```Python hl_lines="90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105" {!./src/security/tutorial004.py!} ``` @@ -134,10 +132,33 @@ Create a `timedelta` with the expiration time of the token. Create a real JWT access token and return it. -```Python hl_lines="114 115 116 117 118" +```Python hl_lines="114 115 116 117 118 119 120 121 122 123" {!./src/security/tutorial004.py!} ``` + +### Technical details about the JWT "subject" `sub` + +The JWT specification says that there's a key `sub`, with the subject of the token. + +It's optional to use it, but that's where you would put the user's identification, so we are using it here. + +JWT might be used for other things apart from identifying a user and allowing him to perform operations directly on your API. + +For example, you could identify a "car" or a "blog post". + +Then you could add permissions about that entity, like "drive" (for the car) or "edit" (for the blog). + +And then, you could give that JWT token to a user (or bot), and he could use it to perform those actions (drive the car, or edit the blog post) without even needing to have an account, just with the JWT token your API generated for that. + +Using these ideas, JWT can be used for way more sophisticate scenarios. + +In those cases, several of those entities could have the same ID, let's say `foo` (a user `foo`, a car `foo`, and a blog post `foo`). + +So, to avoid ID collisions, when creating the JWT token for the user, you could prefix the value of the `sub` key, e.g. with `username:`. + +The important thing to have in mind is that the `sub` key should have a unique identifier across the entire application. + ## Check it Run the server and go to the docs: http://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 000000000..51e28daaf --- /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 d7fbf853d..8bba5e369 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 86b4ca005..4cf737d67 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 32d69e74d..de88d8f15 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 d779bcae1..31ddc946d 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 811d3c197..d7cf5f242 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 000000000..d0f0bdf04 --- /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"}]