Coverage for fastapi/security/oauth2.py: 100%
64 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-08 03:53 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-08 03:53 +0000
1from typing import Any, Dict, List, Optional, Union, cast 1abcde
3from fastapi.exceptions import HTTPException 1abcde
4from fastapi.openapi.models import OAuth2 as OAuth2Model 1abcde
5from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel 1abcde
6from fastapi.param_functions import Form 1abcde
7from fastapi.security.base import SecurityBase 1abcde
8from fastapi.security.utils import get_authorization_scheme_param 1abcde
9from starlette.requests import Request 1abcde
10from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN 1abcde
12# TODO: import from typing when deprecating Python 3.9
13from typing_extensions import Annotated, Doc 1abcde
16class OAuth2PasswordRequestForm: 1abcde
17 """
18 This is a dependency class to collect the `username` and `password` as form data
19 for an OAuth2 password flow.
21 The OAuth2 specification dictates that for a password flow the data should be
22 collected using form data (instead of JSON) and that it should have the specific
23 fields `username` and `password`.
25 All the initialization parameters are extracted from the request.
27 Read more about it in the
28 [FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/).
30 ## Example
32 ```python
33 from typing import Annotated
35 from fastapi import Depends, FastAPI
36 from fastapi.security import OAuth2PasswordRequestForm
38 app = FastAPI()
41 @app.post("/login")
42 def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
43 data = {}
44 data["scopes"] = []
45 for scope in form_data.scopes:
46 data["scopes"].append(scope)
47 if form_data.client_id:
48 data["client_id"] = form_data.client_id
49 if form_data.client_secret:
50 data["client_secret"] = form_data.client_secret
51 return data
52 ```
54 Note that for OAuth2 the scope `items:read` is a single scope in an opaque string.
55 You could have custom internal logic to separate it by colon caracters (`:`) or
56 similar, and get the two parts `items` and `read`. Many applications do that to
57 group and organize permissions, you could do it as well in your application, just
58 know that that it is application specific, it's not part of the specification.
59 """
61 def __init__( 1abcde
62 self,
63 *,
64 grant_type: Annotated[
65 Union[str, None],
66 Form(pattern="password"),
67 Doc(
68 """
69 The OAuth2 spec says it is required and MUST be the fixed string
70 "password". Nevertheless, this dependency class is permissive and
71 allows not passing it. If you want to enforce it, use instead the
72 `OAuth2PasswordRequestFormStrict` dependency.
73 """
74 ),
75 ] = None,
76 username: Annotated[
77 str,
78 Form(),
79 Doc(
80 """
81 `username` string. The OAuth2 spec requires the exact field name
82 `username`.
83 """
84 ),
85 ],
86 password: Annotated[
87 str,
88 Form(),
89 Doc(
90 """
91 `password` string. The OAuth2 spec requires the exact field name
92 `password".
93 """
94 ),
95 ],
96 scope: Annotated[
97 str,
98 Form(),
99 Doc(
100 """
101 A single string with actually several scopes separated by spaces. Each
102 scope is also a string.
104 For example, a single string with:
106 ```python
107 "items:read items:write users:read profile openid"
108 ````
110 would represent the scopes:
112 * `items:read`
113 * `items:write`
114 * `users:read`
115 * `profile`
116 * `openid`
117 """
118 ),
119 ] = "",
120 client_id: Annotated[
121 Union[str, None],
122 Form(),
123 Doc(
124 """
125 If there's a `client_id`, it can be sent as part of the form fields.
126 But the OAuth2 specification recommends sending the `client_id` and
127 `client_secret` (if any) using HTTP Basic auth.
128 """
129 ),
130 ] = None,
131 client_secret: Annotated[
132 Union[str, None],
133 Form(),
134 Doc(
135 """
136 If there's a `client_password` (and a `client_id`), they can be sent
137 as part of the form fields. But the OAuth2 specification recommends
138 sending the `client_id` and `client_secret` (if any) using HTTP Basic
139 auth.
140 """
141 ),
142 ] = None,
143 ):
144 self.grant_type = grant_type 1abcde
145 self.username = username 1abcde
146 self.password = password 1abcde
147 self.scopes = scope.split() 1abcde
148 self.client_id = client_id 1abcde
149 self.client_secret = client_secret 1abcde
152class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm): 1abcde
153 """
154 This is a dependency class to collect the `username` and `password` as form data
155 for an OAuth2 password flow.
157 The OAuth2 specification dictates that for a password flow the data should be
158 collected using form data (instead of JSON) and that it should have the specific
159 fields `username` and `password`.
161 All the initialization parameters are extracted from the request.
163 The only difference between `OAuth2PasswordRequestFormStrict` and
164 `OAuth2PasswordRequestForm` is that `OAuth2PasswordRequestFormStrict` requires the
165 client to send the form field `grant_type` with the value `"password"`, which
166 is required in the OAuth2 specification (it seems that for no particular reason),
167 while for `OAuth2PasswordRequestForm` `grant_type` is optional.
169 Read more about it in the
170 [FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/).
172 ## Example
174 ```python
175 from typing import Annotated
177 from fastapi import Depends, FastAPI
178 from fastapi.security import OAuth2PasswordRequestForm
180 app = FastAPI()
183 @app.post("/login")
184 def login(form_data: Annotated[OAuth2PasswordRequestFormStrict, Depends()]):
185 data = {}
186 data["scopes"] = []
187 for scope in form_data.scopes:
188 data["scopes"].append(scope)
189 if form_data.client_id:
190 data["client_id"] = form_data.client_id
191 if form_data.client_secret:
192 data["client_secret"] = form_data.client_secret
193 return data
194 ```
196 Note that for OAuth2 the scope `items:read` is a single scope in an opaque string.
197 You could have custom internal logic to separate it by colon caracters (`:`) or
198 similar, and get the two parts `items` and `read`. Many applications do that to
199 group and organize permissions, you could do it as well in your application, just
200 know that that it is application specific, it's not part of the specification.
203 grant_type: the OAuth2 spec says it is required and MUST be the fixed string "password".
204 This dependency is strict about it. If you want to be permissive, use instead the
205 OAuth2PasswordRequestForm dependency class.
206 username: username string. The OAuth2 spec requires the exact field name "username".
207 password: password string. The OAuth2 spec requires the exact field name "password".
208 scope: Optional string. Several scopes (each one a string) separated by spaces. E.g.
209 "items:read items:write users:read profile openid"
210 client_id: optional string. OAuth2 recommends sending the client_id and client_secret (if any)
211 using HTTP Basic auth, as: client_id:client_secret
212 client_secret: optional string. OAuth2 recommends sending the client_id and client_secret (if any)
213 using HTTP Basic auth, as: client_id:client_secret
214 """
216 def __init__( 1abcde
217 self,
218 grant_type: Annotated[
219 str,
220 Form(pattern="password"),
221 Doc(
222 """
223 The OAuth2 spec says it is required and MUST be the fixed string
224 "password". This dependency is strict about it. If you want to be
225 permissive, use instead the `OAuth2PasswordRequestForm` dependency
226 class.
227 """
228 ),
229 ],
230 username: Annotated[
231 str,
232 Form(),
233 Doc(
234 """
235 `username` string. The OAuth2 spec requires the exact field name
236 `username`.
237 """
238 ),
239 ],
240 password: Annotated[
241 str,
242 Form(),
243 Doc(
244 """
245 `password` string. The OAuth2 spec requires the exact field name
246 `password".
247 """
248 ),
249 ],
250 scope: Annotated[
251 str,
252 Form(),
253 Doc(
254 """
255 A single string with actually several scopes separated by spaces. Each
256 scope is also a string.
258 For example, a single string with:
260 ```python
261 "items:read items:write users:read profile openid"
262 ````
264 would represent the scopes:
266 * `items:read`
267 * `items:write`
268 * `users:read`
269 * `profile`
270 * `openid`
271 """
272 ),
273 ] = "",
274 client_id: Annotated[
275 Union[str, None],
276 Form(),
277 Doc(
278 """
279 If there's a `client_id`, it can be sent as part of the form fields.
280 But the OAuth2 specification recommends sending the `client_id` and
281 `client_secret` (if any) using HTTP Basic auth.
282 """
283 ),
284 ] = None,
285 client_secret: Annotated[
286 Union[str, None],
287 Form(),
288 Doc(
289 """
290 If there's a `client_password` (and a `client_id`), they can be sent
291 as part of the form fields. But the OAuth2 specification recommends
292 sending the `client_id` and `client_secret` (if any) using HTTP Basic
293 auth.
294 """
295 ),
296 ] = None,
297 ):
298 super().__init__( 1abcde
299 grant_type=grant_type,
300 username=username,
301 password=password,
302 scope=scope,
303 client_id=client_id,
304 client_secret=client_secret,
305 )
308class OAuth2(SecurityBase): 1abcde
309 """
310 This is the base class for OAuth2 authentication, an instance of it would be used
311 as a dependency. All other OAuth2 classes inherit from it and customize it for
312 each OAuth2 flow.
314 You normally would not create a new class inheriting from it but use one of the
315 existing subclasses, and maybe compose them if you want to support multiple flows.
317 Read more about it in the
318 [FastAPI docs for Security](https://fastapi.tiangolo.com/tutorial/security/).
319 """
321 def __init__( 1abcde
322 self,
323 *,
324 flows: Annotated[
325 Union[OAuthFlowsModel, Dict[str, Dict[str, Any]]],
326 Doc(
327 """
328 The dictionary of OAuth2 flows.
329 """
330 ),
331 ] = OAuthFlowsModel(),
332 scheme_name: Annotated[
333 Optional[str],
334 Doc(
335 """
336 Security scheme name.
338 It will be included in the generated OpenAPI (e.g. visible at `/docs`).
339 """
340 ),
341 ] = None,
342 description: Annotated[
343 Optional[str],
344 Doc(
345 """
346 Security scheme description.
348 It will be included in the generated OpenAPI (e.g. visible at `/docs`).
349 """
350 ),
351 ] = None,
352 auto_error: Annotated[
353 bool,
354 Doc(
355 """
356 By default, if no HTTP Authorization header is provided, required for
357 OAuth2 authentication, it will automatically cancel the request and
358 send the client an error.
360 If `auto_error` is set to `False`, when the HTTP Authorization header
361 is not available, instead of erroring out, the dependency result will
362 be `None`.
364 This is useful when you want to have optional authentication.
366 It is also useful when you want to have authentication that can be
367 provided in one of multiple optional ways (for example, with OAuth2
368 or in a cookie).
369 """
370 ),
371 ] = True,
372 ):
373 self.model = OAuth2Model( 1abcde
374 flows=cast(OAuthFlowsModel, flows), description=description
375 )
376 self.scheme_name = scheme_name or self.__class__.__name__ 1abcde
377 self.auto_error = auto_error 1abcde
379 async def __call__(self, request: Request) -> Optional[str]: 1abcde
380 authorization = request.headers.get("Authorization") 1abcde
381 if not authorization: 1abcde
382 if self.auto_error: 1abcde
383 raise HTTPException( 1abcde
384 status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
385 )
386 else:
387 return None 1abcde
388 return authorization 1abcde
391class OAuth2PasswordBearer(OAuth2): 1abcde
392 """
393 OAuth2 flow for authentication using a bearer token obtained with a password.
394 An instance of it would be used as a dependency.
396 Read more about it in the
397 [FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/).
398 """
400 def __init__( 1abcde
401 self,
402 tokenUrl: Annotated[
403 str,
404 Doc(
405 """
406 The URL to obtain the OAuth2 token. This would be the *path operation*
407 that has `OAuth2PasswordRequestForm` as a dependency.
408 """
409 ),
410 ],
411 scheme_name: Annotated[
412 Optional[str],
413 Doc(
414 """
415 Security scheme name.
417 It will be included in the generated OpenAPI (e.g. visible at `/docs`).
418 """
419 ),
420 ] = None,
421 scopes: Annotated[
422 Optional[Dict[str, str]],
423 Doc(
424 """
425 The OAuth2 scopes that would be required by the *path operations* that
426 use this dependency.
427 """
428 ),
429 ] = None,
430 description: Annotated[
431 Optional[str],
432 Doc(
433 """
434 Security scheme description.
436 It will be included in the generated OpenAPI (e.g. visible at `/docs`).
437 """
438 ),
439 ] = None,
440 auto_error: Annotated[
441 bool,
442 Doc(
443 """
444 By default, if no HTTP Authorization header is provided, required for
445 OAuth2 authentication, it will automatically cancel the request and
446 send the client an error.
448 If `auto_error` is set to `False`, when the HTTP Authorization header
449 is not available, instead of erroring out, the dependency result will
450 be `None`.
452 This is useful when you want to have optional authentication.
454 It is also useful when you want to have authentication that can be
455 provided in one of multiple optional ways (for example, with OAuth2
456 or in a cookie).
457 """
458 ),
459 ] = True,
460 ):
461 if not scopes: 1abcde
462 scopes = {} 1abcde
463 flows = OAuthFlowsModel( 1abcde
464 password=cast(Any, {"tokenUrl": tokenUrl, "scopes": scopes})
465 )
466 super().__init__( 1abcde
467 flows=flows,
468 scheme_name=scheme_name,
469 description=description,
470 auto_error=auto_error,
471 )
473 async def __call__(self, request: Request) -> Optional[str]: 1abcde
474 authorization = request.headers.get("Authorization") 1abcde
475 scheme, param = get_authorization_scheme_param(authorization) 1abcde
476 if not authorization or scheme.lower() != "bearer": 1abcde
477 if self.auto_error: 1abcde
478 raise HTTPException( 1abcde
479 status_code=HTTP_401_UNAUTHORIZED,
480 detail="Not authenticated",
481 headers={"WWW-Authenticate": "Bearer"},
482 )
483 else:
484 return None 1abcde
485 return param 1abcde
488class OAuth2AuthorizationCodeBearer(OAuth2): 1abcde
489 """
490 OAuth2 flow for authentication using a bearer token obtained with an OAuth2 code
491 flow. An instance of it would be used as a dependency.
492 """
494 def __init__( 1abcde
495 self,
496 authorizationUrl: str,
497 tokenUrl: Annotated[
498 str,
499 Doc(
500 """
501 The URL to obtain the OAuth2 token.
502 """
503 ),
504 ],
505 refreshUrl: Annotated[
506 Optional[str],
507 Doc(
508 """
509 The URL to refresh the token and obtain a new one.
510 """
511 ),
512 ] = None,
513 scheme_name: Annotated[
514 Optional[str],
515 Doc(
516 """
517 Security scheme name.
519 It will be included in the generated OpenAPI (e.g. visible at `/docs`).
520 """
521 ),
522 ] = None,
523 scopes: Annotated[
524 Optional[Dict[str, str]],
525 Doc(
526 """
527 The OAuth2 scopes that would be required by the *path operations* that
528 use this dependency.
529 """
530 ),
531 ] = None,
532 description: Annotated[
533 Optional[str],
534 Doc(
535 """
536 Security scheme description.
538 It will be included in the generated OpenAPI (e.g. visible at `/docs`).
539 """
540 ),
541 ] = None,
542 auto_error: Annotated[
543 bool,
544 Doc(
545 """
546 By default, if no HTTP Authorization header is provided, required for
547 OAuth2 authentication, it will automatically cancel the request and
548 send the client an error.
550 If `auto_error` is set to `False`, when the HTTP Authorization header
551 is not available, instead of erroring out, the dependency result will
552 be `None`.
554 This is useful when you want to have optional authentication.
556 It is also useful when you want to have authentication that can be
557 provided in one of multiple optional ways (for example, with OAuth2
558 or in a cookie).
559 """
560 ),
561 ] = True,
562 ):
563 if not scopes: 1abcde
564 scopes = {} 1abcde
565 flows = OAuthFlowsModel( 1abcde
566 authorizationCode=cast(
567 Any,
568 {
569 "authorizationUrl": authorizationUrl,
570 "tokenUrl": tokenUrl,
571 "refreshUrl": refreshUrl,
572 "scopes": scopes,
573 },
574 )
575 )
576 super().__init__( 1abcde
577 flows=flows,
578 scheme_name=scheme_name,
579 description=description,
580 auto_error=auto_error,
581 )
583 async def __call__(self, request: Request) -> Optional[str]: 1abcde
584 authorization = request.headers.get("Authorization") 1abcde
585 scheme, param = get_authorization_scheme_param(authorization) 1abcde
586 if not authorization or scheme.lower() != "bearer": 1abcde
587 if self.auto_error: 1abcde
588 raise HTTPException( 1abcde
589 status_code=HTTP_401_UNAUTHORIZED,
590 detail="Not authenticated",
591 headers={"WWW-Authenticate": "Bearer"},
592 )
593 else:
594 return None # pragma: nocover
595 return param 1abcde
598class SecurityScopes: 1abcde
599 """
600 This is a special class that you can define in a parameter in a dependency to
601 obtain the OAuth2 scopes required by all the dependencies in the same chain.
603 This way, multiple dependencies can have different scopes, even when used in the
604 same *path operation*. And with this, you can access all the scopes required in
605 all those dependencies in a single place.
607 Read more about it in the
608 [FastAPI docs for OAuth2 scopes](https://fastapi.tiangolo.com/advanced/security/oauth2-scopes/).
609 """
611 def __init__( 1abcde
612 self,
613 scopes: Annotated[
614 Optional[List[str]],
615 Doc(
616 """
617 This will be filled by FastAPI.
618 """
619 ),
620 ] = None,
621 ):
622 self.scopes: Annotated[ 1abcde
623 List[str],
624 Doc(
625 """
626 The list of all the scopes required by dependencies.
627 """
628 ),
629 ] = scopes or []
630 self.scope_str: Annotated[ 1abcde
631 str,
632 Doc(
633 """
634 All the scopes required by all the dependencies in a single string
635 separated by spaces, as defined in the OAuth2 specification.
636 """
637 ),
638 ] = " ".join(self.scopes)