| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638 |
- from typing import Any, Dict, List, Optional, Union, cast
- from fastapi.exceptions import HTTPException
- from fastapi.openapi.models import OAuth2 as OAuth2Model
- from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel
- from fastapi.param_functions import Form
- from fastapi.security.base import SecurityBase
- from fastapi.security.utils import get_authorization_scheme_param
- from starlette.requests import Request
- from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
- # TODO: import from typing when deprecating Python 3.9
- from typing_extensions import Annotated, Doc
- class OAuth2PasswordRequestForm:
- """
- This is a dependency class to collect the `username` and `password` as form data
- for an OAuth2 password flow.
- The OAuth2 specification dictates that for a password flow the data should be
- collected using form data (instead of JSON) and that it should have the specific
- fields `username` and `password`.
- All the initialization parameters are extracted from the request.
- Read more about it in the
- [FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/).
- ## Example
- ```python
- from typing import Annotated
- from fastapi import Depends, FastAPI
- from fastapi.security import OAuth2PasswordRequestForm
- app = FastAPI()
- @app.post("/login")
- def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
- data = {}
- data["scopes"] = []
- for scope in form_data.scopes:
- data["scopes"].append(scope)
- if form_data.client_id:
- data["client_id"] = form_data.client_id
- if form_data.client_secret:
- data["client_secret"] = form_data.client_secret
- return data
- ```
- Note that for OAuth2 the scope `items:read` is a single scope in an opaque string.
- You could have custom internal logic to separate it by colon characters (`:`) or
- similar, and get the two parts `items` and `read`. Many applications do that to
- group and organize permissions, you could do it as well in your application, just
- know that that it is application specific, it's not part of the specification.
- """
- def __init__(
- self,
- *,
- grant_type: Annotated[
- Union[str, None],
- Form(pattern="^password$"),
- Doc(
- """
- The OAuth2 spec says it is required and MUST be the fixed string
- "password". Nevertheless, this dependency class is permissive and
- allows not passing it. If you want to enforce it, use instead the
- `OAuth2PasswordRequestFormStrict` dependency.
- """
- ),
- ] = None,
- username: Annotated[
- str,
- Form(),
- Doc(
- """
- `username` string. The OAuth2 spec requires the exact field name
- `username`.
- """
- ),
- ],
- password: Annotated[
- str,
- Form(),
- Doc(
- """
- `password` string. The OAuth2 spec requires the exact field name
- `password".
- """
- ),
- ],
- scope: Annotated[
- str,
- Form(),
- Doc(
- """
- A single string with actually several scopes separated by spaces. Each
- scope is also a string.
- For example, a single string with:
- ```python
- "items:read items:write users:read profile openid"
- ````
- would represent the scopes:
- * `items:read`
- * `items:write`
- * `users:read`
- * `profile`
- * `openid`
- """
- ),
- ] = "",
- client_id: Annotated[
- Union[str, None],
- Form(),
- Doc(
- """
- If there's a `client_id`, it can be sent as part of the form fields.
- But the OAuth2 specification recommends sending the `client_id` and
- `client_secret` (if any) using HTTP Basic auth.
- """
- ),
- ] = None,
- client_secret: Annotated[
- Union[str, None],
- Form(),
- Doc(
- """
- If there's a `client_password` (and a `client_id`), they can be sent
- as part of the form fields. But the OAuth2 specification recommends
- sending the `client_id` and `client_secret` (if any) using HTTP Basic
- auth.
- """
- ),
- ] = None,
- ):
- self.grant_type = grant_type
- self.username = username
- self.password = password
- self.scopes = scope.split()
- self.client_id = client_id
- self.client_secret = client_secret
- class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm):
- """
- This is a dependency class to collect the `username` and `password` as form data
- for an OAuth2 password flow.
- The OAuth2 specification dictates that for a password flow the data should be
- collected using form data (instead of JSON) and that it should have the specific
- fields `username` and `password`.
- All the initialization parameters are extracted from the request.
- The only difference between `OAuth2PasswordRequestFormStrict` and
- `OAuth2PasswordRequestForm` is that `OAuth2PasswordRequestFormStrict` requires the
- client to send the form field `grant_type` with the value `"password"`, which
- is required in the OAuth2 specification (it seems that for no particular reason),
- while for `OAuth2PasswordRequestForm` `grant_type` is optional.
- Read more about it in the
- [FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/).
- ## Example
- ```python
- from typing import Annotated
- from fastapi import Depends, FastAPI
- from fastapi.security import OAuth2PasswordRequestForm
- app = FastAPI()
- @app.post("/login")
- def login(form_data: Annotated[OAuth2PasswordRequestFormStrict, Depends()]):
- data = {}
- data["scopes"] = []
- for scope in form_data.scopes:
- data["scopes"].append(scope)
- if form_data.client_id:
- data["client_id"] = form_data.client_id
- if form_data.client_secret:
- data["client_secret"] = form_data.client_secret
- return data
- ```
- Note that for OAuth2 the scope `items:read` is a single scope in an opaque string.
- You could have custom internal logic to separate it by colon characters (`:`) or
- similar, and get the two parts `items` and `read`. Many applications do that to
- group and organize permissions, you could do it as well in your application, just
- know that that it is application specific, it's not part of the specification.
- grant_type: the OAuth2 spec says it is required and MUST be the fixed string "password".
- This dependency is strict about it. If you want to be permissive, use instead the
- OAuth2PasswordRequestForm dependency class.
- username: username string. The OAuth2 spec requires the exact field name "username".
- password: password string. The OAuth2 spec requires the exact field name "password".
- scope: Optional string. Several scopes (each one a string) separated by spaces. E.g.
- "items:read items:write users:read profile openid"
- client_id: optional string. OAuth2 recommends sending the client_id and client_secret (if any)
- using HTTP Basic auth, as: client_id:client_secret
- client_secret: optional string. OAuth2 recommends sending the client_id and client_secret (if any)
- using HTTP Basic auth, as: client_id:client_secret
- """
- def __init__(
- self,
- grant_type: Annotated[
- str,
- Form(pattern="^password$"),
- Doc(
- """
- The OAuth2 spec says it is required and MUST be the fixed string
- "password". This dependency is strict about it. If you want to be
- permissive, use instead the `OAuth2PasswordRequestForm` dependency
- class.
- """
- ),
- ],
- username: Annotated[
- str,
- Form(),
- Doc(
- """
- `username` string. The OAuth2 spec requires the exact field name
- `username`.
- """
- ),
- ],
- password: Annotated[
- str,
- Form(),
- Doc(
- """
- `password` string. The OAuth2 spec requires the exact field name
- `password".
- """
- ),
- ],
- scope: Annotated[
- str,
- Form(),
- Doc(
- """
- A single string with actually several scopes separated by spaces. Each
- scope is also a string.
- For example, a single string with:
- ```python
- "items:read items:write users:read profile openid"
- ````
- would represent the scopes:
- * `items:read`
- * `items:write`
- * `users:read`
- * `profile`
- * `openid`
- """
- ),
- ] = "",
- client_id: Annotated[
- Union[str, None],
- Form(),
- Doc(
- """
- If there's a `client_id`, it can be sent as part of the form fields.
- But the OAuth2 specification recommends sending the `client_id` and
- `client_secret` (if any) using HTTP Basic auth.
- """
- ),
- ] = None,
- client_secret: Annotated[
- Union[str, None],
- Form(),
- Doc(
- """
- If there's a `client_password` (and a `client_id`), they can be sent
- as part of the form fields. But the OAuth2 specification recommends
- sending the `client_id` and `client_secret` (if any) using HTTP Basic
- auth.
- """
- ),
- ] = None,
- ):
- super().__init__(
- grant_type=grant_type,
- username=username,
- password=password,
- scope=scope,
- client_id=client_id,
- client_secret=client_secret,
- )
- class OAuth2(SecurityBase):
- """
- This is the base class for OAuth2 authentication, an instance of it would be used
- as a dependency. All other OAuth2 classes inherit from it and customize it for
- each OAuth2 flow.
- You normally would not create a new class inheriting from it but use one of the
- existing subclasses, and maybe compose them if you want to support multiple flows.
- Read more about it in the
- [FastAPI docs for Security](https://fastapi.tiangolo.com/tutorial/security/).
- """
- def __init__(
- self,
- *,
- flows: Annotated[
- Union[OAuthFlowsModel, Dict[str, Dict[str, Any]]],
- Doc(
- """
- The dictionary of OAuth2 flows.
- """
- ),
- ] = OAuthFlowsModel(),
- scheme_name: Annotated[
- Optional[str],
- Doc(
- """
- Security scheme name.
- It will be included in the generated OpenAPI (e.g. visible at `/docs`).
- """
- ),
- ] = None,
- description: Annotated[
- Optional[str],
- Doc(
- """
- Security scheme description.
- It will be included in the generated OpenAPI (e.g. visible at `/docs`).
- """
- ),
- ] = None,
- auto_error: Annotated[
- bool,
- Doc(
- """
- By default, if no HTTP Authorization header is provided, required for
- OAuth2 authentication, it will automatically cancel the request and
- send the client an error.
- If `auto_error` is set to `False`, when the HTTP Authorization header
- is not available, instead of erroring out, the dependency result will
- be `None`.
- This is useful when you want to have optional authentication.
- It is also useful when you want to have authentication that can be
- provided in one of multiple optional ways (for example, with OAuth2
- or in a cookie).
- """
- ),
- ] = True,
- ):
- self.model = OAuth2Model(
- flows=cast(OAuthFlowsModel, flows), description=description
- )
- self.scheme_name = scheme_name or self.__class__.__name__
- self.auto_error = auto_error
- async def __call__(self, request: Request) -> Optional[str]:
- authorization = request.headers.get("Authorization")
- if not authorization:
- if self.auto_error:
- raise HTTPException(
- status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
- )
- else:
- return None
- return authorization
- class OAuth2PasswordBearer(OAuth2):
- """
- OAuth2 flow for authentication using a bearer token obtained with a password.
- An instance of it would be used as a dependency.
- Read more about it in the
- [FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/).
- """
- def __init__(
- self,
- tokenUrl: Annotated[
- str,
- Doc(
- """
- The URL to obtain the OAuth2 token. This would be the *path operation*
- that has `OAuth2PasswordRequestForm` as a dependency.
- """
- ),
- ],
- scheme_name: Annotated[
- Optional[str],
- Doc(
- """
- Security scheme name.
- It will be included in the generated OpenAPI (e.g. visible at `/docs`).
- """
- ),
- ] = None,
- scopes: Annotated[
- Optional[Dict[str, str]],
- Doc(
- """
- The OAuth2 scopes that would be required by the *path operations* that
- use this dependency.
- """
- ),
- ] = None,
- description: Annotated[
- Optional[str],
- Doc(
- """
- Security scheme description.
- It will be included in the generated OpenAPI (e.g. visible at `/docs`).
- """
- ),
- ] = None,
- auto_error: Annotated[
- bool,
- Doc(
- """
- By default, if no HTTP Authorization header is provided, required for
- OAuth2 authentication, it will automatically cancel the request and
- send the client an error.
- If `auto_error` is set to `False`, when the HTTP Authorization header
- is not available, instead of erroring out, the dependency result will
- be `None`.
- This is useful when you want to have optional authentication.
- It is also useful when you want to have authentication that can be
- provided in one of multiple optional ways (for example, with OAuth2
- or in a cookie).
- """
- ),
- ] = True,
- ):
- if not scopes:
- scopes = {}
- flows = OAuthFlowsModel(
- password=cast(Any, {"tokenUrl": tokenUrl, "scopes": scopes})
- )
- super().__init__(
- flows=flows,
- scheme_name=scheme_name,
- description=description,
- auto_error=auto_error,
- )
- async def __call__(self, request: Request) -> Optional[str]:
- authorization = request.headers.get("Authorization")
- scheme, param = get_authorization_scheme_param(authorization)
- if not authorization or scheme.lower() != "bearer":
- if self.auto_error:
- raise HTTPException(
- status_code=HTTP_401_UNAUTHORIZED,
- detail="Not authenticated",
- headers={"WWW-Authenticate": "Bearer"},
- )
- else:
- return None
- return param
- class OAuth2AuthorizationCodeBearer(OAuth2):
- """
- OAuth2 flow for authentication using a bearer token obtained with an OAuth2 code
- flow. An instance of it would be used as a dependency.
- """
- def __init__(
- self,
- authorizationUrl: str,
- tokenUrl: Annotated[
- str,
- Doc(
- """
- The URL to obtain the OAuth2 token.
- """
- ),
- ],
- refreshUrl: Annotated[
- Optional[str],
- Doc(
- """
- The URL to refresh the token and obtain a new one.
- """
- ),
- ] = None,
- scheme_name: Annotated[
- Optional[str],
- Doc(
- """
- Security scheme name.
- It will be included in the generated OpenAPI (e.g. visible at `/docs`).
- """
- ),
- ] = None,
- scopes: Annotated[
- Optional[Dict[str, str]],
- Doc(
- """
- The OAuth2 scopes that would be required by the *path operations* that
- use this dependency.
- """
- ),
- ] = None,
- description: Annotated[
- Optional[str],
- Doc(
- """
- Security scheme description.
- It will be included in the generated OpenAPI (e.g. visible at `/docs`).
- """
- ),
- ] = None,
- auto_error: Annotated[
- bool,
- Doc(
- """
- By default, if no HTTP Authorization header is provided, required for
- OAuth2 authentication, it will automatically cancel the request and
- send the client an error.
- If `auto_error` is set to `False`, when the HTTP Authorization header
- is not available, instead of erroring out, the dependency result will
- be `None`.
- This is useful when you want to have optional authentication.
- It is also useful when you want to have authentication that can be
- provided in one of multiple optional ways (for example, with OAuth2
- or in a cookie).
- """
- ),
- ] = True,
- ):
- if not scopes:
- scopes = {}
- flows = OAuthFlowsModel(
- authorizationCode=cast(
- Any,
- {
- "authorizationUrl": authorizationUrl,
- "tokenUrl": tokenUrl,
- "refreshUrl": refreshUrl,
- "scopes": scopes,
- },
- )
- )
- super().__init__(
- flows=flows,
- scheme_name=scheme_name,
- description=description,
- auto_error=auto_error,
- )
- async def __call__(self, request: Request) -> Optional[str]:
- authorization = request.headers.get("Authorization")
- scheme, param = get_authorization_scheme_param(authorization)
- if not authorization or scheme.lower() != "bearer":
- if self.auto_error:
- raise HTTPException(
- status_code=HTTP_401_UNAUTHORIZED,
- detail="Not authenticated",
- headers={"WWW-Authenticate": "Bearer"},
- )
- else:
- return None # pragma: nocover
- return param
- class SecurityScopes:
- """
- This is a special class that you can define in a parameter in a dependency to
- obtain the OAuth2 scopes required by all the dependencies in the same chain.
- This way, multiple dependencies can have different scopes, even when used in the
- same *path operation*. And with this, you can access all the scopes required in
- all those dependencies in a single place.
- Read more about it in the
- [FastAPI docs for OAuth2 scopes](https://fastapi.tiangolo.com/advanced/security/oauth2-scopes/).
- """
- def __init__(
- self,
- scopes: Annotated[
- Optional[List[str]],
- Doc(
- """
- This will be filled by FastAPI.
- """
- ),
- ] = None,
- ):
- self.scopes: Annotated[
- List[str],
- Doc(
- """
- The list of all the scopes required by dependencies.
- """
- ),
- ] = scopes or []
- self.scope_str: Annotated[
- str,
- Doc(
- """
- All the scopes required by all the dependencies in a single string
- separated by spaces, as defined in the OAuth2 specification.
- """
- ),
- ] = " ".join(self.scopes)
|