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

1from typing import Any, Dict, List, Optional, Union, cast 1abcde

2 

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

11 

12# TODO: import from typing when deprecating Python 3.9 

13from typing_extensions import Annotated, Doc 1abcde

14 

15 

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. 

20 

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`. 

24 

25 All the initialization parameters are extracted from the request. 

26 

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/). 

29 

30 ## Example 

31 

32 ```python 

33 from typing import Annotated 

34 

35 from fastapi import Depends, FastAPI 

36 from fastapi.security import OAuth2PasswordRequestForm 

37 

38 app = FastAPI() 

39 

40 

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 ``` 

53 

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 """ 

60 

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. 

103 

104 For example, a single string with: 

105 

106 ```python 

107 "items:read items:write users:read profile openid" 

108 ```` 

109 

110 would represent the scopes: 

111 

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

150 

151 

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. 

156 

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`. 

160 

161 All the initialization parameters are extracted from the request. 

162 

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. 

168 

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/). 

171 

172 ## Example 

173 

174 ```python 

175 from typing import Annotated 

176 

177 from fastapi import Depends, FastAPI 

178 from fastapi.security import OAuth2PasswordRequestForm 

179 

180 app = FastAPI() 

181 

182 

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 ``` 

195 

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. 

201 

202 

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 """ 

215 

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. 

257 

258 For example, a single string with: 

259 

260 ```python 

261 "items:read items:write users:read profile openid" 

262 ```` 

263 

264 would represent the scopes: 

265 

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 ) 

306 

307 

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. 

313 

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. 

316 

317 Read more about it in the 

318 [FastAPI docs for Security](https://fastapi.tiangolo.com/tutorial/security/). 

319 """ 

320 

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. 

337 

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. 

347 

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. 

359 

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`. 

363 

364 This is useful when you want to have optional authentication. 

365 

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

378 

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

389 

390 

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. 

395 

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 """ 

399 

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. 

416 

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. 

435 

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. 

447 

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`. 

451 

452 This is useful when you want to have optional authentication. 

453 

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 ) 

472 

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

486 

487 

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 """ 

493 

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. 

518 

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. 

537 

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. 

549 

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`. 

553 

554 This is useful when you want to have optional authentication. 

555 

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 ) 

582 

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

596 

597 

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. 

602 

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. 

606 

607 Read more about it in the 

608 [FastAPI docs for OAuth2 scopes](https://fastapi.tiangolo.com/advanced/security/oauth2-scopes/). 

609 """ 

610 

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)