├── .all-contributorsrc ├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── config.yml ├── dependabot.yml ├── stale.yml └── workflows │ ├── build.yml │ ├── codeql-analysis.yml │ └── documentation.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── docs-overrides └── main.html ├── docs ├── configuration │ ├── authentication │ │ ├── backend.md │ │ ├── index.md │ │ ├── strategies │ │ │ ├── database.md │ │ │ ├── jwt.md │ │ │ └── redis.md │ │ └── transports │ │ │ ├── bearer.md │ │ │ └── cookie.md │ ├── databases │ │ ├── beanie.md │ │ └── sqlalchemy.md │ ├── full-example.md │ ├── oauth.md │ ├── overview.md │ ├── password-hash.md │ ├── routers │ │ ├── auth.md │ │ ├── index.md │ │ ├── register.md │ │ ├── reset.md │ │ ├── users.md │ │ └── verify.md │ ├── schemas.md │ └── user-manager.md ├── cookbook │ └── create-user-programmatically.md ├── favicon.png ├── index.md ├── installation.md ├── migration │ ├── 08_to_1x.md │ ├── 1x_to_2x.md │ ├── 2x_to_3x.md │ ├── 3x_to_4x.md │ ├── 4x_to_5x.md │ ├── 6x_to_7x.md │ ├── 7x_to_8x.md │ ├── 8x_to_9x.md │ └── 9x_to_10x.md ├── src │ ├── cookbook_create_user_programmatically.py │ ├── db_beanie.py │ ├── db_beanie_access_tokens.py │ ├── db_beanie_oauth.py │ ├── db_sqlalchemy.py │ ├── db_sqlalchemy_access_tokens.py │ ├── db_sqlalchemy_oauth.py │ └── user_manager.py └── usage │ ├── current-user.md │ ├── flow.md │ └── routes.md ├── examples ├── beanie-oauth │ ├── app │ │ ├── __init__.py │ │ ├── app.py │ │ ├── db.py │ │ ├── schemas.py │ │ └── users.py │ ├── main.py │ └── requirements.txt ├── beanie │ ├── app │ │ ├── __init__.py │ │ ├── app.py │ │ ├── db.py │ │ ├── schemas.py │ │ └── users.py │ ├── main.py │ └── requirements.txt ├── sqlalchemy-oauth │ ├── app │ │ ├── __init__.py │ │ ├── app.py │ │ ├── db.py │ │ ├── schemas.py │ │ └── users.py │ ├── main.py │ └── requirements.txt └── sqlalchemy │ ├── app │ ├── __init__.py │ ├── app.py │ ├── db.py │ ├── schemas.py │ └── users.py │ ├── main.py │ └── requirements.txt ├── fastapi_users ├── __init__.py ├── authentication │ ├── __init__.py │ ├── authenticator.py │ ├── backend.py │ ├── strategy │ │ ├── __init__.py │ │ ├── base.py │ │ ├── db │ │ │ ├── __init__.py │ │ │ ├── adapter.py │ │ │ ├── models.py │ │ │ └── strategy.py │ │ ├── jwt.py │ │ └── redis.py │ └── transport │ │ ├── __init__.py │ │ ├── base.py │ │ ├── bearer.py │ │ └── cookie.py ├── db │ ├── __init__.py │ └── base.py ├── exceptions.py ├── fastapi_users.py ├── jwt.py ├── manager.py ├── models.py ├── openapi.py ├── password.py ├── py.typed ├── router │ ├── __init__.py │ ├── auth.py │ ├── common.py │ ├── oauth.py │ ├── register.py │ ├── reset.py │ ├── users.py │ └── verify.py ├── schemas.py └── types.py ├── logo.svg ├── logo_github.png ├── mkdocs.yml ├── pyproject.toml ├── setup.cfg ├── test_build.py └── tests ├── __init__.py ├── conftest.py ├── test_authentication_authenticator.py ├── test_authentication_backend.py ├── test_authentication_strategy_db.py ├── test_authentication_strategy_jwt.py ├── test_authentication_strategy_redis.py ├── test_authentication_transport_bearer.py ├── test_authentication_transport_cookie.py ├── test_db_base.py ├── test_fastapi_users.py ├── test_jwt.py ├── test_manager.py ├── test_openapi.py ├── test_router_auth.py ├── test_router_oauth.py ├── test_router_register.py ├── test_router_reset.py ├── test_router_users.py └── test_router_verify.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.yml] 14 | indent_size = 2 15 | 16 | [Makefile] 17 | indent_style = tab 18 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | polar: frankie567 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | 12 | A clear and concise description of what the bug is. 13 | 14 | ## To Reproduce 15 | 16 | Steps to reproduce the behavior: 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | ## Expected behavior 23 | 24 | A clear and concise description of what you expected to happen. 25 | 26 | ## Configuration 27 | - Python version : 28 | - FastAPI version : 29 | - FastAPI Users version : 30 | 31 | ### FastAPI Users configuration 32 | 33 | ```py 34 | # Please copy/paste your FastAPI Users configuration here. 35 | ``` 36 | 37 | ## Additional context 38 | 39 | Add any other context about the problem here. 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: I have a question 🤔 3 | url: https://github.com/fastapi-users/fastapi-users/discussions 4 | about: If you have any question about the usage of FastAPI Users that's not clearly a bug, please open a discussion first. 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | reviewers: 10 | - frankie567 11 | - package-ecosystem: "github-actions" 12 | directory: "/" 13 | schedule: 14 | interval: "weekly" 15 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 14 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - bug 10 | - enhancement 11 | - documentation 12 | # Label to use when marking an issue as stale 13 | staleLabel: stale 14 | # Comment to post when marking an issue as stale. Set to `false` to disable 15 | markComment: > 16 | This issue has been automatically marked as stale because it has not had 17 | recent activity. It will be closed if no further activity occurs. Thank you 18 | for your contributions. 19 | # Comment to post when closing a stale issue. Set to `false` to disable 20 | closeComment: false 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | lint: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python_version: [3.9, '3.10', '3.11', '3.12', '3.13'] 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Python 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: ${{ matrix.python_version }} 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install hatch 23 | - name: Lint and typecheck 24 | run: | 25 | hatch run lint-check 26 | 27 | test: 28 | runs-on: ubuntu-latest 29 | strategy: 30 | matrix: 31 | python_version: [3.9, '3.10', '3.11', '3.12', '3.13'] 32 | 33 | steps: 34 | - uses: actions/checkout@v4 35 | - name: Set up Python 36 | uses: actions/setup-python@v5 37 | with: 38 | python-version: ${{ matrix.python_version }} 39 | - name: Install dependencies 40 | run: | 41 | python -m pip install --upgrade pip 42 | pip install hatch 43 | - name: Test 44 | run: | 45 | hatch run test:test-cov-xml 46 | - uses: codecov/codecov-action@v5 47 | with: 48 | token: ${{ secrets.CODECOV_TOKEN }} 49 | fail_ci_if_error: true 50 | verbose: true 51 | - name: Build and install it on system host 52 | run: | 53 | hatch build 54 | pip install dist/fastapi_users-*.whl 55 | python test_build.py 56 | 57 | release: 58 | runs-on: ubuntu-latest 59 | needs: [lint, test] 60 | if: startsWith(github.ref, 'refs/tags/') 61 | 62 | steps: 63 | - uses: actions/checkout@v4 64 | - name: Set up Python 65 | uses: actions/setup-python@v5 66 | with: 67 | python-version: 3.9 68 | - name: Install dependencies 69 | shell: bash 70 | run: | 71 | python -m pip install --upgrade pip 72 | pip install hatch 73 | - name: Build and publish on PyPI 74 | env: 75 | HATCH_INDEX_USER: ${{ secrets.HATCH_INDEX_USER }} 76 | HATCH_INDEX_AUTH: ${{ secrets.HATCH_INDEX_AUTH }} 77 | run: | 78 | hatch build 79 | hatch publish 80 | - name: Create release 81 | uses: ncipollo/release-action@v1 82 | with: 83 | draft: true 84 | body: ${{ github.event.head_commit.message }} 85 | artifacts: dist/*.whl,dist/*.tar.gz 86 | token: ${{ secrets.GITHUB_TOKEN }} 87 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '26 10 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v3 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v3 73 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Update documentation 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | branches: 8 | - master 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Python 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: 3.9 20 | - name: Install dependencies 21 | shell: bash 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install hatch 25 | - name: Build 26 | run: hatch run mkdocs build 27 | - name: Parse tag 28 | id: version_tag 29 | uses: battila7/get-version-action@v2 30 | - name: Deploy 31 | env: 32 | DOC_TAG: ${{ steps.version_tag.outputs.major && steps.version_tag.outputs.minor && format('{0}.{1} latest', steps.version_tag.outputs.major, steps.version_tag.outputs.minor) || 'dev' }} 33 | run: | 34 | git config user.name fastapi-users-ci 35 | git config user.email fastapi-users-ci@francoisvoron.com 36 | git fetch origin gh-pages --depth=1 37 | hatch run mike deploy --push --update-aliases ${{ env.DOC_TAG }} 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | junit/ 50 | junit.xml 51 | test*.db* 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # dotenv 87 | .env 88 | 89 | # virtualenv 90 | .venv 91 | venv/ 92 | ENV/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | 107 | # OS files 108 | .DS_Store 109 | 110 | # .idea 111 | .idea/ 112 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.analysis.typeCheckingMode": "basic", 3 | "python.analysis.autoImportCompletions": true, 4 | "python.terminal.activateEnvironment": true, 5 | "python.terminal.activateEnvInCurrentTerminal": true, 6 | "python.testing.unittestEnabled": false, 7 | "python.testing.pytestEnabled": true, 8 | "editor.rulers": [88], 9 | "python.defaultInterpreterPath": "${workspaceFolder}/.hatch/fastapi-users/bin/python", 10 | "python.testing.pytestPath": "${workspaceFolder}/.hatch/fastapi-users/bin/pytest", 11 | "python.testing.cwd": "${workspaceFolder}", 12 | "python.testing.pytestArgs": ["--no-cov"], 13 | "[python]": { 14 | "editor.formatOnSave": true, 15 | "editor.codeActionsOnSave": { 16 | "source.fixAll": "explicit", 17 | "source.organizeImports": "explicit" 18 | }, 19 | "editor.defaultFormatter": "charliermarsh.ruff" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 François Voron 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs-overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block outdated %} 4 | You're not viewing the latest version. 5 | 6 | Click here to go to latest. 7 | 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /docs/configuration/authentication/backend.md: -------------------------------------------------------------------------------- 1 | # Create a backend 2 | 3 | As we said, a backend is the combination of a transport and a strategy. That way, you can create a complete strategy exactly fitting your needs. 4 | 5 | For this, you have to use the `AuthenticationBackend` class. 6 | 7 | ```py 8 | from fastapi_users.authentication import AuthenticationBackend, BearerTransport, JWTStrategy 9 | 10 | SECRET = "SECRET" 11 | 12 | bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") 13 | 14 | def get_jwt_strategy() -> JWTStrategy: 15 | return JWTStrategy(secret=SECRET, lifetime_seconds=3600) 16 | 17 | auth_backend = AuthenticationBackend( 18 | name="jwt", 19 | transport=bearer_transport, 20 | get_strategy=get_jwt_strategy, 21 | ) 22 | ``` 23 | 24 | As you can see, instantiation is quite simple. It accepts the following arguments: 25 | 26 | * `name` (`str`): Name of the backend. Each backend should have a unique name. 27 | * `transport` (`Transport`): An instance of a `Transport` class. 28 | * `get_strategy` (`Callable[..., Strategy]`): A dependency callable returning an instance of a `Strategy` class. 29 | 30 | ## Next steps 31 | 32 | You can have as many authentication backends as you wish. You'll then have to pass those backends to your `FastAPIUsers` instance and generate an auth router for each one of them. 33 | -------------------------------------------------------------------------------- /docs/configuration/authentication/index.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | 3 | **FastAPI Users** allows you to plug in several authentication methods. 4 | 5 | ## How it works? 6 | 7 | You can have **several** authentication methods, e.g. a cookie authentication for browser-based queries and a JWT token authentication for pure API queries. 8 | 9 | When checking authentication, each method is run one after the other. The first method yielding a user wins. If no method yields a user, an `HTTPException` is raised. 10 | 11 | For each backend, you'll be able to add a router with the corresponding `/login` and `/logout`. More on this in the [routers documentation](../routers/index.md). 12 | 13 | ## Transport + Strategy = Authentication backend 14 | 15 | An authentication backend is composed of two parts: 16 | 17 | ### Transport 18 | 19 | It manages how the token will be carried over the request. We currently provide two methods: 20 | 21 | #### [Bearer](transports/bearer.md) 22 | 23 | The token will be sent through an `Authorization: Bearer` header. 24 | 25 | !!! tip "Pros and cons" 26 | 27 | * ✅ Easy to read and set in every requests. 28 | * ❌ Needs to be stored manually somewhere in the client. 29 | 30 | ➡️ Use it if you want to implement a mobile application or a pure REST API. 31 | 32 | #### [Cookie](transports/cookie.md) 33 | 34 | The token will be sent through a cookie. 35 | 36 | !!! tip "Pros and cons" 37 | 38 | * ✅ Automatically stored and sent securely by web browsers in every requests. 39 | * ✅ Automatically removed at expiration by web browsers. 40 | * ❌ Needs a CSRF protection for maximum security. 41 | * ❌ Harder to work with outside a browser, like a mobile app or a server. 42 | 43 | ➡️ Use it if you want to implement a web frontend. 44 | 45 | ### Strategy 46 | 47 | It manages how the token is generated and secured. We currently provide three methods: 48 | 49 | #### [JWT](strategies/jwt.md) 50 | 51 | The token is self-contained in a JSON Web Token. 52 | 53 | !!! tip "Pros and cons" 54 | 55 | * ✅ Self-contained: it doesn't need to be stored in a database. 56 | * ❌ Can't be invalidated on the server-side: it's valid until it expires. 57 | 58 | ➡️ Use it if you want to get up-and-running quickly. 59 | 60 | #### [Database](strategies/database.md) 61 | 62 | The token is stored in a table (or collection) in your database. 63 | 64 | !!! tip "Pros and cons" 65 | 66 | * ✅ Secure and performant. 67 | * ✅ Tokens can be invalidated server-side by removing them from the database. 68 | * ✅ Highly customizable: add your own fields, create an API to retrieve the active sessions of your users, etc. 69 | * ❌ Configuration is a bit more complex. 70 | 71 | ➡️ Use it if you want maximum flexibility in your token management. 72 | 73 | #### [Redis](strategies/redis.md) 74 | 75 | The token is stored in a Redis key-store. 76 | 77 | !!! tip "Pros and cons" 78 | 79 | * ✅ Secure and performant. 80 | * ✅ Tokens can be invalidated server-side by removing them from Redis. 81 | * ❌ A Redis server is needed. 82 | 83 | ➡️ Use it if you want maximum performance while being able to invalidate tokens. 84 | -------------------------------------------------------------------------------- /docs/configuration/authentication/strategies/database.md: -------------------------------------------------------------------------------- 1 | # Database 2 | 3 | The most natural way for storing tokens is of course the very same database you're using for your application. In this strategy, we set up a table (or collection) for storing those tokens with the associated user id. On each request, we try to retrieve this token from the database to get the corresponding user id. 4 | 5 | ## Configuration 6 | 7 | The configuration of this strategy is a bit more complex than the others as it requires you to configure models and a database adapter, [exactly like we did for users](../../overview.md#user-model-and-database-adapters). 8 | 9 | 10 | ### Database adapters 11 | 12 | An access token will be structured like this in your database: 13 | 14 | * `token` (`str`) – Unique identifier of the token. It's generated automatically upon login by the strategy. 15 | * `user_id` (`ID`) – User id. of the user associated to this token. 16 | * `created_at` (`datetime`) – Date and time of creation of the token. It's used to determine if the token is expired or not. 17 | 18 | We are providing a base model with those fields for each database we are supporting. 19 | 20 | #### SQLAlchemy 21 | 22 | We'll expand from the basic SQLAlchemy configuration. 23 | 24 | ```py hl_lines="5-8 23-24 45-48" 25 | --8<-- "docs/src/db_sqlalchemy_access_tokens.py" 26 | ``` 27 | 28 | 1. We define an `AccessToken` ORM model inheriting from `SQLAlchemyBaseAccessTokenTableUUID`. 29 | 30 | 2. We define a dependency to instantiate the `SQLAlchemyAccessTokenDatabase` class. Just like the user database adapter, it expects a fresh SQLAlchemy session and the `AccessToken` model class we defined above. 31 | 32 | !!! tip "`user_id` foreign key is defined as UUID" 33 | By default, we use UUID as a primary key ID for your user, so we follow the same convention to define the foreign key pointing to the user. 34 | 35 | If you want to use another type, like an auto-incremented integer, you can use `SQLAlchemyBaseAccessTokenTable` as base class and define your own `user_id` column. 36 | 37 | ```py 38 | class AccessToken(SQLAlchemyBaseAccessTokenTable[int], Base): 39 | @declared_attr 40 | def user_id(cls) -> Mapped[int]: 41 | return mapped_column(Integer, ForeignKey("user.id", ondelete="cascade"), nullable=False) 42 | ``` 43 | 44 | Notice that `SQLAlchemyBaseAccessTokenTable` expects a generic type to define the actual type of ID you use. 45 | 46 | #### Beanie 47 | 48 | We'll expand from the basic Beanie configuration. 49 | 50 | ```py hl_lines="4-7 20-21 28-29" 51 | --8<-- "docs/src/db_beanie_access_tokens.py" 52 | ``` 53 | 54 | 1. We define an `AccessToken` ODM model inheriting from `BeanieBaseAccessToken`. Notice that we set a generic type to define the type of the `user_id` reference. By default, it's a standard MongoDB ObjectID. 55 | 56 | 2. We define a dependency to instantiate the `BeanieAccessTokenDatabase` class. Just like the user database adapter, it expects the `AccessToken` model class we defined above. 57 | 58 | Don't forget to add the `AccessToken` ODM model to the `document_models` array in your Beanie initialization, [just like you did with the `User` model](../../databases/beanie.md#initialize-beanie)! 59 | 60 | !!! info 61 | If you want to add your own custom settings to your `AccessToken` document model - like changing the collection name - don't forget to let your inner `Settings` class inherit the pre-defined settings from `BeanieBaseAccessToken` like this: `Settings(BeanieBaseAccessToken.Settings): # ...`! See Beanie's [documentation on `Settings`](https://beanie-odm.dev/tutorial/defining-a-document/#settings) for details. 62 | 63 | ### Strategy 64 | 65 | ```py 66 | import uuid 67 | 68 | from fastapi import Depends 69 | from fastapi_users.authentication.strategy.db import AccessTokenDatabase, DatabaseStrategy 70 | 71 | from .db import AccessToken, User 72 | 73 | 74 | def get_database_strategy( 75 | access_token_db: AccessTokenDatabase[AccessToken] = Depends(get_access_token_db), 76 | ) -> DatabaseStrategy: 77 | return DatabaseStrategy(access_token_db, lifetime_seconds=3600) 78 | ``` 79 | 80 | As you can see, instantiation is quite simple. It accepts the following arguments: 81 | 82 | * `database` (`AccessTokenDatabase`): A database adapter instance for `AccessToken` table, like we defined above. 83 | * `lifetime_seconds` (`int`): The lifetime of the token in seconds. 84 | 85 | !!! tip "Why it's inside a function?" 86 | To allow strategies to be instantiated dynamically with other dependencies, they have to be provided as a callable to the authentication backend. 87 | 88 | As you can see here, this pattern allows us to dynamically inject a connection to the database. 89 | 90 | ## Logout 91 | 92 | On logout, this strategy will delete the token from the database. 93 | -------------------------------------------------------------------------------- /docs/configuration/authentication/strategies/jwt.md: -------------------------------------------------------------------------------- 1 | # JWT 2 | 3 | [JSON Web Token (JWT)](https://jwt.io/introduction) is an internet standard for creating access tokens based on JSON. They don't need to be stored in a database: the data is self-contained inside and cryptographically signed. 4 | 5 | ## Configuration 6 | 7 | ```py 8 | from fastapi_users.authentication import JWTStrategy 9 | 10 | SECRET = "SECRET" 11 | 12 | def get_jwt_strategy() -> JWTStrategy: 13 | return JWTStrategy(secret=SECRET, lifetime_seconds=3600) 14 | ``` 15 | 16 | As you can see, instantiation is quite simple. It accepts the following arguments: 17 | 18 | - `secret` (`Union[str, pydantic.SecretStr]`): A constant secret which is used to encode the token. **Use a strong passphrase and keep it secure.** 19 | - `lifetime_seconds` (`Optional[int]`): The lifetime of the token in seconds. Can be set to `None` but in this case the token will be valid **forever**; which may raise serious security concerns. 20 | - `token_audience` (`Optional[List[str]]`): A list of valid audiences for the JWT token. Defaults to `["fastapi-users:auth"]`. 21 | - `algorithm` (`Optional[str]`): The JWT encryption algorithm. See [RFC 7519, section 8](https://datatracker.ietf.org/doc/html/rfc7519#section-8). Defaults to `"HS256"`. 22 | - `public_key` (`Optional[Union[str, pydantic.SecretStr]]`): If the JWT encryption algorithm requires a key pair instead of a simple secret, the key to **decrypt** the JWT may be provided here. The `secret` parameter will always be used to **encrypt** the JWT. 23 | 24 | !!! tip "Why it's inside a function?" 25 | To allow strategies to be instantiated dynamically with other dependencies, they have to be provided as a callable to the authentication backend. 26 | 27 | For `JWTStrategy`, since it doesn't require dependencies, it can be as simple as the function above. 28 | 29 | ## RS256 example 30 | 31 | ```py 32 | from fastapi_users.authentication import JWTStrategy 33 | 34 | PUBLIC_KEY = """-----BEGIN PUBLIC KEY----- 35 | # Your RSA public key in PEM format goes here 36 | -----END PUBLIC KEY-----""" 37 | 38 | PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY----- 39 | # Your RSA private key in PEM format goes here 40 | -----END RSA PRIVATE KEY-----""" 41 | 42 | def get_jwt_strategy() -> JWTStrategy: 43 | return JWTStrategy( 44 | secret=PRIVATE_KEY, 45 | lifetime_seconds=3600, 46 | algorithm="RS256", 47 | public_key=PUBLIC_KEY, 48 | ) 49 | ``` 50 | 51 | ## Logout 52 | 53 | On logout, this strategy **won't do anything**. Indeed, a JWT can't be invalidated on the server-side: it's valid until it expires. 54 | -------------------------------------------------------------------------------- /docs/configuration/authentication/strategies/redis.md: -------------------------------------------------------------------------------- 1 | # Redis 2 | 3 | [Redis](https://redis.io/) is an ultra-fast key-store database. As such, it's a good candidate for token management. In this strategy, a token is generated and associated with the user id. in the database. On each request, we try to retrieve this token from Redis to get the corresponding user id. 4 | 5 | ## Installation 6 | 7 | You should install the library with the optional dependencies for Redis: 8 | 9 | ```sh 10 | pip install 'fastapi-users[redis]' 11 | ``` 12 | 13 | ## Configuration 14 | 15 | ```py 16 | import redis.asyncio 17 | from fastapi_users.authentication import RedisStrategy 18 | 19 | redis = redis.asyncio.from_url("redis://localhost:6379", decode_responses=True) 20 | 21 | def get_redis_strategy() -> RedisStrategy: 22 | return RedisStrategy(redis, lifetime_seconds=3600) 23 | ``` 24 | 25 | As you can see, instantiation is quite simple. It accepts the following arguments: 26 | 27 | * `redis` (`redis.asyncio.Redis`): An instance of `redis.asyncio.Redis`. Note that the `decode_responses` flag set to `True` is necessary. 28 | * `lifetime_seconds` (`Optional[int]`): The lifetime of the token in seconds. Defaults to `None`, which means the token doesn't expire. 29 | * `key_prefix` (`str`): The prefix used to set the key in the Redis stored. Defaults to `fastapi_users_token:`. 30 | 31 | !!! tip "Why it's inside a function?" 32 | To allow strategies to be instantiated dynamically with other dependencies, they have to be provided as a callable to the authentication backend. 33 | 34 | ## Logout 35 | 36 | On logout, this strategy will delete the token from the Redis store. 37 | -------------------------------------------------------------------------------- /docs/configuration/authentication/transports/bearer.md: -------------------------------------------------------------------------------- 1 | # Bearer 2 | 3 | With this transport, the token is expected inside the `Authorization` header of the HTTP request with the `Bearer` scheme. It's particularly suited for pure API interaction or mobile apps. 4 | 5 | ## Configuration 6 | 7 | ```py 8 | from fastapi_users.authentication import BearerTransport 9 | 10 | bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") 11 | ``` 12 | 13 | As you can see, instantiation is quite simple. It accepts the following arguments: 14 | 15 | * `tokenUrl` (`str`): The exact path of your login endpoint. It'll allow the interactive documentation to automatically discover it and get a working *Authorize* button. In most cases, you'll probably need a **relative** path, not absolute. You can read more details about this in the [FastAPI documentation](https://fastapi.tiangolo.com/tutorial/security/first-steps/#fastapis-oauth2passwordbearer). 16 | 17 | ## Login 18 | 19 | This method will return the in the following form upon successful login: 20 | 21 | !!! success "`200 OK`" 22 | ```json 23 | { 24 | "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiOTIyMWZmYzktNjQwZi00MzcyLTg2ZDMtY2U2NDJjYmE1NjAzIiwiYXVkIjoiZmFzdGFwaS11c2VyczphdXRoIiwiZXhwIjoxNTcxNTA0MTkzfQ.M10bjOe45I5Ncu_uXvOmVV8QxnL-nZfcH96U90JaocI", 25 | "token_type": "bearer" 26 | } 27 | ``` 28 | 29 | > Check documentation about [login route](../../../usage/routes.md#post-login). 30 | 31 | ## Logout 32 | 33 | !!! success "`204 No content`" 34 | 35 | ## Authentication 36 | 37 | This method expects that you provide a `Bearer` authentication with a valid token corresponding to your strategy. 38 | 39 | ```bash 40 | curl http://localhost:9000/protected-route -H'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiOTIyMWZmYzktNjQwZi00MzcyLTg2ZDMtY2U2NDJjYmE1NjAzIiwiYXVkIjoiZmFzdGFwaS11c2VyczphdXRoIiwiZXhwIjoxNTcxNTA0MTkzfQ.M10bjOe45I5Ncu_uXvOmVV8QxnL-nZfcH96U90JaocI' 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/configuration/authentication/transports/cookie.md: -------------------------------------------------------------------------------- 1 | # Cookie 2 | 3 | Cookies are an easy way to store stateful information into the user browser. Thus, it is more useful for browser-based navigation (e.g. a front-end app making API requests) rather than pure API interaction. 4 | 5 | ## Configuration 6 | 7 | ```py 8 | from fastapi_users.authentication import CookieTransport 9 | 10 | cookie_transport = CookieTransport(cookie_max_age=3600) 11 | ``` 12 | 13 | As you can see, instantiation is quite simple. It accepts the following arguments: 14 | 15 | * `cookie_name` (`fastapiusersauth`): Name of the cookie. 16 | * `cookie_max_age` (`Optional[int]`): The lifetime of the cookie in seconds. `None` by default, which means it's a session cookie. 17 | * `cookie_path` (`/`): Cookie path. 18 | * `cookie_domain` (`None`): Cookie domain. 19 | * `cookie_secure` (`True`): Whether to only send the cookie to the server via SSL request. 20 | * `cookie_httponly` (`True`): Whether to prevent access to the cookie via JavaScript. 21 | * `cookie_samesite` (`lax`): A string that specifies the samesite strategy for the cookie. Valid values are `lax`, `strict` and `none`. Defaults to `lax`. 22 | 23 | ## Login 24 | 25 | This method will return a response with a valid `set-cookie` header upon successful login: 26 | 27 | !!! success "`204 No content`" 28 | 29 | > Check documentation about [login route](../../../usage/routes.md#post-login). 30 | 31 | ## Logout 32 | 33 | This method will remove the authentication cookie: 34 | 35 | !!! success "`204 No content`" 36 | 37 | > Check documentation about [logout route](../../../usage/routes.md#post-logout). 38 | 39 | ## Authentication 40 | 41 | This method expects that you provide a valid cookie in the headers. 42 | -------------------------------------------------------------------------------- /docs/configuration/databases/beanie.md: -------------------------------------------------------------------------------- 1 | # Beanie 2 | 3 | **FastAPI Users** provides the necessary tools to work with MongoDB databases using the [Beanie ODM](https://github.com/roman-right/beanie). 4 | 5 | ## Setup database connection and collection 6 | 7 | The first thing to do is to create a MongoDB connection using [mongodb/motor](https://github.com/mongodb/motor) (automatically installed with Beanie). 8 | 9 | ```py hl_lines="5-9" 10 | --8<-- "docs/src/db_beanie.py" 11 | ``` 12 | 13 | You can choose any name for the database. 14 | 15 | ## Create the User model 16 | 17 | As for any Beanie ODM model, we'll create a `User` model. 18 | 19 | ```py hl_lines="12-13" 20 | --8<-- "docs/src/db_beanie.py" 21 | ``` 22 | 23 | As you can see, **FastAPI Users** provides a base class that will include base fields for our `User` table. You can of course add you own fields there to fit to your needs! 24 | 25 | !!! info 26 | The base class is configured to automatically create a [unique index](https://roman-right.github.io/beanie/tutorial/defining-a-document/#indexes) on `id` and `email`. 27 | 28 | !!! info 29 | If you want to add your own custom settings to your `User` document model - like changing the collection name - don't forget to let your inner `Settings` class inherit the pre-defined settings from `BeanieBaseUser` like this: `class Settings(BeanieBaseUser.Settings): # ...`! See Beanie's [documentation on `Settings`](https://beanie-odm.dev/tutorial/defining-a-document/#settings) for details. 30 | 31 | ## Create the database adapter 32 | 33 | The database adapter of **FastAPI Users** makes the link between your database configuration and the users logic. It should be generated by a FastAPI dependency. 34 | 35 | ```py hl_lines="16-17" 36 | --8<-- "docs/src/db_beanie.py" 37 | ``` 38 | 39 | Notice that we pass a reference to the `User` model we defined above. 40 | 41 | ## Initialize Beanie 42 | 43 | When initializing your FastAPI app, it's important that you [**initialize Beanie**](https://roman-right.github.io/beanie/tutorial/initialization/) so it can discover your models. We can achieve this using [**Lifespan Events**](https://fastapi.tiangolo.com/advanced/events/) on the FastAPI app: 44 | 45 | ```py 46 | from contextlib import asynccontextmanager 47 | from beanie import init_beanie 48 | 49 | 50 | @asynccontextmanager 51 | async def lifespan(app: FastAPI): 52 | await init_beanie( 53 | database=db, # (1)! 54 | document_models=[ 55 | User, # (2)! 56 | ], 57 | ) 58 | yield 59 | 60 | app = FastAPI(lifespan=lifespan) 61 | ``` 62 | 63 | 1. This is the `db` Motor database instance we defined above. 64 | 65 | 2. This is the Beanie `User` model we defined above. Don't forget to also add your very own models! 66 | -------------------------------------------------------------------------------- /docs/configuration/databases/sqlalchemy.md: -------------------------------------------------------------------------------- 1 | # SQLAlchemy 2 | 3 | **FastAPI Users** provides the necessary tools to work with SQL databases thanks to [SQLAlchemy ORM with asyncio](https://docs.sqlalchemy.org/en/14/orm/extensions/asyncio.html). 4 | 5 | ## Asynchronous driver 6 | 7 | To work with your DBMS, you'll need to install the corresponding asyncio driver. The common choices are: 8 | 9 | * For PostgreSQL: `pip install asyncpg` 10 | * For SQLite: `pip install aiosqlite` 11 | 12 | Examples of `DB_URL`s are: 13 | 14 | * PostgreSQL: `postgresql+asyncpg://user:password@host:port/name` 15 | * SQLite: `sqlite+aiosqlite:///name.db` 16 | 17 | For the sake of this tutorial from now on, we'll use a simple SQLite database. 18 | 19 | !!! warning 20 | When using asynchronous sessions, ensure `Session.expire_on_commit` is set to `False` as recommended by the [SQLAlchemy docs on asyncio](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#asyncio-orm-avoid-lazyloads). The examples on this documentation already have this setting correctly defined to `False` when using the `async_sessionmaker` factory. 21 | 22 | ## Create the User model 23 | 24 | As for any SQLAlchemy ORM model, we'll create a `User` model. 25 | 26 | ```py hl_lines="15-16" 27 | --8<-- "docs/src/db_sqlalchemy.py" 28 | ``` 29 | 30 | As you can see, **FastAPI Users** provides a base class that will include base fields for our `User` table. You can of course add you own fields there to fit to your needs! 31 | 32 | !!! tip "Primary key is defined as UUID" 33 | By default, we use UUID as a primary key ID for your user. If you want to use another type, like an auto-incremented integer, you can use `SQLAlchemyBaseUserTable` as base class and define your own `id` column. 34 | 35 | ```py 36 | class User(SQLAlchemyBaseUserTable[int], Base): 37 | id: Mapped[int] = mapped_column(Integer, primary_key=True) 38 | ``` 39 | 40 | Notice that `SQLAlchemyBaseUserTable` expects a generic type to define the actual type of ID you use. 41 | 42 | ## Implement a function to create the tables 43 | 44 | We'll now create an utility function to create all the defined tables. 45 | 46 | ```py hl_lines="23-25" 47 | --8<-- "docs/src/db_sqlalchemy.py" 48 | ``` 49 | 50 | This function can be called, for example, during the initialization of your FastAPI app. 51 | 52 | !!! warning 53 | In production, it's strongly recommended to setup a migration system to update your SQL schemas. See [Alembic](https://alembic.sqlalchemy.org/en/latest/). 54 | 55 | ## Create the database adapter dependency 56 | 57 | The database adapter of **FastAPI Users** makes the link between your database configuration and the users logic. It should be generated by a FastAPI dependency. 58 | 59 | ```py hl_lines="28-34" 60 | --8<-- "docs/src/db_sqlalchemy.py" 61 | ``` 62 | 63 | Notice that we define first a `get_async_session` dependency returning us a fresh SQLAlchemy session to interact with the database. 64 | 65 | It's then used inside the `get_user_db` dependency to generate our adapter. Notice that we pass it two things: 66 | 67 | * The `session` instance we just injected. 68 | * The `User` class, which is the actual SQLAlchemy model. 69 | -------------------------------------------------------------------------------- /docs/configuration/full-example.md: -------------------------------------------------------------------------------- 1 | # Full example 2 | 3 | Here is a full working example with JWT authentication to help get you started. 4 | 5 | !!! warning 6 | Notice that **SECRET** should be changed to a strong passphrase. 7 | Insecure passwords may give attackers full access to your database. 8 | 9 | ## SQLAlchemy 10 | 11 | [Open :material-open-in-new:](https://github.com/fastapi-users/fastapi-users/tree/master/examples/sqlalchemy) 12 | 13 | === "requirements.txt" 14 | 15 | ``` 16 | --8<-- "examples/sqlalchemy/requirements.txt" 17 | ``` 18 | 19 | === "main.py" 20 | 21 | ```py 22 | --8<-- "examples/sqlalchemy/main.py" 23 | ``` 24 | 25 | === "app/app.py" 26 | 27 | ```py 28 | --8<-- "examples/sqlalchemy/app/app.py" 29 | ``` 30 | 31 | === "app/db.py" 32 | 33 | ```py 34 | --8<-- "examples/sqlalchemy/app/db.py" 35 | ``` 36 | 37 | === "app/schemas.py" 38 | 39 | ```py 40 | --8<-- "examples/sqlalchemy/app/schemas.py" 41 | ``` 42 | 43 | === "app/users.py" 44 | 45 | ```py 46 | --8<-- "examples/sqlalchemy/app/users.py" 47 | ``` 48 | 49 | ## Beanie 50 | 51 | [Open :material-open-in-new:](https://github.com/fastapi-users/fastapi-users/tree/master/examples/beanie) 52 | 53 | === "requirements.txt" 54 | 55 | ``` 56 | --8<-- "examples/beanie/requirements.txt" 57 | ``` 58 | 59 | === "main.py" 60 | 61 | ```py 62 | --8<-- "examples/beanie/main.py" 63 | ``` 64 | 65 | === "app/app.py" 66 | 67 | ```py 68 | --8<-- "examples/beanie/app/app.py" 69 | ``` 70 | 71 | === "app/db.py" 72 | 73 | ```py 74 | --8<-- "examples/beanie/app/db.py" 75 | ``` 76 | 77 | === "app/schemas.py" 78 | 79 | ```py 80 | --8<-- "examples/beanie/app/schemas.py" 81 | ``` 82 | 83 | === "app/users.py" 84 | 85 | ```py 86 | --8<-- "examples/beanie/app/users.py" 87 | ``` 88 | 89 | ## What now? 90 | 91 | You're ready to go! Be sure to check the [Usage](../usage/routes.md) section to understand how to work with **FastAPI Users**. 92 | -------------------------------------------------------------------------------- /docs/configuration/overview.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | The schema below shows you how the library is structured and how each part fit together. 4 | 5 | 6 | ```mermaid 7 | flowchart TB 8 | FASTAPI_USERS{FastAPIUsers} 9 | USER_MANAGER{UserManager} 10 | USER_MODEL{User model} 11 | DATABASE_DEPENDENCY[[get_user_db]] 12 | USER_MANAGER_DEPENDENCY[[get_user_manager]] 13 | CURRENT_USER[[current_user]] 14 | subgraph SCHEMAS[Schemas] 15 | USER[User] 16 | USER_CREATE[UserCreate] 17 | USER_UPDATE[UserUpdate] 18 | end 19 | subgraph DATABASE[Database adapters] 20 | SQLALCHEMY[SQLAlchemy] 21 | BEANIE[Beanie] 22 | end 23 | subgraph ROUTERS[Routers] 24 | AUTH[[get_auth_router]] 25 | OAUTH[[get_oauth_router]] 26 | OAUTH_ASSOCIATE[[get_oauth_associate_router]] 27 | REGISTER[[get_register_router]] 28 | VERIFY[[get_verify_router]] 29 | RESET[[get_reset_password_router]] 30 | USERS[[get_users_router]] 31 | end 32 | subgraph AUTH_BACKENDS[Authentication] 33 | subgraph TRANSPORTS[Transports] 34 | COOKIE[CookieTransport] 35 | BEARER[BearerTransport] 36 | end 37 | subgraph STRATEGIES[Strategies] 38 | DB[DatabaseStrategy] 39 | JWT[JWTStrategy] 40 | REDIS[RedisStrategy] 41 | end 42 | AUTH_BACKEND{AuthenticationBackend} 43 | end 44 | DATABASE --> DATABASE_DEPENDENCY 45 | USER_MODEL --> DATABASE_DEPENDENCY 46 | DATABASE_DEPENDENCY --> USER_MANAGER 47 | 48 | USER_MANAGER --> USER_MANAGER_DEPENDENCY 49 | USER_MANAGER_DEPENDENCY --> FASTAPI_USERS 50 | 51 | FASTAPI_USERS --> ROUTERS 52 | 53 | TRANSPORTS --> AUTH_BACKEND 54 | STRATEGIES --> AUTH_BACKEND 55 | 56 | AUTH_BACKEND --> ROUTERS 57 | AUTH_BACKEND --> FASTAPI_USERS 58 | 59 | FASTAPI_USERS --> CURRENT_USER 60 | 61 | SCHEMAS --> ROUTERS 62 | ``` 63 | 64 | ## User model and database adapters 65 | 66 | FastAPI Users is compatible with various **databases and ORM**. To build the interface between those database tools and the library, we provide database adapters classes that you need to instantiate and configure. 67 | 68 | ➡️ [I'm using SQLAlchemy](databases/sqlalchemy.md) 69 | 70 | ➡️ [I'm using Beanie](databases/beanie.md) 71 | 72 | ## Authentication backends 73 | 74 | Authentication backends define the way users sessions are managed in your app, like access tokens or cookies. 75 | 76 | They are composed of two parts: a **transport**, which is how the token will be carried over the requests (e.g. cookies, headers...) and a **strategy**, which is how the token will be generated and secured (e.g. a JWT, a token in database...). 77 | 78 | ➡️ [Configure the authentication backends](./authentication/index.md) 79 | 80 | ## `UserManager` 81 | 82 | The `UserManager` object bears most of the logic of FastAPI Users: registration, verification, password reset... We provide a `BaseUserManager` with this common logic; which you should overload to define how to validate passwords or handle events. 83 | 84 | This `UserManager` object should be provided through a FastAPI dependency, `get_user_manager`. 85 | 86 | ➡️ [Configure `UserManager`](./user-manager.md) 87 | 88 | ## Schemas 89 | 90 | FastAPI is heavily using [Pydantic models](https://pydantic-docs.helpmanual.io/) to validate request payloads and serialize responses. **FastAPI Users** is no exception and will expect you to provide Pydantic schemas representing a user when it's read, created and updated. 91 | 92 | ➡️ [Configure schemas](./schemas.md) 93 | 94 | ## `FastAPIUsers` and routers 95 | 96 | Finally, `FastAPIUsers` object is the main class from which you'll be able to generate routers for classic routes like registration or login, but also get the `current_user` dependency factory to inject the authenticated user in your own routes. 97 | 98 | ➡️ [Configure `FastAPIUsers` and routers](./routers/index.md) 99 | -------------------------------------------------------------------------------- /docs/configuration/password-hash.md: -------------------------------------------------------------------------------- 1 | # Password hash 2 | 3 | By default, FastAPI Users will use the [Argon2 algorithm](https://en.wikipedia.org/wiki/Argon2) to **hash and salt** passwords before storing them in the database, with backwards-compatibility with [Bcrypt](https://en.wikipedia.org/wiki/Bcrypt). 4 | 5 | The implementation is provided by [pwdlib](https://github.com/frankie567/pwdlib), a modern password hashing wrapper. 6 | 7 | ## Customize `PasswordHash` 8 | 9 | If you need to tune the algorithms used or their settings, you can customize the [`PasswordHash` object of pwdlib](https://frankie567.github.io/pwdlib/reference/pwdlib/#pwdlib.PasswordHash). 10 | 11 | For this, you'll need to instantiate the `PasswordHelper` class and pass it your `PasswordHash`. The example below shows you how you can create a `PasswordHash` to only support the Argon2 algorithm. 12 | 13 | ```py 14 | from fastapi_users.password import PasswordHelper 15 | from pwdlib import PasswordHash, exceptions 16 | from pwdlib.hashers.argon2 import Argon2Hasher 17 | 18 | password_hash = PasswordHash(( 19 | Argon2Hasher(), 20 | )) 21 | password_helper = PasswordHelper(password_hash) 22 | ``` 23 | 24 | Finally, pass the `password_helper` variable while instantiating your `UserManager`: 25 | 26 | ```py 27 | async def get_user_manager(user_db=Depends(get_user_db)): 28 | yield UserManager(user_db, password_helper) 29 | ``` 30 | 31 | !!! info "Password hashes are automatically upgraded" 32 | FastAPI Users takes care of upgrading the password hash to a more recent algorithm when needed. 33 | 34 | Typically, when a user logs in, we'll check if the password hash algorithm is deprecated. 35 | 36 | If it is, we take the opportunity of having the password in plain-text at hand (since the user just logged in!) to hash it with a better algorithm and update it in database. 37 | 38 | ## Full customization 39 | 40 | If you don't wish to use `pwdlib` at all – **which we don't recommend unless you're absolutely sure of what you're doing** — you can implement your own `PasswordHelper` class as long as it implements the `PasswordHelperProtocol` and its methods. 41 | 42 | ```py 43 | from typing import Tuple 44 | 45 | from fastapi_users.password import PasswordHelperProtocol 46 | 47 | class PasswordHelper(PasswordHelperProtocol): 48 | def verify_and_update( 49 | self, plain_password: str, hashed_password: str 50 | ) -> Tuple[bool, str]: 51 | ... 52 | 53 | def hash(self, password: str) -> str: 54 | ... 55 | 56 | def generate(self) -> str: 57 | ... 58 | ``` 59 | -------------------------------------------------------------------------------- /docs/configuration/routers/auth.md: -------------------------------------------------------------------------------- 1 | # Auth router 2 | 3 | The auth router will generate `/login` and `/logout` routes for a given [authentication backend](../authentication/index.md). 4 | 5 | Check the [routes usage](../../usage/routes.md) to learn how to use them. 6 | 7 | ## Setup 8 | 9 | ```py 10 | import uuid 11 | 12 | from fastapi import FastAPI 13 | from fastapi_users import FastAPIUsers 14 | 15 | from .db import User 16 | 17 | fastapi_users = FastAPIUsers[User, uuid.UUID]( 18 | get_user_manager, 19 | [auth_backend], 20 | ) 21 | 22 | app = FastAPI() 23 | app.include_router( 24 | fastapi_users.get_auth_router(auth_backend), 25 | prefix="/auth/jwt", 26 | tags=["auth"], 27 | ) 28 | ``` 29 | 30 | ### Optional: user verification 31 | 32 | You can require the user to be **verified** (i.e. `is_verified` property set to `True`) to allow login. You have to set the `requires_verification` parameter to `True` on the router instantiation method: 33 | 34 | ```py 35 | app.include_router( 36 | fastapi_users.get_auth_router(auth_backend, requires_verification=True), 37 | prefix="/auth/jwt", 38 | tags=["auth"], 39 | ) 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/configuration/routers/index.md: -------------------------------------------------------------------------------- 1 | # Routers 2 | 3 | We're almost there! The last step is to configure the `FastAPIUsers` object that will wire the user manager, the authentication classes and let us generate the actual **API routes**. 4 | 5 | ## Configure `FastAPIUsers` 6 | 7 | Configure `FastAPIUsers` object with the elements we defined before. More precisely: 8 | 9 | * `get_user_manager`: Dependency callable getter to inject the 10 | user manager class instance. See [UserManager](../user-manager.md). 11 | * `auth_backends`: List of authentication backends. See [Authentication](../authentication/index.md). 12 | 13 | ```py 14 | import uuid 15 | 16 | from fastapi_users import FastAPIUsers 17 | 18 | from .db import User 19 | 20 | fastapi_users = FastAPIUsers[User, uuid.UUID]( 21 | get_user_manager, 22 | [auth_backend], 23 | ) 24 | ``` 25 | 26 | !!! note "Typing: User and ID generic types are expected" 27 | You can see that we define two generic types when instantiating: 28 | 29 | * `User`, which is the user model we defined in the database part 30 | * The ID, which should correspond to the type of ID you use on your model. Here, we chose UUID, but it can be anything, like an integer or a MongoDB ObjectID. 31 | 32 | It'll help you to have **good type-checking and auto-completion**. 33 | 34 | ## Available routers 35 | 36 | This helper class will let you generate useful routers to setup the authentication system. Each of them is **optional**, so you can pick only the one that you are interested in! Here are the routers provided: 37 | 38 | * [Auth router](./auth.md): Provides `/login` and `/logout` routes for a given [authentication backend](../authentication/index.md). 39 | * [Register router](./register.md): Provides `/register` routes to allow a user to create a new account. 40 | * [Reset password router](./reset.md): Provides `/forgot-password` and `/reset-password` routes to allow a user to reset its password. 41 | * [Verify router](./verify.md): Provides `/request-verify-token` and `/verify` routes to manage user e-mail verification. 42 | * [Users router](./users.md): Provides routes to manage users. 43 | * [OAuth router](../oauth.md): Provides routes to perform an OAuth authentication against a service provider (like Google or Facebook). 44 | 45 | You should check out each of them to understand how to use them. 46 | -------------------------------------------------------------------------------- /docs/configuration/routers/register.md: -------------------------------------------------------------------------------- 1 | # Register routes 2 | 3 | The register router will generate a `/register` route to allow a user to create a new account. 4 | 5 | Check the [routes usage](../../usage/routes.md) to learn how to use them. 6 | 7 | ## Setup 8 | 9 | ```py 10 | import uuid 11 | 12 | from fastapi import FastAPI 13 | from fastapi_users import FastAPIUsers 14 | 15 | from .db import User 16 | from .schemas import UserCreate, UserRead 17 | 18 | fastapi_users = FastAPIUsers[User, uuid.UUID]( 19 | get_user_manager, 20 | [auth_backend], 21 | ) 22 | 23 | app = FastAPI() 24 | app.include_router( 25 | fastapi_users.get_register_router(UserRead, UserCreate), 26 | prefix="/auth", 27 | tags=["auth"], 28 | ) 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/configuration/routers/reset.md: -------------------------------------------------------------------------------- 1 | # Reset password router 2 | 3 | The reset password router will generate `/forgot-password` (the user asks for a token to reset its password) and `/reset-password` (the user changes its password given the token) routes. 4 | 5 | Check the [routes usage](../../usage/routes.md) to learn how to use them. 6 | 7 | ## Setup 8 | 9 | ```py 10 | import uuid 11 | 12 | from fastapi import FastAPI 13 | from fastapi_users import FastAPIUsers 14 | 15 | from .db import User 16 | 17 | fastapi_users = FastAPIUsers[User, uuid.UUID]( 18 | get_user_manager, 19 | [auth_backend], 20 | ) 21 | 22 | app = FastAPI() 23 | app.include_router( 24 | fastapi_users.get_reset_password_router(), 25 | prefix="/auth", 26 | tags=["auth"], 27 | ) 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/configuration/routers/users.md: -------------------------------------------------------------------------------- 1 | # Users router 2 | 3 | This router provides routes to manage users. Check the [routes usage](../../usage/routes.md) to learn how to use them. 4 | 5 | ## Setup 6 | 7 | ```py 8 | import uuid 9 | 10 | from fastapi import FastAPI 11 | from fastapi_users import FastAPIUsers 12 | 13 | from .db import User 14 | from .schemas import UserRead, UserUpdate 15 | 16 | fastapi_users = FastAPIUsers[User, uuid.UUID]( 17 | get_user_manager, 18 | [auth_backend], 19 | ) 20 | 21 | app = FastAPI() 22 | app.include_router( 23 | fastapi_users.get_users_router(UserRead, UserUpdate), 24 | prefix="/users", 25 | tags=["users"], 26 | ) 27 | ``` 28 | 29 | ### Optional: user verification 30 | 31 | You can require the user to be **verified** (i.e. `is_verified` property set to `True`) to access those routes. You have to set the `requires_verification` parameter to `True` on the router instantiation method: 32 | 33 | ```py 34 | app.include_router( 35 | fastapi_users.get_users_router(UserRead, UserUpdate, requires_verification=True), 36 | prefix="/users", 37 | tags=["users"], 38 | ) 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/configuration/routers/verify.md: -------------------------------------------------------------------------------- 1 | # Verify router 2 | 3 | This router provides routes to manage user email verification. Check the [routes usage](../../usage/routes.md) to learn how to use them. 4 | 5 | !!! success "👏👏👏" 6 | A big thank you to [Edd Salkield](https://github.com/eddsalkield) and [Mark Todd](https://github.com/mark-todd) who worked hard on this feature! 7 | 8 | ## Setup 9 | 10 | ```py 11 | import uuid 12 | 13 | from fastapi import FastAPI 14 | from fastapi_users import FastAPIUsers 15 | 16 | from .db import User 17 | from .schemas import UserRead 18 | 19 | fastapi_users = FastAPIUsers[User, uuid.UUID]( 20 | get_user_manager, 21 | [auth_backend], 22 | ) 23 | 24 | app = FastAPI() 25 | app.include_router( 26 | fastapi_users.get_verify_router(UserRead), 27 | prefix="/auth", 28 | tags=["auth"], 29 | ) 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/configuration/schemas.md: -------------------------------------------------------------------------------- 1 | # Schemas 2 | 3 | FastAPI is heavily using [Pydantic models](https://pydantic-docs.helpmanual.io/) to validate request payloads and serialize responses. **FastAPI Users** is no exception and will expect you to provide Pydantic schemas representing a user when it's read, created and updated. 4 | 5 | It's **different from your `User` model**, which is an object that actually interacts with the database. Those schemas on the other hand are here to validate data and correctly serialize it in the API. 6 | 7 | **FastAPI Users** provides a base structure to cover its needs. It is structured like this: 8 | 9 | * `id` (`ID`) – Unique identifier of the user. It matches the type of your ID, like UUID or integer. 10 | * `email` (`str`) – Email of the user. Validated by [`email-validator`](https://github.com/JoshData/python-email-validator). 11 | * `is_active` (`bool`) – Whether or not the user is active. If not, login and forgot password requests will be denied. Defaults to `True`. 12 | * `is_verified` (`bool`) – Whether or not the user is verified. Optional but helpful with the [`verify` router](./routers/verify.md) logic. Defaults to `False`. 13 | * `is_superuser` (`bool`) – Whether or not the user is a superuser. Useful to implement administration logic. Defaults to `False`. 14 | 15 | ## Define your schemas 16 | 17 | There are four Pydantic models variations provided as mixins: 18 | 19 | * `BaseUser`, which provides the basic fields and validation; 20 | * `BaseCreateUser`, dedicated to user registration, which consists of compulsory `email` and `password` fields; 21 | * `BaseUpdateUser`, dedicated to user profile update, which adds an optional `password` field; 22 | 23 | You should define each of those variations, inheriting from each mixin: 24 | 25 | ```py 26 | import uuid 27 | 28 | from fastapi_users import schemas 29 | 30 | 31 | class UserRead(schemas.BaseUser[uuid.UUID]): 32 | pass 33 | 34 | 35 | class UserCreate(schemas.BaseUserCreate): 36 | pass 37 | 38 | 39 | class UserUpdate(schemas.BaseUserUpdate): 40 | pass 41 | ``` 42 | 43 | !!! note "Typing: ID generic type is expected" 44 | You can see that we define a generic type when extending the `BaseUser` class. It should correspond to the type of ID you use on your model. Here, we chose UUID, but it can be anything, like an integer or a MongoDB ObjectID. 45 | 46 | ### Adding your own fields 47 | 48 | You can of course add your own properties there to fit to your needs. In the example below, we add a required string property, `first_name`, and an optional date property, `birthdate`. 49 | 50 | ```py 51 | import datetime 52 | import uuid 53 | 54 | from fastapi_users import schemas 55 | 56 | 57 | class UserRead(schemas.BaseUser[uuid.UUID]): 58 | first_name: str 59 | birthdate: Optional[datetime.date] 60 | 61 | 62 | class UserCreate(schemas.BaseUserCreate): 63 | first_name: str 64 | birthdate: Optional[datetime.date] 65 | 66 | 67 | class UserUpdate(schemas.BaseUserUpdate): 68 | first_name: Optional[str] 69 | birthdate: Optional[datetime.date] 70 | ``` 71 | 72 | !!! warning "Make sure to mirror this in your database model" 73 | The `User` model you defined earlier for your specific database will be the central object that will actually store the data. Therefore, you need to define the very same fields in it so the data can be actually stored. 74 | -------------------------------------------------------------------------------- /docs/cookbook/create-user-programmatically.md: -------------------------------------------------------------------------------- 1 | # Create a user programmatically 2 | 3 | Sometimes, you'll need to create a user programmatically in the code rather than passing by the REST API endpoint. To do this, we'll create a function that you can call from your code. 4 | 5 | In this context, we are outside the dependency injection mechanism of FastAPI, so we have to take care of instantiating the [`UserManager` class](../configuration/user-manager.md) and all other dependent objects **manually**. 6 | 7 | For this cookbook, we'll consider you are starting from the [SQLAlchemy full example](../configuration/full-example.md), but it'll be rather similar for other DBMS. 8 | 9 | ## 1. Define dependencies as context managers 10 | 11 | Generally, FastAPI dependencies are defined as **generators**, using the `yield` keyword. FastAPI knows very well to handle them inside its dependency injection system. For example, here is the definition of the `get_user_manager` dependency: 12 | 13 | ```py 14 | async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)): 15 | yield UserManager(user_db) 16 | ``` 17 | 18 | In Python, when we want to use a generator, we have to use a `for` loop, which would be a bit unnatural in this context since we have only one value to get, the user manager instance. To avoid this, we'll transform them into **context managers**, so we can call them using the `with..as` syntax. Fortunately, the standard library provides tools to automatically transform generators into context managers. 19 | 20 | In the following sample, we import our dependencies and create a context manager version using `contextlib.asynccontextmanager`: 21 | 22 | ```py hl_lines="8-10" 23 | --8<-- "docs/src/cookbook_create_user_programmatically.py" 24 | ``` 25 | 26 | !!! info "I have other dependencies" 27 | Since FastAPI Users fully embraces dependency injection, you may have more arguments passed to your database or user manager dependencies. It's important then to not forget anyone. Once again, outside the dependency injection system, you are responsible of instantiating **everything** yourself. 28 | 29 | ## 2. Write a function 30 | 31 | We are now ready to write a function. The example below shows you a basic example but you can of course adapt it to your own needs. The key part here is once again to **take care of opening every context managers and pass them every required arguments**, as the dependency manager would do. 32 | 33 | ```py hl_lines="13-27" 34 | --8<-- "docs/src/cookbook_create_user_programmatically.py" 35 | ``` 36 | 37 | ## 3. Use it 38 | 39 | You can now easily use it in a script. For example: 40 | 41 | ```py 42 | import asyncio 43 | 44 | if __name__ == "__main__": 45 | asyncio.run(create_user("king.arthur@camelot.bt", "guinevere")) 46 | ``` 47 | -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastapi-users/fastapi-users/9d78b2a35dc7f35c2ffca67232c11f4d27a5db00/docs/favicon.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --8<-- "README.md" 2 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | You can add **FastAPI Users** to your FastAPI project in a few easy steps. First of all, install the dependency: 4 | 5 | ## With SQLAlchemy support 6 | 7 | ```sh 8 | pip install 'fastapi-users[sqlalchemy]' 9 | ``` 10 | 11 | 12 | ## With Beanie support 13 | 14 | ```sh 15 | pip install 'fastapi-users[beanie]' 16 | ``` 17 | 18 | ## With Redis authentication backend support 19 | 20 | Information on installing with proper database support can be found in the [Redis](configuration/authentication/strategies/redis.md) section. 21 | 22 | ## With OAuth2 support 23 | 24 | Information on installing with proper database support can be found in the [OAuth2](configuration/oauth.md) section. 25 | 26 | 27 | --- 28 | 29 | That's it! In the next section, we'll have an [overview](./configuration/overview.md) of how things work. 30 | -------------------------------------------------------------------------------- /docs/migration/08_to_1x.md: -------------------------------------------------------------------------------- 1 | # 0.8.x ➡️ 1.x.x 2 | 3 | 1.0 version introduces major breaking changes that need you to update some of your code and migrate your data. 4 | 5 | ## Id. are UUID 6 | 7 | Users and OAuth accounts id. are now represented as real UUID objects instead of plain strings. This change was introduced to leverage efficient storage and indexing for DBMS that supports UUID (especially PostgreSQL and Mongo). 8 | 9 | ### In Python code 10 | 11 | If you were doing comparison betwen a user id. and a string (in unit tests for example), you should now cast the id. to string: 12 | 13 | ```py 14 | # Before 15 | assert "d35d213e-f3d8-4f08-954a-7e0d1bea286f" == user.id 16 | 17 | # Now 18 | assert "d35d213e-f3d8-4f08-954a-7e0d1bea286f" == str(user.id) 19 | ``` 20 | 21 | If you were refering to user id. in your Pydantic models, the field should now be of `UUID4` type instead of `str`: 22 | 23 | ```py 24 | from pydantic import BaseModel, UUID4 25 | 26 | # Before 27 | class Model(BaseModel): 28 | user_id: str 29 | 30 | # After 31 | class Model(BaseModel): 32 | user_id: UUID4 33 | ``` 34 | 35 | #### MongoDB 36 | 37 | To avoid any issues, it's recommended to use the `standard` UUID representation when instantiating the MongoDB client: 38 | 39 | ```py 40 | DATABASE_URL = "mongodb://localhost:27017" 41 | client = motor.motor_asyncio.AsyncIOMotorClient( 42 | DATABASE_URL, uuidRepresentation="standard" 43 | ) 44 | ``` 45 | 46 | This parameter controls how the UUID values will be encoded in the database. By default, it's set to `pythonLegacy` but new applications should consider setting this to `standard` for cross language compatibility. [Read more about this](https://pymongo.readthedocs.io/en/stable/api/pymongo/mongo_client.html#pymongo.mongo_client.MongoClient). 47 | 48 | 49 | ### In database 50 | 51 | Id. were before stored as strings in the database. You should make a migration to convert string data to UUID data. 52 | 53 | !!! danger 54 | Scripts below are provided as guidelines. Please **review them carefully**, **adapt them** and check that they are working on a test database before applying them to production. **BE CAREFUL. THEY CAN DESTROY YOUR DATA.**. 55 | 56 | #### PostgreSQL 57 | 58 | PostgreSQL supports UUID type. If not already, you should enable the `uuid-ossp` extension: 59 | 60 | ```sql 61 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 62 | ``` 63 | 64 | To convert the existing id. string column, we can: 65 | 66 | 1. Create a new column with UUID type. 67 | 2. Fill it with the id. converted to UUID. 68 | 3. Drop the original id. column. 69 | 4. Make the new column a primary key and rename it. 70 | 71 | ```sql 72 | ALTER TABLE "user" ADD uuid_id UUID; 73 | UPDATE "user" SET uuid_id = uuid(id); 74 | ALTER TABLE "user" DROP id; 75 | ALTER TABLE "user" ADD PRIMARY KEY (uuid_id); 76 | ALTER TABLE "user" RENAME COLUMN uuid_id TO id; 77 | ``` 78 | 79 | #### MySQL 80 | 81 | MySQL doesn't support UUID type. We'll just convert the column to `CHAR(36)` type: 82 | 83 | ```sql 84 | ALTER TABLE "user" MODIFY id CHAR(36); 85 | ``` 86 | 87 | #### MongoDB 88 | 89 | ##### Mongo shell 90 | 91 | For MongoDB, we can use a `forEach` iterator to convert the id. for each document: 92 | 93 | ```js 94 | db.getCollection('users').find().forEach(function(user) { 95 | var uuid = UUID(user.id); 96 | db.getCollection('users').update({_id: user._id}, [{$set: {id: uuid}}]); 97 | }); 98 | ``` 99 | 100 | ##### Python 101 | 102 | ```py 103 | import uuid 104 | 105 | import motor.motor_asyncio 106 | 107 | 108 | async def migrate_uuid(): 109 | client = motor.motor_asyncio.AsyncIOMotorClient( 110 | DATABASE_URL, uuidRepresentation="standard" 111 | ) 112 | db = client["database_name"] 113 | users = db["users"] 114 | 115 | async for user in users.find({}): 116 | await users.update_one( 117 | {"_id": user["_id"]}, 118 | {"$set": {"id": uuid.UUID(user["id"])}}, 119 | ) 120 | ``` 121 | 122 | ## Splitted routers 123 | 124 | You now have the responsibility to **wire the routers**. FastAPI Users doesn't give a bloated users router anymore. 125 | 126 | **Event handlers** are also removed. You have to provide your "after-" logic as a parameter of the router generator. 127 | 128 | ### Before 129 | 130 | ```py 131 | jwt_authentication = JWTAuthentication(secret=SECRET, lifetime_seconds=3600) 132 | 133 | app = FastAPI() 134 | fastapi_users = FastAPIUsers( 135 | user_db, [jwt_authentication], User, UserCreate, UserUpdate, UserDB, 136 | ) 137 | app.include_router(fastapi_users.router, prefix="/users", tags=["users"]) 138 | 139 | 140 | @fastapi_users.on_after_register() 141 | def on_after_register(user: User, request: Request): 142 | print(f"User {user.id} has registered.") 143 | 144 | 145 | @fastapi_users.on_after_forgot_password() 146 | def on_after_forgot_password(user: User, token: str, request: Request): 147 | print(f"User {user.id} has forgot their password. Reset token: {token}") 148 | ``` 149 | 150 | ### After 151 | 152 | ```py 153 | def on_after_register(user: UserDB, request: Request): 154 | print(f"User {user.id} has registered.") 155 | 156 | 157 | def on_after_forgot_password(user: UserDB, token: str, request: Request): 158 | print(f"User {user.id} has forgot their password. Reset token: {token}") 159 | 160 | 161 | jwt_authentication = JWTAuthentication(secret=SECRET, lifetime_seconds=3600) 162 | 163 | app = FastAPI() 164 | fastapi_users = FastAPIUsers( 165 | user_db, [jwt_authentication], User, UserCreate, UserUpdate, UserDB, 166 | ) 167 | app.include_router( 168 | fastapi_users.get_auth_router(jwt_authentication), prefix="/auth/jwt", tags=["auth"] 169 | ) 170 | app.include_router( 171 | fastapi_users.get_register_router(on_after_register), prefix="/auth", tags=["auth"] 172 | ) 173 | app.include_router( 174 | fastapi_users.get_reset_password_router( 175 | SECRET, after_forgot_password=on_after_forgot_password 176 | ), 177 | prefix="/auth", 178 | tags=["auth"], 179 | ) 180 | app.include_router(fastapi_users.get_users_router(), prefix="/users", tags=["users"]) 181 | ``` 182 | 183 | Important things to notice: 184 | 185 | * `FastAPIUsers` takes two arguments less (`reset_password_token_secret` and `reset_password_token_lifetime_seconds`). 186 | * You have more flexibility to choose the **prefix** and **tags** of the routers. 187 | * The `/login`/`/logout` are now your responsibility to include for each backend. The path will change (before `/login/jwt`, after `/jwt/login`). 188 | * If you don't care about some of those routers, you can discard them. 189 | -------------------------------------------------------------------------------- /docs/migration/1x_to_2x.md: -------------------------------------------------------------------------------- 1 | # 1.x.x ➡️ 2.x.x 2 | 3 | ## JWT authentication backend 4 | 5 | To be fully compatible with Swagger authentication, the output of a successful login operation with the JWT authentication backend has changed: 6 | 7 | **Before** 8 | 9 | ```json 10 | { 11 | "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiOTIyMWZmYzktNjQwZi00MzcyLTg2ZDMtY2U2NDJjYmE1NjAzIiwiYXVkIjoiZmFzdGFwaS11c2VyczphdXRoIiwiZXhwIjoxNTcxNTA0MTkzfQ.M10bjOe45I5Ncu_uXvOmVV8QxnL-nZfcH96U90JaocI" 12 | } 13 | ``` 14 | 15 | **After** 16 | 17 | ```json 18 | { 19 | "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiOTIyMWZmYzktNjQwZi00MzcyLTg2ZDMtY2U2NDJjYmE1NjAzIiwiYXVkIjoiZmFzdGFwaS11c2VyczphdXRoIiwiZXhwIjoxNTcxNTA0MTkzfQ.M10bjOe45I5Ncu_uXvOmVV8QxnL-nZfcH96U90JaocI", 20 | "token_type": "bearer" 21 | } 22 | ``` 23 | 24 | Make sure to update your clients to read the token in the right property. 25 | -------------------------------------------------------------------------------- /docs/migration/2x_to_3x.md: -------------------------------------------------------------------------------- 1 | # 2.x.x ➡️ 3.x.x 2 | 3 | ## Emails are now case-insensitive 4 | 5 | Before 3.x.x, the local part (before the @) of the email address was case-sensitive. Therefore, `king.arthur@camelot.bt` and `King.Arthur@camelot.bt` were considered as **two different users**. This behaviour was a bit confusing and not consistent with 99% of web services out there. 6 | 7 | After 3.x.x, users are fetched from the database with a case-insensitive email search. Bear in mind though that if the user registers with the email `King.Arthur@camelot.bt`, it will be stored exactly like this in the database (with casing) ; but he will be able to login as `king.arthur@camelot.bt`. 8 | 9 | !!! danger 10 | It's super important then, before you upgrade to 3.x.x that you **check if there are several users with the same email with different cases** ; and that you **merge or delete those accounts**. 11 | -------------------------------------------------------------------------------- /docs/migration/3x_to_4x.md: -------------------------------------------------------------------------------- 1 | # 3.x.x ➡️ 4.x.x 2 | 3 | ## `expires_at` property in `OAuthAccount` is now optional 4 | 5 | Before 4.x.x, the `expires_at` property in `OAuthAccount` model was mandatory. It was causing issues with some services that don't have such expiration property. 6 | 7 | If you use **SQLAlchemy** or **Tortoise** databases adapters, you'll have to make a migration to update your database schema. 8 | -------------------------------------------------------------------------------- /docs/migration/4x_to_5x.md: -------------------------------------------------------------------------------- 1 | # 4.x.x ➡️ 5.x.x 2 | 3 | ## New property `is_verified` in `User` model. 4 | 5 | Starting 5.x.x., there is a new [e-mail verification feature](../configuration/routers/verify.md). Even if optional, the `is_verified` property has been added to the `User` model. 6 | 7 | If you use **SQLAlchemy** or **Tortoise** databases adapters, you'll have to make a migration to update your database schema. 8 | -------------------------------------------------------------------------------- /docs/migration/6x_to_7x.md: -------------------------------------------------------------------------------- 1 | # 6.x.x ➡️ 7.x.x 2 | 3 | * The deprecated dependencies to retrieve current user have been removed. Use the `current_user` factory instead. [[Documentation](https://fastapi-users.github.io/fastapi-users/usage/current-user/)] 4 | * When trying to authenticate a not verified user, a **status code 403** is raised instead of status code 401. Thanks @daanbeverdam 🎉 [[Documentation](https://fastapi-users.github.io/fastapi-users/usage/current-user/#current_user)] 5 | * Your `UserUpdate` model shouldn't inherit from the base `User` class. If you have custom fields, you should repeat them in this model. [[Documentation](https://fastapi-users.github.io/fastapi-users/configuration/model/#define-your-models)] 6 | 7 | --- 8 | 9 | * Database adapters now live in their [own repositories and packages](https://github.com/fastapi-users). 10 | * When upgrading to v7.0.0, the dependency for your database adapter should automatically be installed. 11 | * The `import` statements remain unchanged. 12 | -------------------------------------------------------------------------------- /docs/migration/7x_to_8x.md: -------------------------------------------------------------------------------- 1 | # 7.x.x ➡️ 8.x.x 2 | 3 | Version 8 includes the biggest code changes since version 1. We reorganized lot of parts of the code to make it even more modular and integrate more into the dependency injection system of FastAPI. 4 | 5 | Most importantly, you need now to implement a `UserManager` class and a associated dependency to create an instance of this class. 6 | 7 | ## Event handlers should live in the `UserManager` 8 | 9 | Before, event handlers like `on_after_register` or `on_after_forgot_password` were defined in their own functions that were passed as arguments of router generators. 10 | 11 | Now, they should be **methods** of the `UserManager` class. 12 | 13 | You can read more in the [`UserManager` documentation](../configuration/user-manager.md). 14 | 15 | ## Password validation should live in the `UserManager` 16 | 17 | Before, password validation was defined in its own function that was passed as argument of `FastAPIUsers`. 18 | 19 | Now, it should be a method of the `UserManager` class. 20 | 21 | You can read more in the [`UserManager` documentation](../configuration/user-manager.md). 22 | 23 | ## Verify token secret and lifetime parameters are attributes of `UserManager` 24 | 25 | Before, verify token and lifetime parameters were passed as argument of `get_verify_router`. 26 | 27 | Now, they should be defined as attributes of the `UserManager` class. 28 | 29 | You can read more in the [`UserManager` documentation](../configuration/user-manager.md). 30 | 31 | ## Reset password token secret and lifetime parameters are attributes of `UserManager` 32 | 33 | Before, reset password token and lifetime parameters were passed as argument of `get_verify_router`. 34 | 35 | Now, they should be defined as attributes of the `UserManager` class. 36 | 37 | You can read more in the [`UserManager` documentation](../configuration/user-manager.md). 38 | 39 | ## Database adapter should be provided in a dependency 40 | 41 | Before, we advised to directly instantiate the database adapter class. 42 | 43 | Now, it should be instantiated inside a dependency that you define yourself. The benefit of this is that it lives in the dependency injection system of FastAPI, allowing you to have more dynamic logic to create your instance. 44 | 45 | 46 | ➡️ [I'm using SQLAlchemy](../configuration/databases/sqlalchemy.md) 47 | 48 | ➡️ [I'm using MongoDB](../configuration/databases/mongodb.md) 49 | 50 | ➡️ [I'm using Tortoise ORM](../configuration/databases/tortoise.md) 51 | 52 | ➡️ [I'm using ormar](../configuration/databases/ormar.md) 53 | 54 | ## FastAPIUsers now expect a `get_user_manager` dependency 55 | 56 | Before, the database adapter instance was passed as argument of `FastAPIUsers`. 57 | 58 | Now, you should define a `get_user_manager` dependency returning an instance of your `UserManager` class. This dependency will be dependent of the database adapter dependency. 59 | 60 | 61 | You can read more in the [`UserManager` documentation](../configuration/user-manager.md) and [`FastAPIUsers` documentation](http://localhost:8000/configuration/routers/) 62 | 63 | ## Lost? 64 | 65 | If you're unsure or a bit lost, make sure to check the [full working examples](../configuration/full-example.md). 66 | -------------------------------------------------------------------------------- /docs/migration/8x_to_9x.md: -------------------------------------------------------------------------------- 1 | # 8.x.x ➡️ 9.x.x 2 | 3 | Version 9 revamps the authentication backends: we splitted the logic of a backend into two: the **transport**, which is how the token will be carried over the request and the **strategy**, which is how the token is generated and secured. 4 | 5 | The benefit of this is that we'll soon be able to propose new strategies, like database session tokens, without having to repeat the transport logic which remains the same. 6 | 7 | ## Convert the authentication backend 8 | 9 | You now have to generate an authentication backend with a transport and a strategy. 10 | 11 | ### I used JWTAuthentication 12 | 13 | === "Before" 14 | 15 | ```py 16 | from fastapi_users.authentication import JWTAuthentication 17 | 18 | jwt_authentication = JWTAuthentication( 19 | secret=SECRET, lifetime_seconds=3600, tokenUrl="auth/jwt/login" 20 | ) 21 | ``` 22 | 23 | === "After" 24 | 25 | ```py 26 | from fastapi_users.authentication import AuthenticationBackend, BearerTransport, JWTStrategy 27 | 28 | SECRET = "SECRET" 29 | 30 | bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") 31 | 32 | def get_jwt_strategy() -> JWTStrategy: 33 | return JWTStrategy(secret=SECRET, lifetime_seconds=3600) 34 | 35 | auth_backend = AuthenticationBackend( 36 | name="jwt", 37 | transport=bearer_transport, 38 | get_strategy=get_jwt_strategy, 39 | ) 40 | ``` 41 | 42 | !!! warning 43 | There is no default `name` anymore: you need to provide it yourself for each of your backends. 44 | 45 | ### I used CookieAuthentication 46 | 47 | === "Before" 48 | 49 | ```py 50 | from fastapi_users.authentication import CookieAuthentication 51 | 52 | cookie_authentication = CookieAuthentication(secret=SECRET, lifetime_seconds=3600) 53 | ``` 54 | 55 | === "After" 56 | 57 | ```py 58 | from fastapi_users.authentication import AuthenticationBackend, CookieTransport, JWTStrategy 59 | 60 | SECRET = "SECRET" 61 | 62 | cookie_transport = CookieTransport(cookie_max_age=3600) 63 | 64 | def get_jwt_strategy() -> JWTStrategy: 65 | return JWTStrategy(secret=SECRET, lifetime_seconds=3600) 66 | 67 | auth_backend = AuthenticationBackend( 68 | name="cookie", 69 | transport=cookie_transport, 70 | get_strategy=get_jwt_strategy, 71 | ) 72 | ``` 73 | 74 | !!! warning 75 | There is no default `name` anymore: you need to provide it yourself for each of your backends. 76 | 77 | !!! tip 78 | Notice that the strategy is the same for both authentication backends. That's the beauty of this approach: the token generation is decoupled from its transport. 79 | 80 | ## OAuth: one router for each backend 81 | 82 | Before, a single OAuth router was enough to login with any of your authentication backend. Now, you need to generate a router for each of your backends. 83 | 84 | === "Before" 85 | 86 | ```py 87 | app.include_router( 88 | fastapi_users.get_oauth_router(google_oauth_client, "SECRET"), 89 | prefix="/auth/google", 90 | tags=["auth"], 91 | ) 92 | ``` 93 | 94 | === "After" 95 | 96 | ```py 97 | app.include_router( 98 | fastapi_users.get_oauth_router(google_oauth_client, auth_backend, "SECRET"), 99 | prefix="/auth/google", 100 | tags=["auth"], 101 | ) 102 | ``` 103 | 104 | ### `authentication_backend` is not needed on `/authorize` 105 | 106 | The consequence of this is that you don't need to specify the authentication backend when making a request to `/authorize`. 107 | 108 | 109 | === "Before" 110 | 111 | ``` bash 112 | curl \ 113 | -H "Content-Type: application/json" \ 114 | -X GET \ 115 | http://localhost:8000/auth/google/authorize?authentication_backend=jwt 116 | ``` 117 | 118 | === "After" 119 | 120 | ``` bash 121 | curl \ 122 | -H "Content-Type: application/json" \ 123 | -X GET \ 124 | http://localhost:8000/auth/google/authorize 125 | ``` 126 | 127 | ## Lost? 128 | 129 | If you're unsure or a bit lost, make sure to check the [full working examples](../configuration/full-example.md). 130 | -------------------------------------------------------------------------------- /docs/src/cookbook_create_user_programmatically.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | 3 | from app.db import get_async_session, get_user_db 4 | from app.schemas import UserCreate 5 | from app.users import get_user_manager 6 | from fastapi_users.exceptions import UserAlreadyExists 7 | 8 | get_async_session_context = contextlib.asynccontextmanager(get_async_session) 9 | get_user_db_context = contextlib.asynccontextmanager(get_user_db) 10 | get_user_manager_context = contextlib.asynccontextmanager(get_user_manager) 11 | 12 | 13 | async def create_user(email: str, password: str, is_superuser: bool = False): 14 | try: 15 | async with get_async_session_context() as session: 16 | async with get_user_db_context(session) as user_db: 17 | async with get_user_manager_context(user_db) as user_manager: 18 | user = await user_manager.create( 19 | UserCreate( 20 | email=email, password=password, is_superuser=is_superuser 21 | ) 22 | ) 23 | print(f"User created {user}") 24 | return user 25 | except UserAlreadyExists: 26 | print(f"User {email} already exists") 27 | raise 28 | -------------------------------------------------------------------------------- /docs/src/db_beanie.py: -------------------------------------------------------------------------------- 1 | import motor.motor_asyncio 2 | from beanie import Document 3 | from fastapi_users.db import BeanieBaseUser, BeanieUserDatabase 4 | 5 | DATABASE_URL = "mongodb://localhost:27017" 6 | client = motor.motor_asyncio.AsyncIOMotorClient( 7 | DATABASE_URL, uuidRepresentation="standard" 8 | ) 9 | db = client["database_name"] 10 | 11 | 12 | class User(BeanieBaseUser, Document): 13 | pass 14 | 15 | 16 | async def get_user_db(): 17 | yield BeanieUserDatabase(User) 18 | -------------------------------------------------------------------------------- /docs/src/db_beanie_access_tokens.py: -------------------------------------------------------------------------------- 1 | import motor.motor_asyncio 2 | from beanie import Document 3 | from fastapi_users.db import BeanieBaseUser, BeanieUserDatabase 4 | from fastapi_users_db_beanie.access_token import ( 5 | BeanieAccessTokenDatabase, 6 | BeanieBaseAccessToken, 7 | ) 8 | 9 | DATABASE_URL = "mongodb://localhost:27017" 10 | client = motor.motor_asyncio.AsyncIOMotorClient( 11 | DATABASE_URL, uuidRepresentation="standard" 12 | ) 13 | db = client["database_name"] 14 | 15 | 16 | class User(BeanieBaseUser, Document): 17 | pass 18 | 19 | 20 | class AccessToken(BeanieBaseAccessToken, Document): # (1)! 21 | pass 22 | 23 | 24 | async def get_user_db(): 25 | yield BeanieUserDatabase(User) 26 | 27 | 28 | async def get_access_token_db(): # (2)! 29 | yield BeanieAccessTokenDatabase(AccessToken) 30 | -------------------------------------------------------------------------------- /docs/src/db_beanie_oauth.py: -------------------------------------------------------------------------------- 1 | import motor.motor_asyncio 2 | from beanie import Document 3 | from fastapi_users.db import BaseOAuthAccount, BeanieBaseUser, BeanieUserDatabase 4 | from pydantic import Field 5 | 6 | DATABASE_URL = "mongodb://localhost:27017" 7 | client = motor.motor_asyncio.AsyncIOMotorClient( 8 | DATABASE_URL, uuidRepresentation="standard" 9 | ) 10 | db = client["database_name"] 11 | 12 | 13 | class OAuthAccount(BaseOAuthAccount): 14 | pass 15 | 16 | 17 | class User(BeanieBaseUser, Document): 18 | oauth_accounts: list[OAuthAccount] = Field(default_factory=list) 19 | 20 | 21 | async def get_user_db(): 22 | yield BeanieUserDatabase(User, OAuthAccount) 23 | -------------------------------------------------------------------------------- /docs/src/db_sqlalchemy.py: -------------------------------------------------------------------------------- 1 | from collections.abc import AsyncGenerator 2 | 3 | from fastapi import Depends 4 | from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase 5 | from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine 6 | from sqlalchemy.orm import DeclarativeBase 7 | 8 | DATABASE_URL = "sqlite+aiosqlite:///./test.db" 9 | 10 | 11 | class Base(DeclarativeBase): 12 | pass 13 | 14 | 15 | class User(SQLAlchemyBaseUserTableUUID, Base): 16 | pass 17 | 18 | 19 | engine = create_async_engine(DATABASE_URL) 20 | async_session_maker = async_sessionmaker(engine, expire_on_commit=False) 21 | 22 | 23 | async def create_db_and_tables(): 24 | async with engine.begin() as conn: 25 | await conn.run_sync(Base.metadata.create_all) 26 | 27 | 28 | async def get_async_session() -> AsyncGenerator[AsyncSession, None]: 29 | async with async_session_maker() as session: 30 | yield session 31 | 32 | 33 | async def get_user_db(session: AsyncSession = Depends(get_async_session)): 34 | yield SQLAlchemyUserDatabase(session, User) 35 | -------------------------------------------------------------------------------- /docs/src/db_sqlalchemy_access_tokens.py: -------------------------------------------------------------------------------- 1 | from collections.abc import AsyncGenerator 2 | 3 | from fastapi import Depends 4 | from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase 5 | from fastapi_users_db_sqlalchemy.access_token import ( 6 | SQLAlchemyAccessTokenDatabase, 7 | SQLAlchemyBaseAccessTokenTableUUID, 8 | ) 9 | from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine 10 | from sqlalchemy.orm import DeclarativeBase 11 | 12 | DATABASE_URL = "sqlite+aiosqlite:///./test.db" 13 | 14 | 15 | class Base(DeclarativeBase): 16 | pass 17 | 18 | 19 | class User(SQLAlchemyBaseUserTableUUID, Base): 20 | pass 21 | 22 | 23 | class AccessToken(SQLAlchemyBaseAccessTokenTableUUID, Base): # (1)! 24 | pass 25 | 26 | 27 | engine = create_async_engine(DATABASE_URL) 28 | async_session_maker = async_sessionmaker(engine, expire_on_commit=False) 29 | 30 | 31 | async def create_db_and_tables(): 32 | async with engine.begin() as conn: 33 | await conn.run_sync(Base.metadata.create_all) 34 | 35 | 36 | async def get_async_session() -> AsyncGenerator[AsyncSession, None]: 37 | async with async_session_maker() as session: 38 | yield session 39 | 40 | 41 | async def get_user_db(session: AsyncSession = Depends(get_async_session)): 42 | yield SQLAlchemyUserDatabase(session, User) 43 | 44 | 45 | async def get_access_token_db( 46 | session: AsyncSession = Depends(get_async_session), 47 | ): # (2)! 48 | yield SQLAlchemyAccessTokenDatabase(session, AccessToken) 49 | -------------------------------------------------------------------------------- /docs/src/db_sqlalchemy_oauth.py: -------------------------------------------------------------------------------- 1 | from collections.abc import AsyncGenerator 2 | 3 | from fastapi import Depends 4 | from fastapi_users.db import ( 5 | SQLAlchemyBaseOAuthAccountTableUUID, 6 | SQLAlchemyBaseUserTableUUID, 7 | SQLAlchemyUserDatabase, 8 | ) 9 | from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine 10 | from sqlalchemy.orm import DeclarativeBase, Mapped, relationship 11 | 12 | DATABASE_URL = "sqlite+aiosqlite:///./test.db" 13 | 14 | 15 | class Base(DeclarativeBase): 16 | pass 17 | 18 | 19 | class OAuthAccount(SQLAlchemyBaseOAuthAccountTableUUID, Base): 20 | pass 21 | 22 | 23 | class User(SQLAlchemyBaseUserTableUUID, Base): 24 | oauth_accounts: Mapped[list[OAuthAccount]] = relationship( 25 | "OAuthAccount", lazy="joined" 26 | ) 27 | 28 | 29 | engine = create_async_engine(DATABASE_URL) 30 | async_session_maker = async_sessionmaker(engine, expire_on_commit=False) 31 | 32 | 33 | async def create_db_and_tables(): 34 | async with engine.begin() as conn: 35 | await conn.run_sync(Base.metadata.create_all) 36 | 37 | 38 | async def get_async_session() -> AsyncGenerator[AsyncSession, None]: 39 | async with async_session_maker() as session: 40 | yield session 41 | 42 | 43 | async def get_user_db(session: AsyncSession = Depends(get_async_session)): 44 | yield SQLAlchemyUserDatabase(session, User, OAuthAccount) 45 | -------------------------------------------------------------------------------- /docs/src/user_manager.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from typing import Optional 3 | 4 | from fastapi import Depends, Request 5 | from fastapi_users import BaseUserManager, UUIDIDMixin 6 | 7 | from .db import User, get_user_db 8 | 9 | SECRET = "SECRET" 10 | 11 | 12 | class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): 13 | reset_password_token_secret = SECRET 14 | verification_token_secret = SECRET 15 | 16 | async def on_after_register(self, user: User, request: Optional[Request] = None): 17 | print(f"User {user.id} has registered.") 18 | 19 | async def on_after_forgot_password( 20 | self, user: User, token: str, request: Optional[Request] = None 21 | ): 22 | print(f"User {user.id} has forgot their password. Reset token: {token}") 23 | 24 | async def on_after_request_verify( 25 | self, user: User, token: str, request: Optional[Request] = None 26 | ): 27 | print(f"Verification requested for user {user.id}. Verification token: {token}") 28 | 29 | 30 | async def get_user_manager(user_db=Depends(get_user_db)): 31 | yield UserManager(user_db) 32 | -------------------------------------------------------------------------------- /docs/usage/current-user.md: -------------------------------------------------------------------------------- 1 | # Get current user 2 | 3 | **FastAPI Users** provides a dependency callable to easily inject authenticated user in your routes. They are available from your `FastAPIUsers` instance. 4 | 5 | !!! tip 6 | For more information about how to make an authenticated request to your API, check the documentation of your [Authentication method](../configuration/authentication/index.md). 7 | 8 | ## `current_user` 9 | 10 | Return a dependency callable to retrieve currently authenticated user, passing the following parameters: 11 | 12 | * `optional`: If `True`, `None` is returned if there is no authenticated user or if it doesn't pass the other requirements. Otherwise, throw `401 Unauthorized`. Defaults to `False`. 13 | * `active`: If `True`, throw `401 Unauthorized` if the authenticated user is inactive. Defaults to `False`. 14 | * `verified`: If `True`, throw `403 Forbidden` if the authenticated user is not verified. Defaults to `False`. 15 | * `superuser`: If `True`, throw `403 Forbidden` if the authenticated user is not a superuser. Defaults to `False`. 16 | * `get_enabled_backends`: Optional dependency callable returning a list of enabled authentication backends. Useful if you want to dynamically enable some authentication backends based on external logic, like a configuration in database. By default, all specified authentication backends are enabled. *Please not however that every backends will appear in the OpenAPI documentation, as FastAPI resolves it statically.* 17 | 18 | !!! tip "Create it once and reuse it" 19 | This function is a **factory**, a function returning another function 🤯 20 | 21 | It's this returned function that will be the dependency called by FastAPI in your API routes. 22 | 23 | To avoid having to generate it on each route and avoid issues when unit testing, it's **strongly recommended** that you assign the result in a variable and reuse it at will in your routes. The examples below demonstrate this pattern. 24 | 25 | ## Examples 26 | 27 | ### Get the current user (**active or not**) 28 | 29 | ```py 30 | current_user = fastapi_users.current_user() 31 | 32 | @app.get("/protected-route") 33 | def protected_route(user: User = Depends(current_user)): 34 | return f"Hello, {user.email}" 35 | ``` 36 | 37 | ### Get the current **active** user 38 | 39 | ```py 40 | current_active_user = fastapi_users.current_user(active=True) 41 | 42 | @app.get("/protected-route") 43 | def protected_route(user: User = Depends(current_active_user)): 44 | return f"Hello, {user.email}" 45 | ``` 46 | 47 | ### Get the current **active** and **verified** user 48 | 49 | ```py 50 | current_active_verified_user = fastapi_users.current_user(active=True, verified=True) 51 | 52 | @app.get("/protected-route") 53 | def protected_route(user: User = Depends(current_active_verified_user)): 54 | return f"Hello, {user.email}" 55 | ``` 56 | 57 | ### Get the current active **superuser** 58 | 59 | ```py 60 | current_superuser = fastapi_users.current_user(active=True, superuser=True) 61 | 62 | @app.get("/protected-route") 63 | def protected_route(user: User = Depends(current_superuser)): 64 | return f"Hello, {user.email}" 65 | ``` 66 | 67 | ### Dynamically enable authentication backends 68 | 69 | !!! warning 70 | This is an advanced feature for cases where you have several authentication backends that are enabled conditionally. In most cases, you won't need this option. 71 | 72 | ```py 73 | from fastapi import Request 74 | from fastapi_users.authentication import AuthenticationBackend, BearerTransport, CookieTransport, JWTStrategy 75 | 76 | SECRET = "SECRET" 77 | 78 | bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") 79 | cookie_transport = CookieTransport(cookie_max_age=3600) 80 | 81 | def get_jwt_strategy() -> JWTStrategy: 82 | return JWTStrategy(secret=SECRET, lifetime_seconds=3600) 83 | 84 | jwt_backend = AuthenticationBackend( 85 | name="jwt", 86 | transport=bearer_transport, 87 | get_strategy=get_jwt_strategy, 88 | ) 89 | cookie_backend = AuthenticationBackend( 90 | name="jwt", 91 | transport=cookie_transport, 92 | get_strategy=get_jwt_strategy, 93 | ) 94 | 95 | async def get_enabled_backends(request: Request): 96 | """Return the enabled dependencies following custom logic.""" 97 | if request.url.path == "/protected-route-only-jwt": 98 | return [jwt_backend] 99 | else: 100 | return [cookie_backend, jwt_backend] 101 | 102 | 103 | current_active_user = fastapi_users.current_user(active=True, get_enabled_backends=get_enabled_backends) 104 | 105 | 106 | @app.get("/protected-route") 107 | def protected_route(user: User = Depends(current_active_user)): 108 | return f"Hello, {user.email}. You are authenticated with a cookie or a JWT." 109 | 110 | 111 | @app.get("/protected-route-only-jwt") 112 | def protected_route(user: User = Depends(current_active_user)): 113 | return f"Hello, {user.email}. You are authenticated with a JWT." 114 | ``` 115 | 116 | ## In a path operation 117 | 118 | If you don't need the user in the route logic, you can use this syntax: 119 | 120 | ```py 121 | @app.get("/protected-route", dependencies=[Depends(current_superuser)]) 122 | def protected_route(): 123 | return "Hello, some user." 124 | ``` 125 | 126 | You can read more about this [in FastAPI docs](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). 127 | -------------------------------------------------------------------------------- /examples/beanie-oauth/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastapi-users/fastapi-users/9d78b2a35dc7f35c2ffca67232c11f4d27a5db00/examples/beanie-oauth/app/__init__.py -------------------------------------------------------------------------------- /examples/beanie-oauth/app/app.py: -------------------------------------------------------------------------------- 1 | from contextlib import asynccontextmanager 2 | 3 | from beanie import init_beanie 4 | from fastapi import Depends, FastAPI 5 | 6 | from app.db import User, db 7 | from app.schemas import UserCreate, UserRead, UserUpdate 8 | from app.users import ( 9 | SECRET, 10 | auth_backend, 11 | current_active_user, 12 | fastapi_users, 13 | google_oauth_client, 14 | ) 15 | 16 | 17 | @asynccontextmanager 18 | async def lifespan(app: FastAPI): 19 | await init_beanie( 20 | database=db, 21 | document_models=[ 22 | User, 23 | ], 24 | ) 25 | yield 26 | 27 | 28 | app = FastAPI(lifespan=lifespan) 29 | 30 | app.include_router( 31 | fastapi_users.get_auth_router(auth_backend), prefix="/auth/jwt", tags=["auth"] 32 | ) 33 | app.include_router( 34 | fastapi_users.get_register_router(UserRead, UserCreate), 35 | prefix="/auth", 36 | tags=["auth"], 37 | ) 38 | app.include_router( 39 | fastapi_users.get_reset_password_router(), 40 | prefix="/auth", 41 | tags=["auth"], 42 | ) 43 | app.include_router( 44 | fastapi_users.get_verify_router(UserRead), 45 | prefix="/auth", 46 | tags=["auth"], 47 | ) 48 | app.include_router( 49 | fastapi_users.get_users_router(UserRead, UserUpdate), 50 | prefix="/users", 51 | tags=["users"], 52 | ) 53 | app.include_router( 54 | fastapi_users.get_oauth_router(google_oauth_client, auth_backend, SECRET), 55 | prefix="/auth/google", 56 | tags=["auth"], 57 | ) 58 | 59 | 60 | @app.get("/authenticated-route") 61 | async def authenticated_route(user: User = Depends(current_active_user)): 62 | return {"message": f"Hello {user.email}!"} 63 | -------------------------------------------------------------------------------- /examples/beanie-oauth/app/db.py: -------------------------------------------------------------------------------- 1 | import motor.motor_asyncio 2 | from beanie import Document 3 | from fastapi_users.db import BaseOAuthAccount, BeanieBaseUser, BeanieUserDatabase 4 | from pydantic import Field 5 | 6 | DATABASE_URL = "mongodb://localhost:27017" 7 | client = motor.motor_asyncio.AsyncIOMotorClient( 8 | DATABASE_URL, uuidRepresentation="standard" 9 | ) 10 | db = client["database_name"] 11 | 12 | 13 | class OAuthAccount(BaseOAuthAccount): 14 | pass 15 | 16 | 17 | class User(BeanieBaseUser, Document): 18 | oauth_accounts: list[OAuthAccount] = Field(default_factory=list) 19 | 20 | 21 | async def get_user_db(): 22 | yield BeanieUserDatabase(User, OAuthAccount) 23 | -------------------------------------------------------------------------------- /examples/beanie-oauth/app/schemas.py: -------------------------------------------------------------------------------- 1 | from beanie import PydanticObjectId 2 | from fastapi_users import schemas 3 | 4 | 5 | class UserRead(schemas.BaseUser[PydanticObjectId]): 6 | pass 7 | 8 | 9 | class UserCreate(schemas.BaseUserCreate): 10 | pass 11 | 12 | 13 | class UserUpdate(schemas.BaseUserUpdate): 14 | pass 15 | -------------------------------------------------------------------------------- /examples/beanie-oauth/app/users.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Optional 3 | 4 | from beanie import PydanticObjectId 5 | from fastapi import Depends, Request 6 | from fastapi_users import BaseUserManager, FastAPIUsers, models 7 | from fastapi_users.authentication import ( 8 | AuthenticationBackend, 9 | BearerTransport, 10 | JWTStrategy, 11 | ) 12 | from fastapi_users.db import BeanieUserDatabase, ObjectIDIDMixin 13 | from httpx_oauth.clients.google import GoogleOAuth2 14 | 15 | from app.db import User, get_user_db 16 | 17 | SECRET = "SECRET" 18 | 19 | google_oauth_client = GoogleOAuth2( 20 | os.getenv("GOOGLE_OAUTH_CLIENT_ID", ""), 21 | os.getenv("GOOGLE_OAUTH_CLIENT_SECRET", ""), 22 | ) 23 | 24 | 25 | class UserManager(ObjectIDIDMixin, BaseUserManager[User, PydanticObjectId]): 26 | reset_password_token_secret = SECRET 27 | verification_token_secret = SECRET 28 | 29 | async def on_after_register(self, user: User, request: Optional[Request] = None): 30 | print(f"User {user.id} has registered.") 31 | 32 | async def on_after_forgot_password( 33 | self, user: User, token: str, request: Optional[Request] = None 34 | ): 35 | print(f"User {user.id} has forgot their password. Reset token: {token}") 36 | 37 | async def on_after_request_verify( 38 | self, user: User, token: str, request: Optional[Request] = None 39 | ): 40 | print(f"Verification requested for user {user.id}. Verification token: {token}") 41 | 42 | 43 | async def get_user_manager(user_db: BeanieUserDatabase = Depends(get_user_db)): 44 | yield UserManager(user_db) 45 | 46 | 47 | bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") 48 | 49 | 50 | def get_jwt_strategy() -> JWTStrategy[models.UP, models.ID]: 51 | return JWTStrategy(secret=SECRET, lifetime_seconds=3600) 52 | 53 | 54 | auth_backend = AuthenticationBackend( 55 | name="jwt", 56 | transport=bearer_transport, 57 | get_strategy=get_jwt_strategy, 58 | ) 59 | 60 | fastapi_users = FastAPIUsers[User, PydanticObjectId](get_user_manager, [auth_backend]) 61 | 62 | current_active_user = fastapi_users.current_user(active=True) 63 | -------------------------------------------------------------------------------- /examples/beanie-oauth/main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | if __name__ == "__main__": 4 | uvicorn.run("app.app:app", host="0.0.0.0", log_level="info") 5 | -------------------------------------------------------------------------------- /examples/beanie-oauth/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | fastapi-users[beanie,oauth] 3 | uvicorn[standard] 4 | -------------------------------------------------------------------------------- /examples/beanie/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastapi-users/fastapi-users/9d78b2a35dc7f35c2ffca67232c11f4d27a5db00/examples/beanie/app/__init__.py -------------------------------------------------------------------------------- /examples/beanie/app/app.py: -------------------------------------------------------------------------------- 1 | from contextlib import asynccontextmanager 2 | 3 | from beanie import init_beanie 4 | from fastapi import Depends, FastAPI 5 | 6 | from app.db import User, db 7 | from app.schemas import UserCreate, UserRead, UserUpdate 8 | from app.users import auth_backend, current_active_user, fastapi_users 9 | 10 | 11 | @asynccontextmanager 12 | async def lifespan(app: FastAPI): 13 | await init_beanie( 14 | database=db, 15 | document_models=[ 16 | User, 17 | ], 18 | ) 19 | yield 20 | 21 | 22 | app = FastAPI(lifespan=lifespan) 23 | 24 | 25 | app.include_router( 26 | fastapi_users.get_auth_router(auth_backend), prefix="/auth/jwt", tags=["auth"] 27 | ) 28 | app.include_router( 29 | fastapi_users.get_register_router(UserRead, UserCreate), 30 | prefix="/auth", 31 | tags=["auth"], 32 | ) 33 | app.include_router( 34 | fastapi_users.get_reset_password_router(), 35 | prefix="/auth", 36 | tags=["auth"], 37 | ) 38 | app.include_router( 39 | fastapi_users.get_verify_router(UserRead), 40 | prefix="/auth", 41 | tags=["auth"], 42 | ) 43 | app.include_router( 44 | fastapi_users.get_users_router(UserRead, UserUpdate), 45 | prefix="/users", 46 | tags=["users"], 47 | ) 48 | 49 | 50 | @app.get("/authenticated-route") 51 | async def authenticated_route(user: User = Depends(current_active_user)): 52 | return {"message": f"Hello {user.email}!"} 53 | -------------------------------------------------------------------------------- /examples/beanie/app/db.py: -------------------------------------------------------------------------------- 1 | import motor.motor_asyncio 2 | from beanie import Document 3 | from fastapi_users.db import BeanieBaseUser 4 | from fastapi_users_db_beanie import BeanieUserDatabase 5 | 6 | DATABASE_URL = "mongodb://localhost:27017" 7 | client = motor.motor_asyncio.AsyncIOMotorClient( 8 | DATABASE_URL, uuidRepresentation="standard" 9 | ) 10 | db = client["database_name"] 11 | 12 | 13 | class User(BeanieBaseUser, Document): 14 | pass 15 | 16 | 17 | async def get_user_db(): 18 | yield BeanieUserDatabase(User) 19 | -------------------------------------------------------------------------------- /examples/beanie/app/schemas.py: -------------------------------------------------------------------------------- 1 | from beanie import PydanticObjectId 2 | from fastapi_users import schemas 3 | 4 | 5 | class UserRead(schemas.BaseUser[PydanticObjectId]): 6 | pass 7 | 8 | 9 | class UserCreate(schemas.BaseUserCreate): 10 | pass 11 | 12 | 13 | class UserUpdate(schemas.BaseUserUpdate): 14 | pass 15 | -------------------------------------------------------------------------------- /examples/beanie/app/users.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from beanie import PydanticObjectId 4 | from fastapi import Depends, Request 5 | from fastapi_users import BaseUserManager, FastAPIUsers 6 | from fastapi_users.authentication import ( 7 | AuthenticationBackend, 8 | BearerTransport, 9 | JWTStrategy, 10 | ) 11 | from fastapi_users.db import BeanieUserDatabase, ObjectIDIDMixin 12 | 13 | from app.db import User, get_user_db 14 | 15 | SECRET = "SECRET" 16 | 17 | 18 | class UserManager(ObjectIDIDMixin, BaseUserManager[User, PydanticObjectId]): 19 | reset_password_token_secret = SECRET 20 | verification_token_secret = SECRET 21 | 22 | async def on_after_register(self, user: User, request: Optional[Request] = None): 23 | print(f"User {user.id} has registered.") 24 | 25 | async def on_after_forgot_password( 26 | self, user: User, token: str, request: Optional[Request] = None 27 | ): 28 | print(f"User {user.id} has forgot their password. Reset token: {token}") 29 | 30 | async def on_after_request_verify( 31 | self, user: User, token: str, request: Optional[Request] = None 32 | ): 33 | print(f"Verification requested for user {user.id}. Verification token: {token}") 34 | 35 | 36 | async def get_user_manager(user_db: BeanieUserDatabase = Depends(get_user_db)): 37 | yield UserManager(user_db) 38 | 39 | 40 | bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") 41 | 42 | 43 | def get_jwt_strategy() -> JWTStrategy: 44 | return JWTStrategy(secret=SECRET, lifetime_seconds=3600) 45 | 46 | 47 | auth_backend = AuthenticationBackend( 48 | name="jwt", 49 | transport=bearer_transport, 50 | get_strategy=get_jwt_strategy, 51 | ) 52 | 53 | fastapi_users = FastAPIUsers[User, PydanticObjectId](get_user_manager, [auth_backend]) 54 | 55 | current_active_user = fastapi_users.current_user(active=True) 56 | -------------------------------------------------------------------------------- /examples/beanie/main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | if __name__ == "__main__": 4 | uvicorn.run("app.app:app", host="0.0.0.0", log_level="info") 5 | -------------------------------------------------------------------------------- /examples/beanie/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | fastapi-users[beanie] 3 | uvicorn[standard] 4 | -------------------------------------------------------------------------------- /examples/sqlalchemy-oauth/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastapi-users/fastapi-users/9d78b2a35dc7f35c2ffca67232c11f4d27a5db00/examples/sqlalchemy-oauth/app/__init__.py -------------------------------------------------------------------------------- /examples/sqlalchemy-oauth/app/app.py: -------------------------------------------------------------------------------- 1 | from contextlib import asynccontextmanager 2 | 3 | from fastapi import Depends, FastAPI 4 | 5 | from app.db import User, create_db_and_tables 6 | from app.schemas import UserCreate, UserRead, UserUpdate 7 | from app.users import ( 8 | SECRET, 9 | auth_backend, 10 | current_active_user, 11 | fastapi_users, 12 | google_oauth_client, 13 | ) 14 | 15 | 16 | @asynccontextmanager 17 | async def lifespan(app: FastAPI): 18 | # Not needed if you setup a migration system like Alembic 19 | await create_db_and_tables() 20 | yield 21 | 22 | 23 | app = FastAPI(lifespan=lifespan) 24 | 25 | app.include_router( 26 | fastapi_users.get_auth_router(auth_backend), prefix="/auth/jwt", tags=["auth"] 27 | ) 28 | app.include_router( 29 | fastapi_users.get_register_router(UserRead, UserCreate), 30 | prefix="/auth", 31 | tags=["auth"], 32 | ) 33 | app.include_router( 34 | fastapi_users.get_reset_password_router(), 35 | prefix="/auth", 36 | tags=["auth"], 37 | ) 38 | app.include_router( 39 | fastapi_users.get_verify_router(UserRead), 40 | prefix="/auth", 41 | tags=["auth"], 42 | ) 43 | app.include_router( 44 | fastapi_users.get_users_router(UserRead, UserUpdate), 45 | prefix="/users", 46 | tags=["users"], 47 | ) 48 | app.include_router( 49 | fastapi_users.get_oauth_router(google_oauth_client, auth_backend, SECRET), 50 | prefix="/auth/google", 51 | tags=["auth"], 52 | ) 53 | 54 | 55 | @app.get("/authenticated-route") 56 | async def authenticated_route(user: User = Depends(current_active_user)): 57 | return {"message": f"Hello {user.email}!"} 58 | -------------------------------------------------------------------------------- /examples/sqlalchemy-oauth/app/db.py: -------------------------------------------------------------------------------- 1 | from collections.abc import AsyncGenerator 2 | 3 | from fastapi import Depends 4 | from fastapi_users.db import ( 5 | SQLAlchemyBaseOAuthAccountTableUUID, 6 | SQLAlchemyBaseUserTableUUID, 7 | SQLAlchemyUserDatabase, 8 | ) 9 | from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine 10 | from sqlalchemy.orm import DeclarativeBase, Mapped, relationship 11 | 12 | DATABASE_URL = "sqlite+aiosqlite:///./test.db" 13 | 14 | 15 | class Base(DeclarativeBase): 16 | pass 17 | 18 | 19 | class OAuthAccount(SQLAlchemyBaseOAuthAccountTableUUID, Base): 20 | pass 21 | 22 | 23 | class User(SQLAlchemyBaseUserTableUUID, Base): 24 | oauth_accounts: Mapped[list[OAuthAccount]] = relationship( 25 | "OAuthAccount", lazy="joined" 26 | ) 27 | 28 | 29 | engine = create_async_engine(DATABASE_URL) 30 | async_session_maker = async_sessionmaker(engine, expire_on_commit=False) 31 | 32 | 33 | async def create_db_and_tables(): 34 | async with engine.begin() as conn: 35 | await conn.run_sync(Base.metadata.create_all) 36 | 37 | 38 | async def get_async_session() -> AsyncGenerator[AsyncSession, None]: 39 | async with async_session_maker() as session: 40 | yield session 41 | 42 | 43 | async def get_user_db(session: AsyncSession = Depends(get_async_session)): 44 | yield SQLAlchemyUserDatabase(session, User, OAuthAccount) 45 | -------------------------------------------------------------------------------- /examples/sqlalchemy-oauth/app/schemas.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from fastapi_users import schemas 4 | 5 | 6 | class UserRead(schemas.BaseUser[uuid.UUID]): 7 | pass 8 | 9 | 10 | class UserCreate(schemas.BaseUserCreate): 11 | pass 12 | 13 | 14 | class UserUpdate(schemas.BaseUserUpdate): 15 | pass 16 | -------------------------------------------------------------------------------- /examples/sqlalchemy-oauth/app/users.py: -------------------------------------------------------------------------------- 1 | import os 2 | import uuid 3 | from typing import Optional 4 | 5 | from fastapi import Depends, Request 6 | from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin, models 7 | from fastapi_users.authentication import ( 8 | AuthenticationBackend, 9 | BearerTransport, 10 | JWTStrategy, 11 | ) 12 | from fastapi_users.db import SQLAlchemyUserDatabase 13 | from httpx_oauth.clients.google import GoogleOAuth2 14 | 15 | from app.db import User, get_user_db 16 | 17 | SECRET = "SECRET" 18 | 19 | google_oauth_client = GoogleOAuth2( 20 | os.getenv("GOOGLE_OAUTH_CLIENT_ID", ""), 21 | os.getenv("GOOGLE_OAUTH_CLIENT_SECRET", ""), 22 | ) 23 | 24 | 25 | class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): 26 | reset_password_token_secret = SECRET 27 | verification_token_secret = SECRET 28 | 29 | async def on_after_register(self, user: User, request: Optional[Request] = None): 30 | print(f"User {user.id} has registered.") 31 | 32 | async def on_after_forgot_password( 33 | self, user: User, token: str, request: Optional[Request] = None 34 | ): 35 | print(f"User {user.id} has forgot their password. Reset token: {token}") 36 | 37 | async def on_after_request_verify( 38 | self, user: User, token: str, request: Optional[Request] = None 39 | ): 40 | print(f"Verification requested for user {user.id}. Verification token: {token}") 41 | 42 | 43 | async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)): 44 | yield UserManager(user_db) 45 | 46 | 47 | bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") 48 | 49 | 50 | def get_jwt_strategy() -> JWTStrategy[models.UP, models.ID]: 51 | return JWTStrategy(secret=SECRET, lifetime_seconds=3600) 52 | 53 | 54 | auth_backend = AuthenticationBackend( 55 | name="jwt", 56 | transport=bearer_transport, 57 | get_strategy=get_jwt_strategy, 58 | ) 59 | 60 | fastapi_users = FastAPIUsers[User, uuid.UUID](get_user_manager, [auth_backend]) 61 | 62 | current_active_user = fastapi_users.current_user(active=True) 63 | -------------------------------------------------------------------------------- /examples/sqlalchemy-oauth/main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | if __name__ == "__main__": 4 | uvicorn.run("app.app:app", host="0.0.0.0", log_level="info") 5 | -------------------------------------------------------------------------------- /examples/sqlalchemy-oauth/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | fastapi-users[sqlalchemy,oauth] 3 | uvicorn[standard] 4 | aiosqlite 5 | -------------------------------------------------------------------------------- /examples/sqlalchemy/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastapi-users/fastapi-users/9d78b2a35dc7f35c2ffca67232c11f4d27a5db00/examples/sqlalchemy/app/__init__.py -------------------------------------------------------------------------------- /examples/sqlalchemy/app/app.py: -------------------------------------------------------------------------------- 1 | from contextlib import asynccontextmanager 2 | 3 | from fastapi import Depends, FastAPI 4 | 5 | from app.db import User, create_db_and_tables 6 | from app.schemas import UserCreate, UserRead, UserUpdate 7 | from app.users import auth_backend, current_active_user, fastapi_users 8 | 9 | 10 | @asynccontextmanager 11 | async def lifespan(app: FastAPI): 12 | # Not needed if you setup a migration system like Alembic 13 | await create_db_and_tables() 14 | yield 15 | 16 | 17 | app = FastAPI(lifespan=lifespan) 18 | 19 | app.include_router( 20 | fastapi_users.get_auth_router(auth_backend), prefix="/auth/jwt", tags=["auth"] 21 | ) 22 | app.include_router( 23 | fastapi_users.get_register_router(UserRead, UserCreate), 24 | prefix="/auth", 25 | tags=["auth"], 26 | ) 27 | app.include_router( 28 | fastapi_users.get_reset_password_router(), 29 | prefix="/auth", 30 | tags=["auth"], 31 | ) 32 | app.include_router( 33 | fastapi_users.get_verify_router(UserRead), 34 | prefix="/auth", 35 | tags=["auth"], 36 | ) 37 | app.include_router( 38 | fastapi_users.get_users_router(UserRead, UserUpdate), 39 | prefix="/users", 40 | tags=["users"], 41 | ) 42 | 43 | 44 | @app.get("/authenticated-route") 45 | async def authenticated_route(user: User = Depends(current_active_user)): 46 | return {"message": f"Hello {user.email}!"} 47 | -------------------------------------------------------------------------------- /examples/sqlalchemy/app/db.py: -------------------------------------------------------------------------------- 1 | from collections.abc import AsyncGenerator 2 | 3 | from fastapi import Depends 4 | from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase 5 | from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine 6 | from sqlalchemy.orm import DeclarativeBase 7 | 8 | DATABASE_URL = "sqlite+aiosqlite:///./test.db" 9 | 10 | 11 | class Base(DeclarativeBase): 12 | pass 13 | 14 | 15 | class User(SQLAlchemyBaseUserTableUUID, Base): 16 | pass 17 | 18 | 19 | engine = create_async_engine(DATABASE_URL) 20 | async_session_maker = async_sessionmaker(engine, expire_on_commit=False) 21 | 22 | 23 | async def create_db_and_tables(): 24 | async with engine.begin() as conn: 25 | await conn.run_sync(Base.metadata.create_all) 26 | 27 | 28 | async def get_async_session() -> AsyncGenerator[AsyncSession, None]: 29 | async with async_session_maker() as session: 30 | yield session 31 | 32 | 33 | async def get_user_db(session: AsyncSession = Depends(get_async_session)): 34 | yield SQLAlchemyUserDatabase(session, User) 35 | -------------------------------------------------------------------------------- /examples/sqlalchemy/app/schemas.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from fastapi_users import schemas 4 | 5 | 6 | class UserRead(schemas.BaseUser[uuid.UUID]): 7 | pass 8 | 9 | 10 | class UserCreate(schemas.BaseUserCreate): 11 | pass 12 | 13 | 14 | class UserUpdate(schemas.BaseUserUpdate): 15 | pass 16 | -------------------------------------------------------------------------------- /examples/sqlalchemy/app/users.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from typing import Optional 3 | 4 | from fastapi import Depends, Request 5 | from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin, models 6 | from fastapi_users.authentication import ( 7 | AuthenticationBackend, 8 | BearerTransport, 9 | JWTStrategy, 10 | ) 11 | from fastapi_users.db import SQLAlchemyUserDatabase 12 | 13 | from app.db import User, get_user_db 14 | 15 | SECRET = "SECRET" 16 | 17 | 18 | class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): 19 | reset_password_token_secret = SECRET 20 | verification_token_secret = SECRET 21 | 22 | async def on_after_register(self, user: User, request: Optional[Request] = None): 23 | print(f"User {user.id} has registered.") 24 | 25 | async def on_after_forgot_password( 26 | self, user: User, token: str, request: Optional[Request] = None 27 | ): 28 | print(f"User {user.id} has forgot their password. Reset token: {token}") 29 | 30 | async def on_after_request_verify( 31 | self, user: User, token: str, request: Optional[Request] = None 32 | ): 33 | print(f"Verification requested for user {user.id}. Verification token: {token}") 34 | 35 | 36 | async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)): 37 | yield UserManager(user_db) 38 | 39 | 40 | bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") 41 | 42 | 43 | def get_jwt_strategy() -> JWTStrategy[models.UP, models.ID]: 44 | return JWTStrategy(secret=SECRET, lifetime_seconds=3600) 45 | 46 | 47 | auth_backend = AuthenticationBackend( 48 | name="jwt", 49 | transport=bearer_transport, 50 | get_strategy=get_jwt_strategy, 51 | ) 52 | 53 | fastapi_users = FastAPIUsers[User, uuid.UUID](get_user_manager, [auth_backend]) 54 | 55 | current_active_user = fastapi_users.current_user(active=True) 56 | -------------------------------------------------------------------------------- /examples/sqlalchemy/main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | if __name__ == "__main__": 4 | uvicorn.run("app.app:app", host="0.0.0.0", log_level="info") 5 | -------------------------------------------------------------------------------- /examples/sqlalchemy/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | fastapi-users[sqlalchemy] 3 | uvicorn[standard] 4 | aiosqlite 5 | -------------------------------------------------------------------------------- /fastapi_users/__init__.py: -------------------------------------------------------------------------------- 1 | """Ready-to-use and customizable users management for FastAPI.""" 2 | 3 | __version__ = "14.0.1" 4 | 5 | from fastapi_users import models, schemas # noqa: F401 6 | from fastapi_users.exceptions import InvalidID, InvalidPasswordException 7 | from fastapi_users.fastapi_users import FastAPIUsers # noqa: F401 8 | from fastapi_users.manager import ( # noqa: F401 9 | BaseUserManager, 10 | IntegerIDMixin, 11 | UUIDIDMixin, 12 | ) 13 | 14 | __all__ = [ 15 | "models", 16 | "schemas", 17 | "FastAPIUsers", 18 | "BaseUserManager", 19 | "InvalidPasswordException", 20 | "InvalidID", 21 | "UUIDIDMixin", 22 | "IntegerIDMixin", 23 | ] 24 | -------------------------------------------------------------------------------- /fastapi_users/authentication/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi_users.authentication.authenticator import Authenticator 2 | from fastapi_users.authentication.backend import AuthenticationBackend 3 | from fastapi_users.authentication.strategy import JWTStrategy, Strategy 4 | 5 | try: 6 | from fastapi_users.authentication.strategy import RedisStrategy 7 | except ImportError: # pragma: no cover 8 | pass 9 | 10 | from fastapi_users.authentication.transport import ( 11 | BearerTransport, 12 | CookieTransport, 13 | Transport, 14 | ) 15 | 16 | __all__ = [ 17 | "Authenticator", 18 | "AuthenticationBackend", 19 | "BearerTransport", 20 | "CookieTransport", 21 | "JWTStrategy", 22 | "RedisStrategy", 23 | "Strategy", 24 | "Transport", 25 | ] 26 | -------------------------------------------------------------------------------- /fastapi_users/authentication/backend.py: -------------------------------------------------------------------------------- 1 | from typing import Generic 2 | 3 | from fastapi import Response, status 4 | 5 | from fastapi_users import models 6 | from fastapi_users.authentication.strategy import ( 7 | Strategy, 8 | StrategyDestroyNotSupportedError, 9 | ) 10 | from fastapi_users.authentication.transport import ( 11 | Transport, 12 | TransportLogoutNotSupportedError, 13 | ) 14 | from fastapi_users.types import DependencyCallable 15 | 16 | 17 | class AuthenticationBackend(Generic[models.UP, models.ID]): 18 | """ 19 | Combination of an authentication transport and strategy. 20 | 21 | Together, they provide a full authentication method logic. 22 | 23 | :param name: Name of the backend. 24 | :param transport: Authentication transport instance. 25 | :param get_strategy: Dependency callable returning 26 | an authentication strategy instance. 27 | """ 28 | 29 | name: str 30 | transport: Transport 31 | 32 | def __init__( 33 | self, 34 | name: str, 35 | transport: Transport, 36 | get_strategy: DependencyCallable[Strategy[models.UP, models.ID]], 37 | ): 38 | self.name = name 39 | self.transport = transport 40 | self.get_strategy = get_strategy 41 | 42 | async def login( 43 | self, strategy: Strategy[models.UP, models.ID], user: models.UP 44 | ) -> Response: 45 | token = await strategy.write_token(user) 46 | return await self.transport.get_login_response(token) 47 | 48 | async def logout( 49 | self, strategy: Strategy[models.UP, models.ID], user: models.UP, token: str 50 | ) -> Response: 51 | try: 52 | await strategy.destroy_token(token, user) 53 | except StrategyDestroyNotSupportedError: 54 | pass 55 | 56 | try: 57 | response = await self.transport.get_logout_response() 58 | except TransportLogoutNotSupportedError: 59 | response = Response(status_code=status.HTTP_204_NO_CONTENT) 60 | 61 | return response 62 | -------------------------------------------------------------------------------- /fastapi_users/authentication/strategy/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi_users.authentication.strategy.base import ( 2 | Strategy, 3 | StrategyDestroyNotSupportedError, 4 | ) 5 | from fastapi_users.authentication.strategy.db import ( 6 | AP, 7 | AccessTokenDatabase, 8 | AccessTokenProtocol, 9 | DatabaseStrategy, 10 | ) 11 | from fastapi_users.authentication.strategy.jwt import JWTStrategy 12 | 13 | try: 14 | from fastapi_users.authentication.strategy.redis import RedisStrategy 15 | except ImportError: # pragma: no cover 16 | pass 17 | 18 | __all__ = [ 19 | "AP", 20 | "AccessTokenDatabase", 21 | "AccessTokenProtocol", 22 | "DatabaseStrategy", 23 | "JWTStrategy", 24 | "Strategy", 25 | "StrategyDestroyNotSupportedError", 26 | "RedisStrategy", 27 | ] 28 | -------------------------------------------------------------------------------- /fastapi_users/authentication/strategy/base.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, Optional, Protocol 2 | 3 | from fastapi_users import models 4 | from fastapi_users.manager import BaseUserManager 5 | 6 | 7 | class StrategyDestroyNotSupportedError(Exception): 8 | pass 9 | 10 | 11 | class Strategy(Protocol, Generic[models.UP, models.ID]): 12 | async def read_token( 13 | self, token: Optional[str], user_manager: BaseUserManager[models.UP, models.ID] 14 | ) -> Optional[models.UP]: ... # pragma: no cover 15 | 16 | async def write_token(self, user: models.UP) -> str: ... # pragma: no cover 17 | 18 | async def destroy_token( 19 | self, token: str, user: models.UP 20 | ) -> None: ... # pragma: no cover 21 | -------------------------------------------------------------------------------- /fastapi_users/authentication/strategy/db/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi_users.authentication.strategy.db.adapter import AccessTokenDatabase 2 | from fastapi_users.authentication.strategy.db.models import AP, AccessTokenProtocol 3 | from fastapi_users.authentication.strategy.db.strategy import DatabaseStrategy 4 | 5 | __all__ = ["AP", "AccessTokenDatabase", "AccessTokenProtocol", "DatabaseStrategy"] 6 | -------------------------------------------------------------------------------- /fastapi_users/authentication/strategy/db/adapter.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Any, Generic, Optional, Protocol 3 | 4 | from fastapi_users.authentication.strategy.db.models import AP 5 | 6 | 7 | class AccessTokenDatabase(Protocol, Generic[AP]): 8 | """Protocol for retrieving, creating and updating access tokens from a database.""" 9 | 10 | async def get_by_token( 11 | self, token: str, max_age: Optional[datetime] = None 12 | ) -> Optional[AP]: 13 | """Get a single access token by token.""" 14 | ... # pragma: no cover 15 | 16 | async def create(self, create_dict: dict[str, Any]) -> AP: 17 | """Create an access token.""" 18 | ... # pragma: no cover 19 | 20 | async def update(self, access_token: AP, update_dict: dict[str, Any]) -> AP: 21 | """Update an access token.""" 22 | ... # pragma: no cover 23 | 24 | async def delete(self, access_token: AP) -> None: 25 | """Delete an access token.""" 26 | ... # pragma: no cover 27 | -------------------------------------------------------------------------------- /fastapi_users/authentication/strategy/db/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Protocol, TypeVar 3 | 4 | from fastapi_users import models 5 | 6 | 7 | class AccessTokenProtocol(Protocol[models.ID]): 8 | """Access token protocol that ORM model should follow.""" 9 | 10 | token: str 11 | user_id: models.ID 12 | created_at: datetime 13 | 14 | 15 | AP = TypeVar("AP", bound=AccessTokenProtocol) 16 | -------------------------------------------------------------------------------- /fastapi_users/authentication/strategy/db/strategy.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | from datetime import datetime, timedelta, timezone 3 | from typing import Any, Generic, Optional 4 | 5 | from fastapi_users import exceptions, models 6 | from fastapi_users.authentication.strategy.base import Strategy 7 | from fastapi_users.authentication.strategy.db.adapter import AccessTokenDatabase 8 | from fastapi_users.authentication.strategy.db.models import AP 9 | from fastapi_users.manager import BaseUserManager 10 | 11 | 12 | class DatabaseStrategy( 13 | Strategy[models.UP, models.ID], Generic[models.UP, models.ID, AP] 14 | ): 15 | def __init__( 16 | self, database: AccessTokenDatabase[AP], lifetime_seconds: Optional[int] = None 17 | ): 18 | self.database = database 19 | self.lifetime_seconds = lifetime_seconds 20 | 21 | async def read_token( 22 | self, token: Optional[str], user_manager: BaseUserManager[models.UP, models.ID] 23 | ) -> Optional[models.UP]: 24 | if token is None: 25 | return None 26 | 27 | max_age = None 28 | if self.lifetime_seconds: 29 | max_age = datetime.now(timezone.utc) - timedelta( 30 | seconds=self.lifetime_seconds 31 | ) 32 | 33 | access_token = await self.database.get_by_token(token, max_age) 34 | if access_token is None: 35 | return None 36 | 37 | try: 38 | parsed_id = user_manager.parse_id(access_token.user_id) 39 | return await user_manager.get(parsed_id) 40 | except (exceptions.UserNotExists, exceptions.InvalidID): 41 | return None 42 | 43 | async def write_token(self, user: models.UP) -> str: 44 | access_token_dict = self._create_access_token_dict(user) 45 | access_token = await self.database.create(access_token_dict) 46 | return access_token.token 47 | 48 | async def destroy_token(self, token: str, user: models.UP) -> None: 49 | access_token = await self.database.get_by_token(token) 50 | if access_token is not None: 51 | await self.database.delete(access_token) 52 | 53 | def _create_access_token_dict(self, user: models.UP) -> dict[str, Any]: 54 | token = secrets.token_urlsafe() 55 | return {"token": token, "user_id": user.id} 56 | -------------------------------------------------------------------------------- /fastapi_users/authentication/strategy/jwt.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, Optional 2 | 3 | import jwt 4 | 5 | from fastapi_users import exceptions, models 6 | from fastapi_users.authentication.strategy.base import ( 7 | Strategy, 8 | StrategyDestroyNotSupportedError, 9 | ) 10 | from fastapi_users.jwt import SecretType, decode_jwt, generate_jwt 11 | from fastapi_users.manager import BaseUserManager 12 | 13 | 14 | class JWTStrategyDestroyNotSupportedError(StrategyDestroyNotSupportedError): 15 | def __init__(self) -> None: 16 | message = "A JWT can't be invalidated: it's valid until it expires." 17 | super().__init__(message) 18 | 19 | 20 | class JWTStrategy(Strategy[models.UP, models.ID], Generic[models.UP, models.ID]): 21 | def __init__( 22 | self, 23 | secret: SecretType, 24 | lifetime_seconds: Optional[int], 25 | token_audience: list[str] = ["fastapi-users:auth"], 26 | algorithm: str = "HS256", 27 | public_key: Optional[SecretType] = None, 28 | ): 29 | self.secret = secret 30 | self.lifetime_seconds = lifetime_seconds 31 | self.token_audience = token_audience 32 | self.algorithm = algorithm 33 | self.public_key = public_key 34 | 35 | @property 36 | def encode_key(self) -> SecretType: 37 | return self.secret 38 | 39 | @property 40 | def decode_key(self) -> SecretType: 41 | return self.public_key or self.secret 42 | 43 | async def read_token( 44 | self, token: Optional[str], user_manager: BaseUserManager[models.UP, models.ID] 45 | ) -> Optional[models.UP]: 46 | if token is None: 47 | return None 48 | 49 | try: 50 | data = decode_jwt( 51 | token, self.decode_key, self.token_audience, algorithms=[self.algorithm] 52 | ) 53 | user_id = data.get("sub") 54 | if user_id is None: 55 | return None 56 | except jwt.PyJWTError: 57 | return None 58 | 59 | try: 60 | parsed_id = user_manager.parse_id(user_id) 61 | return await user_manager.get(parsed_id) 62 | except (exceptions.UserNotExists, exceptions.InvalidID): 63 | return None 64 | 65 | async def write_token(self, user: models.UP) -> str: 66 | data = {"sub": str(user.id), "aud": self.token_audience} 67 | return generate_jwt( 68 | data, self.encode_key, self.lifetime_seconds, algorithm=self.algorithm 69 | ) 70 | 71 | async def destroy_token(self, token: str, user: models.UP) -> None: 72 | raise JWTStrategyDestroyNotSupportedError() 73 | -------------------------------------------------------------------------------- /fastapi_users/authentication/strategy/redis.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | from typing import Generic, Optional 3 | 4 | import redis.asyncio 5 | 6 | from fastapi_users import exceptions, models 7 | from fastapi_users.authentication.strategy.base import Strategy 8 | from fastapi_users.manager import BaseUserManager 9 | 10 | 11 | class RedisStrategy(Strategy[models.UP, models.ID], Generic[models.UP, models.ID]): 12 | def __init__( 13 | self, 14 | redis: redis.asyncio.Redis, 15 | lifetime_seconds: Optional[int] = None, 16 | *, 17 | key_prefix: str = "fastapi_users_token:", 18 | ): 19 | self.redis = redis 20 | self.lifetime_seconds = lifetime_seconds 21 | self.key_prefix = key_prefix 22 | 23 | async def read_token( 24 | self, token: Optional[str], user_manager: BaseUserManager[models.UP, models.ID] 25 | ) -> Optional[models.UP]: 26 | if token is None: 27 | return None 28 | 29 | user_id = await self.redis.get(f"{self.key_prefix}{token}") 30 | if user_id is None: 31 | return None 32 | 33 | try: 34 | parsed_id = user_manager.parse_id(user_id) 35 | return await user_manager.get(parsed_id) 36 | except (exceptions.UserNotExists, exceptions.InvalidID): 37 | return None 38 | 39 | async def write_token(self, user: models.UP) -> str: 40 | token = secrets.token_urlsafe() 41 | await self.redis.set( 42 | f"{self.key_prefix}{token}", str(user.id), ex=self.lifetime_seconds 43 | ) 44 | return token 45 | 46 | async def destroy_token(self, token: str, user: models.UP) -> None: 47 | await self.redis.delete(f"{self.key_prefix}{token}") 48 | -------------------------------------------------------------------------------- /fastapi_users/authentication/transport/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi_users.authentication.transport.base import ( 2 | Transport, 3 | TransportLogoutNotSupportedError, 4 | ) 5 | from fastapi_users.authentication.transport.bearer import BearerTransport 6 | from fastapi_users.authentication.transport.cookie import CookieTransport 7 | 8 | __all__ = [ 9 | "BearerTransport", 10 | "CookieTransport", 11 | "Transport", 12 | "TransportLogoutNotSupportedError", 13 | ] 14 | -------------------------------------------------------------------------------- /fastapi_users/authentication/transport/base.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol 2 | 3 | from fastapi import Response 4 | from fastapi.security.base import SecurityBase 5 | 6 | from fastapi_users.openapi import OpenAPIResponseType 7 | 8 | 9 | class TransportLogoutNotSupportedError(Exception): 10 | pass 11 | 12 | 13 | class Transport(Protocol): 14 | scheme: SecurityBase 15 | 16 | async def get_login_response(self, token: str) -> Response: ... # pragma: no cover 17 | 18 | async def get_logout_response(self) -> Response: ... # pragma: no cover 19 | 20 | @staticmethod 21 | def get_openapi_login_responses_success() -> OpenAPIResponseType: 22 | """Return a dictionary to use for the openapi responses route parameter.""" 23 | ... # pragma: no cover 24 | 25 | @staticmethod 26 | def get_openapi_logout_responses_success() -> OpenAPIResponseType: 27 | """Return a dictionary to use for the openapi responses route parameter.""" 28 | ... # pragma: no cover 29 | -------------------------------------------------------------------------------- /fastapi_users/authentication/transport/bearer.py: -------------------------------------------------------------------------------- 1 | from fastapi import Response, status 2 | from fastapi.responses import JSONResponse 3 | from fastapi.security import OAuth2PasswordBearer 4 | from pydantic import BaseModel 5 | 6 | from fastapi_users.authentication.transport.base import ( 7 | Transport, 8 | TransportLogoutNotSupportedError, 9 | ) 10 | from fastapi_users.openapi import OpenAPIResponseType 11 | from fastapi_users.schemas import model_dump 12 | 13 | 14 | class BearerResponse(BaseModel): 15 | access_token: str 16 | token_type: str 17 | 18 | 19 | class BearerTransport(Transport): 20 | scheme: OAuth2PasswordBearer 21 | 22 | def __init__(self, tokenUrl: str): 23 | self.scheme = OAuth2PasswordBearer(tokenUrl, auto_error=False) 24 | 25 | async def get_login_response(self, token: str) -> Response: 26 | bearer_response = BearerResponse(access_token=token, token_type="bearer") 27 | return JSONResponse(model_dump(bearer_response)) 28 | 29 | async def get_logout_response(self) -> Response: 30 | raise TransportLogoutNotSupportedError() 31 | 32 | @staticmethod 33 | def get_openapi_login_responses_success() -> OpenAPIResponseType: 34 | return { 35 | status.HTTP_200_OK: { 36 | "model": BearerResponse, 37 | "content": { 38 | "application/json": { 39 | "example": { 40 | "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1" 41 | "c2VyX2lkIjoiOTIyMWZmYzktNjQwZi00MzcyLTg2Z" 42 | "DMtY2U2NDJjYmE1NjAzIiwiYXVkIjoiZmFzdGFwaS" 43 | "11c2VyczphdXRoIiwiZXhwIjoxNTcxNTA0MTkzfQ." 44 | "M10bjOe45I5Ncu_uXvOmVV8QxnL-nZfcH96U90JaocI", 45 | "token_type": "bearer", 46 | } 47 | } 48 | }, 49 | }, 50 | } 51 | 52 | @staticmethod 53 | def get_openapi_logout_responses_success() -> OpenAPIResponseType: 54 | return {} 55 | -------------------------------------------------------------------------------- /fastapi_users/authentication/transport/cookie.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, Optional 2 | 3 | from fastapi import Response, status 4 | from fastapi.security import APIKeyCookie 5 | 6 | from fastapi_users.authentication.transport.base import Transport 7 | from fastapi_users.openapi import OpenAPIResponseType 8 | 9 | 10 | class CookieTransport(Transport): 11 | scheme: APIKeyCookie 12 | 13 | def __init__( 14 | self, 15 | cookie_name: str = "fastapiusersauth", 16 | cookie_max_age: Optional[int] = None, 17 | cookie_path: str = "/", 18 | cookie_domain: Optional[str] = None, 19 | cookie_secure: bool = True, 20 | cookie_httponly: bool = True, 21 | cookie_samesite: Literal["lax", "strict", "none"] = "lax", 22 | ): 23 | self.cookie_name = cookie_name 24 | self.cookie_max_age = cookie_max_age 25 | self.cookie_path = cookie_path 26 | self.cookie_domain = cookie_domain 27 | self.cookie_secure = cookie_secure 28 | self.cookie_httponly = cookie_httponly 29 | self.cookie_samesite = cookie_samesite 30 | self.scheme = APIKeyCookie(name=self.cookie_name, auto_error=False) 31 | 32 | async def get_login_response(self, token: str) -> Response: 33 | response = Response(status_code=status.HTTP_204_NO_CONTENT) 34 | return self._set_login_cookie(response, token) 35 | 36 | async def get_logout_response(self) -> Response: 37 | response = Response(status_code=status.HTTP_204_NO_CONTENT) 38 | return self._set_logout_cookie(response) 39 | 40 | def _set_login_cookie(self, response: Response, token: str) -> Response: 41 | response.set_cookie( 42 | self.cookie_name, 43 | token, 44 | max_age=self.cookie_max_age, 45 | path=self.cookie_path, 46 | domain=self.cookie_domain, 47 | secure=self.cookie_secure, 48 | httponly=self.cookie_httponly, 49 | samesite=self.cookie_samesite, 50 | ) 51 | return response 52 | 53 | def _set_logout_cookie(self, response: Response) -> Response: 54 | response.set_cookie( 55 | self.cookie_name, 56 | "", 57 | max_age=0, 58 | path=self.cookie_path, 59 | domain=self.cookie_domain, 60 | secure=self.cookie_secure, 61 | httponly=self.cookie_httponly, 62 | samesite=self.cookie_samesite, 63 | ) 64 | return response 65 | 66 | @staticmethod 67 | def get_openapi_login_responses_success() -> OpenAPIResponseType: 68 | return {status.HTTP_204_NO_CONTENT: {"model": None}} 69 | 70 | @staticmethod 71 | def get_openapi_logout_responses_success() -> OpenAPIResponseType: 72 | return {status.HTTP_204_NO_CONTENT: {"model": None}} 73 | -------------------------------------------------------------------------------- /fastapi_users/db/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi_users.db.base import BaseUserDatabase, UserDatabaseDependency 2 | 3 | __all__ = ["BaseUserDatabase", "UserDatabaseDependency"] 4 | 5 | 6 | try: # pragma: no cover 7 | from fastapi_users_db_sqlalchemy import ( # noqa: F401 8 | SQLAlchemyBaseOAuthAccountTable, 9 | SQLAlchemyBaseOAuthAccountTableUUID, 10 | SQLAlchemyBaseUserTable, 11 | SQLAlchemyBaseUserTableUUID, 12 | SQLAlchemyUserDatabase, 13 | ) 14 | 15 | __all__.append("SQLAlchemyBaseUserTable") 16 | __all__.append("SQLAlchemyBaseUserTableUUID") 17 | __all__.append("SQLAlchemyBaseOAuthAccountTable") 18 | __all__.append("SQLAlchemyBaseOAuthAccountTableUUID") 19 | __all__.append("SQLAlchemyUserDatabase") 20 | except ImportError: # pragma: no cover 21 | pass 22 | 23 | try: # pragma: no cover 24 | from fastapi_users_db_beanie import ( # noqa: F401 25 | BaseOAuthAccount, 26 | BeanieBaseUser, 27 | BeanieUserDatabase, 28 | ObjectIDIDMixin, 29 | ) 30 | 31 | __all__.append("BeanieBaseUser") 32 | __all__.append("BaseOAuthAccount") 33 | __all__.append("BeanieUserDatabase") 34 | __all__.append("ObjectIDIDMixin") 35 | except ImportError: # pragma: no cover 36 | pass 37 | -------------------------------------------------------------------------------- /fastapi_users/db/base.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Generic, Optional 2 | 3 | from fastapi_users.models import ID, OAP, UOAP, UP 4 | from fastapi_users.types import DependencyCallable 5 | 6 | 7 | class BaseUserDatabase(Generic[UP, ID]): 8 | """Base adapter for retrieving, creating and updating users from a database.""" 9 | 10 | async def get(self, id: ID) -> Optional[UP]: 11 | """Get a single user by id.""" 12 | raise NotImplementedError() 13 | 14 | async def get_by_email(self, email: str) -> Optional[UP]: 15 | """Get a single user by email.""" 16 | raise NotImplementedError() 17 | 18 | async def get_by_oauth_account(self, oauth: str, account_id: str) -> Optional[UP]: 19 | """Get a single user by OAuth account id.""" 20 | raise NotImplementedError() 21 | 22 | async def create(self, create_dict: dict[str, Any]) -> UP: 23 | """Create a user.""" 24 | raise NotImplementedError() 25 | 26 | async def update(self, user: UP, update_dict: dict[str, Any]) -> UP: 27 | """Update a user.""" 28 | raise NotImplementedError() 29 | 30 | async def delete(self, user: UP) -> None: 31 | """Delete a user.""" 32 | raise NotImplementedError() 33 | 34 | async def add_oauth_account( 35 | self: "BaseUserDatabase[UOAP, ID]", user: UOAP, create_dict: dict[str, Any] 36 | ) -> UOAP: 37 | """Create an OAuth account and add it to the user.""" 38 | raise NotImplementedError() 39 | 40 | async def update_oauth_account( 41 | self: "BaseUserDatabase[UOAP, ID]", 42 | user: UOAP, 43 | oauth_account: OAP, 44 | update_dict: dict[str, Any], 45 | ) -> UOAP: 46 | """Update an OAuth account on a user.""" 47 | raise NotImplementedError() 48 | 49 | 50 | UserDatabaseDependency = DependencyCallable[BaseUserDatabase[UP, ID]] 51 | -------------------------------------------------------------------------------- /fastapi_users/exceptions.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | 4 | class FastAPIUsersException(Exception): 5 | pass 6 | 7 | 8 | class InvalidID(FastAPIUsersException): 9 | pass 10 | 11 | 12 | class UserAlreadyExists(FastAPIUsersException): 13 | pass 14 | 15 | 16 | class UserNotExists(FastAPIUsersException): 17 | pass 18 | 19 | 20 | class UserInactive(FastAPIUsersException): 21 | pass 22 | 23 | 24 | class UserAlreadyVerified(FastAPIUsersException): 25 | pass 26 | 27 | 28 | class InvalidVerifyToken(FastAPIUsersException): 29 | pass 30 | 31 | 32 | class InvalidResetPasswordToken(FastAPIUsersException): 33 | pass 34 | 35 | 36 | class InvalidPasswordException(FastAPIUsersException): 37 | def __init__(self, reason: Any) -> None: 38 | self.reason = reason 39 | -------------------------------------------------------------------------------- /fastapi_users/fastapi_users.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Sequence 2 | from typing import Generic, Optional 3 | 4 | from fastapi import APIRouter 5 | 6 | from fastapi_users import models, schemas 7 | from fastapi_users.authentication import AuthenticationBackend, Authenticator 8 | from fastapi_users.jwt import SecretType 9 | from fastapi_users.manager import UserManagerDependency 10 | from fastapi_users.router import ( 11 | get_auth_router, 12 | get_register_router, 13 | get_reset_password_router, 14 | get_users_router, 15 | get_verify_router, 16 | ) 17 | 18 | try: 19 | from httpx_oauth.oauth2 import BaseOAuth2 20 | 21 | from fastapi_users.router import get_oauth_router 22 | from fastapi_users.router.oauth import get_oauth_associate_router 23 | except ModuleNotFoundError: # pragma: no cover 24 | BaseOAuth2 = type # type: ignore 25 | 26 | 27 | class FastAPIUsers(Generic[models.UP, models.ID]): 28 | """ 29 | Main object that ties together the component for users authentication. 30 | 31 | :param get_user_manager: Dependency callable getter to inject the 32 | user manager class instance. 33 | :param auth_backends: List of authentication backends. 34 | 35 | :attribute current_user: Dependency callable getter to inject authenticated user 36 | with a specific set of parameters. 37 | """ 38 | 39 | authenticator: Authenticator[models.UP, models.ID] 40 | 41 | def __init__( 42 | self, 43 | get_user_manager: UserManagerDependency[models.UP, models.ID], 44 | auth_backends: Sequence[AuthenticationBackend[models.UP, models.ID]], 45 | ): 46 | self.authenticator = Authenticator(auth_backends, get_user_manager) 47 | self.get_user_manager = get_user_manager 48 | self.current_user = self.authenticator.current_user 49 | 50 | def get_register_router( 51 | self, user_schema: type[schemas.U], user_create_schema: type[schemas.UC] 52 | ) -> APIRouter: 53 | """ 54 | Return a router with a register route. 55 | 56 | :param user_schema: Pydantic schema of a public user. 57 | :param user_create_schema: Pydantic schema for creating a user. 58 | """ 59 | return get_register_router( 60 | self.get_user_manager, user_schema, user_create_schema 61 | ) 62 | 63 | def get_verify_router(self, user_schema: type[schemas.U]) -> APIRouter: 64 | """ 65 | Return a router with e-mail verification routes. 66 | 67 | :param user_schema: Pydantic schema of a public user. 68 | """ 69 | return get_verify_router(self.get_user_manager, user_schema) 70 | 71 | def get_reset_password_router(self) -> APIRouter: 72 | """Return a reset password process router.""" 73 | return get_reset_password_router(self.get_user_manager) 74 | 75 | def get_auth_router( 76 | self, 77 | backend: AuthenticationBackend[models.UP, models.ID], 78 | requires_verification: bool = False, 79 | ) -> APIRouter: 80 | """ 81 | Return an auth router for a given authentication backend. 82 | 83 | :param backend: The authentication backend instance. 84 | :param requires_verification: Whether the authentication 85 | require the user to be verified or not. Defaults to False. 86 | """ 87 | return get_auth_router( 88 | backend, 89 | self.get_user_manager, 90 | self.authenticator, 91 | requires_verification, 92 | ) 93 | 94 | def get_oauth_router( 95 | self, 96 | oauth_client: BaseOAuth2, 97 | backend: AuthenticationBackend[models.UP, models.ID], 98 | state_secret: SecretType, 99 | redirect_url: Optional[str] = None, 100 | associate_by_email: bool = False, 101 | is_verified_by_default: bool = False, 102 | ) -> APIRouter: 103 | """ 104 | Return an OAuth router for a given OAuth client and authentication backend. 105 | 106 | :param oauth_client: The HTTPX OAuth client instance. 107 | :param backend: The authentication backend instance. 108 | :param state_secret: Secret used to encode the state JWT. 109 | :param redirect_url: Optional arbitrary redirect URL for the OAuth2 flow. 110 | If not given, the URL to the callback endpoint will be generated. 111 | :param associate_by_email: If True, any existing user with the same 112 | e-mail address will be associated to this user. Defaults to False. 113 | :param is_verified_by_default: If True, the `is_verified` flag will be 114 | set to `True` on newly created user. Make sure the OAuth Provider you're 115 | using does verify the email address before enabling this flag. 116 | """ 117 | return get_oauth_router( 118 | oauth_client, 119 | backend, 120 | self.get_user_manager, 121 | state_secret, 122 | redirect_url, 123 | associate_by_email, 124 | is_verified_by_default, 125 | ) 126 | 127 | def get_oauth_associate_router( 128 | self, 129 | oauth_client: BaseOAuth2, 130 | user_schema: type[schemas.U], 131 | state_secret: SecretType, 132 | redirect_url: Optional[str] = None, 133 | requires_verification: bool = False, 134 | ) -> APIRouter: 135 | """ 136 | Return an OAuth association router for a given OAuth client. 137 | 138 | :param oauth_client: The HTTPX OAuth client instance. 139 | :param user_schema: Pydantic schema of a public user. 140 | :param state_secret: Secret used to encode the state JWT. 141 | :param redirect_url: Optional arbitrary redirect URL for the OAuth2 flow. 142 | If not given, the URL to the callback endpoint will be generated. 143 | :param requires_verification: Whether the endpoints 144 | require the users to be verified or not. Defaults to False. 145 | """ 146 | return get_oauth_associate_router( 147 | oauth_client, 148 | self.authenticator, 149 | self.get_user_manager, 150 | user_schema, 151 | state_secret, 152 | redirect_url, 153 | requires_verification, 154 | ) 155 | 156 | def get_users_router( 157 | self, 158 | user_schema: type[schemas.U], 159 | user_update_schema: type[schemas.UU], 160 | requires_verification: bool = False, 161 | ) -> APIRouter: 162 | """ 163 | Return a router with routes to manage users. 164 | 165 | :param user_schema: Pydantic schema of a public user. 166 | :param user_update_schema: Pydantic schema for updating a user. 167 | :param requires_verification: Whether the endpoints 168 | require the users to be verified or not. Defaults to False. 169 | """ 170 | return get_users_router( 171 | self.get_user_manager, 172 | user_schema, 173 | user_update_schema, 174 | self.authenticator, 175 | requires_verification, 176 | ) 177 | -------------------------------------------------------------------------------- /fastapi_users/jwt.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta, timezone 2 | from typing import Any, Optional, Union 3 | 4 | import jwt 5 | from pydantic import SecretStr 6 | 7 | SecretType = Union[str, SecretStr] 8 | JWT_ALGORITHM = "HS256" 9 | 10 | 11 | def _get_secret_value(secret: SecretType) -> str: 12 | if isinstance(secret, SecretStr): 13 | return secret.get_secret_value() 14 | return secret 15 | 16 | 17 | def generate_jwt( 18 | data: dict, 19 | secret: SecretType, 20 | lifetime_seconds: Optional[int] = None, 21 | algorithm: str = JWT_ALGORITHM, 22 | ) -> str: 23 | payload = data.copy() 24 | if lifetime_seconds: 25 | expire = datetime.now(timezone.utc) + timedelta(seconds=lifetime_seconds) 26 | payload["exp"] = expire 27 | return jwt.encode(payload, _get_secret_value(secret), algorithm=algorithm) 28 | 29 | 30 | def decode_jwt( 31 | encoded_jwt: str, 32 | secret: SecretType, 33 | audience: list[str], 34 | algorithms: list[str] = [JWT_ALGORITHM], 35 | ) -> dict[str, Any]: 36 | return jwt.decode( 37 | encoded_jwt, 38 | _get_secret_value(secret), 39 | audience=audience, 40 | algorithms=algorithms, 41 | ) 42 | -------------------------------------------------------------------------------- /fastapi_users/models.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, Optional, Protocol, TypeVar 2 | 3 | ID = TypeVar("ID") 4 | 5 | 6 | class UserProtocol(Protocol[ID]): 7 | """User protocol that ORM model should follow.""" 8 | 9 | id: ID 10 | email: str 11 | hashed_password: str 12 | is_active: bool 13 | is_superuser: bool 14 | is_verified: bool 15 | 16 | 17 | class OAuthAccountProtocol(Protocol[ID]): 18 | """OAuth account protocol that ORM model should follow.""" 19 | 20 | id: ID 21 | oauth_name: str 22 | access_token: str 23 | expires_at: Optional[int] 24 | refresh_token: Optional[str] 25 | account_id: str 26 | account_email: str 27 | 28 | 29 | UP = TypeVar("UP", bound=UserProtocol) 30 | OAP = TypeVar("OAP", bound=OAuthAccountProtocol) 31 | 32 | 33 | class UserOAuthProtocol(UserProtocol[ID], Generic[ID, OAP]): 34 | """User protocol including a list of OAuth accounts.""" 35 | 36 | id: ID 37 | email: str 38 | hashed_password: str 39 | is_active: bool 40 | is_superuser: bool 41 | is_verified: bool 42 | oauth_accounts: list[OAP] 43 | 44 | 45 | UOAP = TypeVar("UOAP", bound=UserOAuthProtocol) 46 | -------------------------------------------------------------------------------- /fastapi_users/openapi.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Union 2 | 3 | OpenAPIResponseType = dict[Union[int, str], dict[str, Any]] 4 | -------------------------------------------------------------------------------- /fastapi_users/password.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | from typing import Optional, Protocol, Union 3 | 4 | from pwdlib import PasswordHash 5 | from pwdlib.hashers.argon2 import Argon2Hasher 6 | from pwdlib.hashers.bcrypt import BcryptHasher 7 | 8 | 9 | class PasswordHelperProtocol(Protocol): 10 | def verify_and_update( 11 | self, plain_password: str, hashed_password: str 12 | ) -> tuple[bool, Union[str, None]]: ... # pragma: no cover 13 | 14 | def hash(self, password: str) -> str: ... # pragma: no cover 15 | 16 | def generate(self) -> str: ... # pragma: no cover 17 | 18 | 19 | class PasswordHelper(PasswordHelperProtocol): 20 | def __init__(self, password_hash: Optional[PasswordHash] = None) -> None: 21 | if password_hash is None: 22 | self.password_hash = PasswordHash( 23 | ( 24 | Argon2Hasher(), 25 | BcryptHasher(), 26 | ) 27 | ) 28 | else: 29 | self.password_hash = password_hash # pragma: no cover 30 | 31 | def verify_and_update( 32 | self, plain_password: str, hashed_password: str 33 | ) -> tuple[bool, Union[str, None]]: 34 | return self.password_hash.verify_and_update(plain_password, hashed_password) 35 | 36 | def hash(self, password: str) -> str: 37 | return self.password_hash.hash(password) 38 | 39 | def generate(self) -> str: 40 | return secrets.token_urlsafe() 41 | -------------------------------------------------------------------------------- /fastapi_users/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastapi-users/fastapi-users/9d78b2a35dc7f35c2ffca67232c11f4d27a5db00/fastapi_users/py.typed -------------------------------------------------------------------------------- /fastapi_users/router/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi_users.router.auth import get_auth_router 2 | from fastapi_users.router.common import ErrorCode 3 | from fastapi_users.router.register import get_register_router 4 | from fastapi_users.router.reset import get_reset_password_router 5 | from fastapi_users.router.users import get_users_router 6 | from fastapi_users.router.verify import get_verify_router 7 | 8 | __all__ = [ 9 | "ErrorCode", 10 | "get_auth_router", 11 | "get_register_router", 12 | "get_reset_password_router", 13 | "get_users_router", 14 | "get_verify_router", 15 | ] 16 | 17 | try: # pragma: no cover 18 | from fastapi_users.router.oauth import get_oauth_router # noqa: F401 19 | 20 | __all__.append("get_oauth_router") 21 | except ModuleNotFoundError: # pragma: no cover 22 | pass 23 | -------------------------------------------------------------------------------- /fastapi_users/router/auth.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, HTTPException, Request, status 2 | from fastapi.security import OAuth2PasswordRequestForm 3 | 4 | from fastapi_users import models 5 | from fastapi_users.authentication import AuthenticationBackend, Authenticator, Strategy 6 | from fastapi_users.manager import BaseUserManager, UserManagerDependency 7 | from fastapi_users.openapi import OpenAPIResponseType 8 | from fastapi_users.router.common import ErrorCode, ErrorModel 9 | 10 | 11 | def get_auth_router( 12 | backend: AuthenticationBackend[models.UP, models.ID], 13 | get_user_manager: UserManagerDependency[models.UP, models.ID], 14 | authenticator: Authenticator[models.UP, models.ID], 15 | requires_verification: bool = False, 16 | ) -> APIRouter: 17 | """Generate a router with login/logout routes for an authentication backend.""" 18 | router = APIRouter() 19 | get_current_user_token = authenticator.current_user_token( 20 | active=True, verified=requires_verification 21 | ) 22 | 23 | login_responses: OpenAPIResponseType = { 24 | status.HTTP_400_BAD_REQUEST: { 25 | "model": ErrorModel, 26 | "content": { 27 | "application/json": { 28 | "examples": { 29 | ErrorCode.LOGIN_BAD_CREDENTIALS: { 30 | "summary": "Bad credentials or the user is inactive.", 31 | "value": {"detail": ErrorCode.LOGIN_BAD_CREDENTIALS}, 32 | }, 33 | ErrorCode.LOGIN_USER_NOT_VERIFIED: { 34 | "summary": "The user is not verified.", 35 | "value": {"detail": ErrorCode.LOGIN_USER_NOT_VERIFIED}, 36 | }, 37 | } 38 | } 39 | }, 40 | }, 41 | **backend.transport.get_openapi_login_responses_success(), 42 | } 43 | 44 | @router.post( 45 | "/login", 46 | name=f"auth:{backend.name}.login", 47 | responses=login_responses, 48 | ) 49 | async def login( 50 | request: Request, 51 | credentials: OAuth2PasswordRequestForm = Depends(), 52 | user_manager: BaseUserManager[models.UP, models.ID] = Depends(get_user_manager), 53 | strategy: Strategy[models.UP, models.ID] = Depends(backend.get_strategy), 54 | ): 55 | user = await user_manager.authenticate(credentials) 56 | 57 | if user is None or not user.is_active: 58 | raise HTTPException( 59 | status_code=status.HTTP_400_BAD_REQUEST, 60 | detail=ErrorCode.LOGIN_BAD_CREDENTIALS, 61 | ) 62 | if requires_verification and not user.is_verified: 63 | raise HTTPException( 64 | status_code=status.HTTP_400_BAD_REQUEST, 65 | detail=ErrorCode.LOGIN_USER_NOT_VERIFIED, 66 | ) 67 | response = await backend.login(strategy, user) 68 | await user_manager.on_after_login(user, request, response) 69 | return response 70 | 71 | logout_responses: OpenAPIResponseType = { 72 | **{ 73 | status.HTTP_401_UNAUTHORIZED: { 74 | "description": "Missing token or inactive user." 75 | } 76 | }, 77 | **backend.transport.get_openapi_logout_responses_success(), 78 | } 79 | 80 | @router.post( 81 | "/logout", name=f"auth:{backend.name}.logout", responses=logout_responses 82 | ) 83 | async def logout( 84 | user_token: tuple[models.UP, str] = Depends(get_current_user_token), 85 | strategy: Strategy[models.UP, models.ID] = Depends(backend.get_strategy), 86 | ): 87 | user, token = user_token 88 | return await backend.logout(strategy, user, token) 89 | 90 | return router 91 | -------------------------------------------------------------------------------- /fastapi_users/router/common.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Union 3 | 4 | from pydantic import BaseModel 5 | 6 | 7 | class ErrorModel(BaseModel): 8 | detail: Union[str, dict[str, str]] 9 | 10 | 11 | class ErrorCodeReasonModel(BaseModel): 12 | code: str 13 | reason: str 14 | 15 | 16 | class ErrorCode(str, Enum): 17 | REGISTER_INVALID_PASSWORD = "REGISTER_INVALID_PASSWORD" 18 | REGISTER_USER_ALREADY_EXISTS = "REGISTER_USER_ALREADY_EXISTS" 19 | OAUTH_NOT_AVAILABLE_EMAIL = "OAUTH_NOT_AVAILABLE_EMAIL" 20 | OAUTH_USER_ALREADY_EXISTS = "OAUTH_USER_ALREADY_EXISTS" 21 | LOGIN_BAD_CREDENTIALS = "LOGIN_BAD_CREDENTIALS" 22 | LOGIN_USER_NOT_VERIFIED = "LOGIN_USER_NOT_VERIFIED" 23 | RESET_PASSWORD_BAD_TOKEN = "RESET_PASSWORD_BAD_TOKEN" 24 | RESET_PASSWORD_INVALID_PASSWORD = "RESET_PASSWORD_INVALID_PASSWORD" 25 | VERIFY_USER_BAD_TOKEN = "VERIFY_USER_BAD_TOKEN" 26 | VERIFY_USER_ALREADY_VERIFIED = "VERIFY_USER_ALREADY_VERIFIED" 27 | UPDATE_USER_EMAIL_ALREADY_EXISTS = "UPDATE_USER_EMAIL_ALREADY_EXISTS" 28 | UPDATE_USER_INVALID_PASSWORD = "UPDATE_USER_INVALID_PASSWORD" 29 | -------------------------------------------------------------------------------- /fastapi_users/router/register.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, HTTPException, Request, status 2 | 3 | from fastapi_users import exceptions, models, schemas 4 | from fastapi_users.manager import BaseUserManager, UserManagerDependency 5 | from fastapi_users.router.common import ErrorCode, ErrorModel 6 | 7 | 8 | def get_register_router( 9 | get_user_manager: UserManagerDependency[models.UP, models.ID], 10 | user_schema: type[schemas.U], 11 | user_create_schema: type[schemas.UC], 12 | ) -> APIRouter: 13 | """Generate a router with the register route.""" 14 | router = APIRouter() 15 | 16 | @router.post( 17 | "/register", 18 | response_model=user_schema, 19 | status_code=status.HTTP_201_CREATED, 20 | name="register:register", 21 | responses={ 22 | status.HTTP_400_BAD_REQUEST: { 23 | "model": ErrorModel, 24 | "content": { 25 | "application/json": { 26 | "examples": { 27 | ErrorCode.REGISTER_USER_ALREADY_EXISTS: { 28 | "summary": "A user with this email already exists.", 29 | "value": { 30 | "detail": ErrorCode.REGISTER_USER_ALREADY_EXISTS 31 | }, 32 | }, 33 | ErrorCode.REGISTER_INVALID_PASSWORD: { 34 | "summary": "Password validation failed.", 35 | "value": { 36 | "detail": { 37 | "code": ErrorCode.REGISTER_INVALID_PASSWORD, 38 | "reason": "Password should be" 39 | "at least 3 characters", 40 | } 41 | }, 42 | }, 43 | } 44 | } 45 | }, 46 | }, 47 | }, 48 | ) 49 | async def register( 50 | request: Request, 51 | user_create: user_create_schema, # type: ignore 52 | user_manager: BaseUserManager[models.UP, models.ID] = Depends(get_user_manager), 53 | ): 54 | try: 55 | created_user = await user_manager.create( 56 | user_create, safe=True, request=request 57 | ) 58 | except exceptions.UserAlreadyExists: 59 | raise HTTPException( 60 | status_code=status.HTTP_400_BAD_REQUEST, 61 | detail=ErrorCode.REGISTER_USER_ALREADY_EXISTS, 62 | ) 63 | except exceptions.InvalidPasswordException as e: 64 | raise HTTPException( 65 | status_code=status.HTTP_400_BAD_REQUEST, 66 | detail={ 67 | "code": ErrorCode.REGISTER_INVALID_PASSWORD, 68 | "reason": e.reason, 69 | }, 70 | ) 71 | 72 | return schemas.model_validate(user_schema, created_user) 73 | 74 | return router 75 | -------------------------------------------------------------------------------- /fastapi_users/router/reset.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Body, Depends, HTTPException, Request, status 2 | from pydantic import EmailStr 3 | 4 | from fastapi_users import exceptions, models 5 | from fastapi_users.manager import BaseUserManager, UserManagerDependency 6 | from fastapi_users.openapi import OpenAPIResponseType 7 | from fastapi_users.router.common import ErrorCode, ErrorModel 8 | 9 | RESET_PASSWORD_RESPONSES: OpenAPIResponseType = { 10 | status.HTTP_400_BAD_REQUEST: { 11 | "model": ErrorModel, 12 | "content": { 13 | "application/json": { 14 | "examples": { 15 | ErrorCode.RESET_PASSWORD_BAD_TOKEN: { 16 | "summary": "Bad or expired token.", 17 | "value": {"detail": ErrorCode.RESET_PASSWORD_BAD_TOKEN}, 18 | }, 19 | ErrorCode.RESET_PASSWORD_INVALID_PASSWORD: { 20 | "summary": "Password validation failed.", 21 | "value": { 22 | "detail": { 23 | "code": ErrorCode.RESET_PASSWORD_INVALID_PASSWORD, 24 | "reason": "Password should be at least 3 characters", 25 | } 26 | }, 27 | }, 28 | } 29 | } 30 | }, 31 | }, 32 | } 33 | 34 | 35 | def get_reset_password_router( 36 | get_user_manager: UserManagerDependency[models.UP, models.ID], 37 | ) -> APIRouter: 38 | """Generate a router with the reset password routes.""" 39 | router = APIRouter() 40 | 41 | @router.post( 42 | "/forgot-password", 43 | status_code=status.HTTP_202_ACCEPTED, 44 | name="reset:forgot_password", 45 | ) 46 | async def forgot_password( 47 | request: Request, 48 | email: EmailStr = Body(..., embed=True), 49 | user_manager: BaseUserManager[models.UP, models.ID] = Depends(get_user_manager), 50 | ): 51 | try: 52 | user = await user_manager.get_by_email(email) 53 | except exceptions.UserNotExists: 54 | return None 55 | 56 | try: 57 | await user_manager.forgot_password(user, request) 58 | except exceptions.UserInactive: 59 | pass 60 | 61 | return None 62 | 63 | @router.post( 64 | "/reset-password", 65 | name="reset:reset_password", 66 | responses=RESET_PASSWORD_RESPONSES, 67 | ) 68 | async def reset_password( 69 | request: Request, 70 | token: str = Body(...), 71 | password: str = Body(...), 72 | user_manager: BaseUserManager[models.UP, models.ID] = Depends(get_user_manager), 73 | ): 74 | try: 75 | await user_manager.reset_password(token, password, request) 76 | except ( 77 | exceptions.InvalidResetPasswordToken, 78 | exceptions.UserNotExists, 79 | exceptions.UserInactive, 80 | ): 81 | raise HTTPException( 82 | status_code=status.HTTP_400_BAD_REQUEST, 83 | detail=ErrorCode.RESET_PASSWORD_BAD_TOKEN, 84 | ) 85 | except exceptions.InvalidPasswordException as e: 86 | raise HTTPException( 87 | status_code=status.HTTP_400_BAD_REQUEST, 88 | detail={ 89 | "code": ErrorCode.RESET_PASSWORD_INVALID_PASSWORD, 90 | "reason": e.reason, 91 | }, 92 | ) 93 | 94 | return router 95 | -------------------------------------------------------------------------------- /fastapi_users/router/verify.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Body, Depends, HTTPException, Request, status 2 | from pydantic import EmailStr 3 | 4 | from fastapi_users import exceptions, models, schemas 5 | from fastapi_users.manager import BaseUserManager, UserManagerDependency 6 | from fastapi_users.router.common import ErrorCode, ErrorModel 7 | 8 | 9 | def get_verify_router( 10 | get_user_manager: UserManagerDependency[models.UP, models.ID], 11 | user_schema: type[schemas.U], 12 | ): 13 | router = APIRouter() 14 | 15 | @router.post( 16 | "/request-verify-token", 17 | status_code=status.HTTP_202_ACCEPTED, 18 | name="verify:request-token", 19 | ) 20 | async def request_verify_token( 21 | request: Request, 22 | email: EmailStr = Body(..., embed=True), 23 | user_manager: BaseUserManager[models.UP, models.ID] = Depends(get_user_manager), 24 | ): 25 | try: 26 | user = await user_manager.get_by_email(email) 27 | await user_manager.request_verify(user, request) 28 | except ( 29 | exceptions.UserNotExists, 30 | exceptions.UserInactive, 31 | exceptions.UserAlreadyVerified, 32 | ): 33 | pass 34 | 35 | return None 36 | 37 | @router.post( 38 | "/verify", 39 | response_model=user_schema, 40 | name="verify:verify", 41 | responses={ 42 | status.HTTP_400_BAD_REQUEST: { 43 | "model": ErrorModel, 44 | "content": { 45 | "application/json": { 46 | "examples": { 47 | ErrorCode.VERIFY_USER_BAD_TOKEN: { 48 | "summary": "Bad token, not existing user or" 49 | "not the e-mail currently set for the user.", 50 | "value": {"detail": ErrorCode.VERIFY_USER_BAD_TOKEN}, 51 | }, 52 | ErrorCode.VERIFY_USER_ALREADY_VERIFIED: { 53 | "summary": "The user is already verified.", 54 | "value": { 55 | "detail": ErrorCode.VERIFY_USER_ALREADY_VERIFIED 56 | }, 57 | }, 58 | } 59 | } 60 | }, 61 | } 62 | }, 63 | ) 64 | async def verify( 65 | request: Request, 66 | token: str = Body(..., embed=True), 67 | user_manager: BaseUserManager[models.UP, models.ID] = Depends(get_user_manager), 68 | ): 69 | try: 70 | user = await user_manager.verify(token, request) 71 | return schemas.model_validate(user_schema, user) 72 | except (exceptions.InvalidVerifyToken, exceptions.UserNotExists): 73 | raise HTTPException( 74 | status_code=status.HTTP_400_BAD_REQUEST, 75 | detail=ErrorCode.VERIFY_USER_BAD_TOKEN, 76 | ) 77 | except exceptions.UserAlreadyVerified: 78 | raise HTTPException( 79 | status_code=status.HTTP_400_BAD_REQUEST, 80 | detail=ErrorCode.VERIFY_USER_ALREADY_VERIFIED, 81 | ) 82 | 83 | return router 84 | -------------------------------------------------------------------------------- /fastapi_users/schemas.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Generic, Optional, TypeVar 2 | 3 | from pydantic import BaseModel, ConfigDict, EmailStr 4 | from pydantic.version import VERSION as PYDANTIC_VERSION 5 | 6 | from fastapi_users import models 7 | 8 | PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.") 9 | 10 | SCHEMA = TypeVar("SCHEMA", bound=BaseModel) 11 | 12 | if PYDANTIC_V2: # pragma: no cover 13 | 14 | def model_dump(model: BaseModel, *args, **kwargs) -> dict[str, Any]: 15 | return model.model_dump(*args, **kwargs) # type: ignore 16 | 17 | def model_validate(schema: type[SCHEMA], obj: Any, *args, **kwargs) -> SCHEMA: 18 | return schema.model_validate(obj, *args, **kwargs) # type: ignore 19 | 20 | else: # pragma: no cover # type: ignore 21 | 22 | def model_dump(model: BaseModel, *args, **kwargs) -> dict[str, Any]: 23 | return model.dict(*args, **kwargs) # type: ignore 24 | 25 | def model_validate(schema: type[SCHEMA], obj: Any, *args, **kwargs) -> SCHEMA: 26 | return schema.from_orm(obj) # type: ignore 27 | 28 | 29 | class CreateUpdateDictModel(BaseModel): 30 | def create_update_dict(self): 31 | return model_dump( 32 | self, 33 | exclude_unset=True, 34 | exclude={ 35 | "id", 36 | "is_superuser", 37 | "is_active", 38 | "is_verified", 39 | "oauth_accounts", 40 | }, 41 | ) 42 | 43 | def create_update_dict_superuser(self): 44 | return model_dump(self, exclude_unset=True, exclude={"id"}) 45 | 46 | 47 | class BaseUser(CreateUpdateDictModel, Generic[models.ID]): 48 | """Base User model.""" 49 | 50 | id: models.ID 51 | email: EmailStr 52 | is_active: bool = True 53 | is_superuser: bool = False 54 | is_verified: bool = False 55 | 56 | if PYDANTIC_V2: # pragma: no cover 57 | model_config = ConfigDict(from_attributes=True) # type: ignore 58 | else: # pragma: no cover 59 | 60 | class Config: 61 | orm_mode = True 62 | 63 | 64 | class BaseUserCreate(CreateUpdateDictModel): 65 | email: EmailStr 66 | password: str 67 | is_active: Optional[bool] = True 68 | is_superuser: Optional[bool] = False 69 | is_verified: Optional[bool] = False 70 | 71 | 72 | class BaseUserUpdate(CreateUpdateDictModel): 73 | password: Optional[str] = None 74 | email: Optional[EmailStr] = None 75 | is_active: Optional[bool] = None 76 | is_superuser: Optional[bool] = None 77 | is_verified: Optional[bool] = None 78 | 79 | 80 | U = TypeVar("U", bound=BaseUser) 81 | UC = TypeVar("UC", bound=BaseUserCreate) 82 | UU = TypeVar("UU", bound=BaseUserUpdate) 83 | 84 | 85 | class BaseOAuthAccount(BaseModel, Generic[models.ID]): 86 | """Base OAuth account model.""" 87 | 88 | id: models.ID 89 | oauth_name: str 90 | access_token: str 91 | expires_at: Optional[int] = None 92 | refresh_token: Optional[str] = None 93 | account_id: str 94 | account_email: str 95 | 96 | if PYDANTIC_V2: # pragma: no cover 97 | model_config = ConfigDict(from_attributes=True) # type: ignore 98 | else: # pragma: no cover 99 | 100 | class Config: 101 | orm_mode = True 102 | 103 | 104 | class BaseOAuthAccountMixin(BaseModel): 105 | """Adds OAuth accounts list to a User model.""" 106 | 107 | oauth_accounts: list[BaseOAuthAccount] = [] 108 | -------------------------------------------------------------------------------- /fastapi_users/types.py: -------------------------------------------------------------------------------- 1 | from collections.abc import AsyncGenerator, AsyncIterator, Coroutine, Generator 2 | from typing import Callable, TypeVar, Union 3 | 4 | RETURN_TYPE = TypeVar("RETURN_TYPE") 5 | 6 | DependencyCallable = Callable[ 7 | ..., 8 | Union[ 9 | RETURN_TYPE, 10 | Coroutine[None, None, RETURN_TYPE], 11 | AsyncGenerator[RETURN_TYPE, None], 12 | Generator[RETURN_TYPE, None, None], 13 | AsyncIterator[RETURN_TYPE], 14 | ], 15 | ] 16 | -------------------------------------------------------------------------------- /logo_github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastapi-users/fastapi-users/9d78b2a35dc7f35c2ffca67232c11f4d27a5db00/logo_github.png -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: FastAPI Users 2 | site_description: Ready-to-use and customizable users management for FastAPI 3 | 4 | theme: 5 | name: 'material' 6 | custom_dir: docs-overrides 7 | palette: 8 | - scheme: default 9 | primary: 'red' 10 | accent: 'red' 11 | toggle: 12 | icon: material/weather-sunny 13 | name: Switch to dark mode 14 | - scheme: slate 15 | primary: 'red' 16 | accent: 'red' 17 | toggle: 18 | icon: material/weather-night 19 | name: Switch to light mode 20 | icon: 21 | logo: material/account-supervisor 22 | favicon: 'favicon.png' 23 | features: 24 | - navigation.instant 25 | - navigation.top 26 | - navigation.sections 27 | - search.suggest 28 | - search.highlight 29 | - content.code.annotate 30 | 31 | repo_name: fastapi-users/fastapi-users 32 | repo_url: https://github.com/fastapi-users/fastapi-users 33 | edit_uri: "" 34 | 35 | markdown_extensions: 36 | - toc: 37 | permalink: true 38 | - admonition 39 | - pymdownx.details 40 | - pymdownx.highlight: 41 | anchor_linenums: true 42 | - pymdownx.inlinehilite 43 | - pymdownx.snippets 44 | - pymdownx.superfences: 45 | custom_fences: 46 | - name: mermaid 47 | class: mermaid 48 | format: !!python/name:pymdownx.superfences.fence_code_format 49 | - pymdownx.tasklist: 50 | custom_checkbox: true 51 | - pymdownx.tabbed: 52 | alternate_style: true 53 | - pymdownx.emoji: 54 | emoji_index: !!python/name:materialx.emoji.twemoji 55 | emoji_generator: !!python/name:materialx.emoji.to_svg 56 | - attr_list 57 | - tables 58 | - def_list 59 | 60 | plugins: 61 | - search 62 | - mike 63 | 64 | extra: 65 | version: 66 | provider: mike 67 | 68 | nav: 69 | - About: index.md 70 | - installation.md 71 | - Configuration: 72 | - configuration/overview.md 73 | - User model and databases: 74 | - configuration/databases/sqlalchemy.md 75 | - configuration/databases/beanie.md 76 | - Authentication backends: 77 | - Introduction: configuration/authentication/index.md 78 | - Transports: 79 | - configuration/authentication/transports/cookie.md 80 | - configuration/authentication/transports/bearer.md 81 | - Strategies: 82 | - configuration/authentication/strategies/database.md 83 | - configuration/authentication/strategies/jwt.md 84 | - configuration/authentication/strategies/redis.md 85 | - configuration/authentication/backend.md 86 | - configuration/user-manager.md 87 | - configuration/schemas.md 88 | - Routers: 89 | - Introduction: configuration/routers/index.md 90 | - configuration/routers/auth.md 91 | - configuration/routers/register.md 92 | - configuration/routers/verify.md 93 | - configuration/routers/reset.md 94 | - configuration/routers/users.md 95 | - configuration/full-example.md 96 | - configuration/oauth.md 97 | - configuration/password-hash.md 98 | - Usage: 99 | - usage/flow.md 100 | - usage/routes.md 101 | - usage/current-user.md 102 | - Cookbook: 103 | - cookbook/create-user-programmatically.md 104 | - Migration: 105 | - migration/08_to_1x.md 106 | - migration/1x_to_2x.md 107 | - migration/2x_to_3x.md 108 | - migration/3x_to_4x.md 109 | - migration/4x_to_5x.md 110 | - migration/6x_to_7x.md 111 | - migration/7x_to_8x.md 112 | - migration/8x_to_9x.md 113 | - migration/9x_to_10x.md 114 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.isort] 2 | profile = "black" 3 | 4 | [[tool.mypy.overrides]] 5 | module = "motor.*" 6 | ignore_missing_imports = true 7 | 8 | [[tool.mypy.overrides]] 9 | module = "passlib.*" 10 | ignore_missing_imports = true 11 | 12 | [[tool.mypy.overrides]] 13 | module = "fastapi_users_db_beanie.*" 14 | ignore_missing_imports = true 15 | 16 | [[tool.mypy.overrides]] 17 | module = "fastapi_users_db_sqlalchemy.*" 18 | ignore_missing_imports = true 19 | 20 | [tool.pytest.ini_options] 21 | asyncio_mode = "auto" 22 | addopts = "--ignore=test_build.py" 23 | asyncio_default_fixture_loop_scope = "session" 24 | markers = [ 25 | "authentication", 26 | "db", 27 | "fastapi_users", 28 | "jwt", 29 | "manager", 30 | "oauth", 31 | "openapi", 32 | "router", 33 | ] 34 | 35 | [tool.ruff] 36 | target-version = "py39" 37 | 38 | [tool.ruff.lint] 39 | extend-select = ["UP", "TRY"] 40 | 41 | [tool.hatch] 42 | 43 | [tool.hatch.metadata] 44 | allow-direct-references = true 45 | 46 | [tool.hatch.version] 47 | source = "regex_commit" 48 | commit_extra_args = ["-e"] 49 | path = "fastapi_users/__init__.py" 50 | 51 | [tool.hatch.envs.default] 52 | installer = "uv" 53 | features = [ 54 | "sqlalchemy", 55 | "beanie", 56 | "oauth", 57 | "redis", 58 | ] 59 | dependencies = [ 60 | "pytest", 61 | "isort", 62 | "pytest-asyncio", 63 | "mike", 64 | "mkdocs", 65 | "mkdocs-material", 66 | "mkdocs-mermaid2-plugin", 67 | "mypy", 68 | "pytest-cov", 69 | "pytest-mock", 70 | "markdown-include", 71 | "pygments", 72 | "pymdown-extensions", 73 | "httpx-oauth", 74 | "httpx", 75 | "asgi_lifespan", 76 | "uvicorn", 77 | "types-redis", 78 | "ruff", 79 | ] 80 | 81 | [tool.hatch.envs.default.scripts] 82 | lint = [ 83 | "isort ./fastapi_users ./tests", 84 | "isort ./docs/src -o fastapi_users", 85 | "isort ./examples -o fastapi_users -p app", 86 | "ruff format .", 87 | "ruff check --fix .", 88 | "mypy fastapi_users/", 89 | ] 90 | lint-check = [ 91 | "isort --check-only ./fastapi_users ./tests", 92 | "isort --check-only ./docs/src -o fastapi_users", 93 | "isort --check-only ./examples -o fastapi_users -p app", 94 | "ruff format .", 95 | "ruff check .", 96 | "mypy fastapi_users/", 97 | ] 98 | docs = "mkdocs serve" 99 | 100 | [tool.hatch.envs.test] 101 | 102 | [tool.hatch.envs.test.scripts] 103 | test = "pytest --cov=fastapi_users/ --cov-report=term-missing --cov-fail-under=100" 104 | test-cov-xml = "pytest --cov=fastapi_users/ --cov-report=xml --cov-fail-under=100" 105 | 106 | [[tool.hatch.envs.test.matrix]] 107 | pydantic = ["v1", "v2"] 108 | 109 | [tool.hatch.envs.test.overrides] 110 | matrix.pydantic.extra-dependencies = [ 111 | {value = "pydantic<2.0", if = ["v1"]}, 112 | {value = "pydantic>=2.0", if = ["v2"]}, 113 | ] 114 | 115 | [tool.hatch.build.targets.sdist] 116 | support-legacy = true # Create setup.py 117 | 118 | [build-system] 119 | requires = ["hatchling", "hatch-regex-commit"] 120 | build-backend = "hatchling.build" 121 | 122 | [project] 123 | name = "fastapi-users" 124 | authors = [ 125 | { name = "François Voron", email = "fvoron@gmail.com" } 126 | ] 127 | description = "Ready-to-use and customizable users management for FastAPI" 128 | readme = "README.md" 129 | dynamic = ["version"] 130 | classifiers = [ 131 | "License :: OSI Approved :: MIT License", 132 | "Development Status :: 5 - Production/Stable", 133 | "Framework :: FastAPI", 134 | "Framework :: AsyncIO", 135 | "Intended Audience :: Developers", 136 | "Programming Language :: Python :: 3.9", 137 | "Programming Language :: Python :: 3.10", 138 | "Programming Language :: Python :: 3.11", 139 | "Programming Language :: Python :: 3.12", 140 | "Programming Language :: Python :: 3.13", 141 | "Programming Language :: Python :: 3 :: Only", 142 | "Topic :: Internet :: WWW/HTTP :: Session", 143 | ] 144 | requires-python = ">=3.9" 145 | dependencies = [ 146 | "fastapi >=0.65.2", 147 | "pwdlib[argon2,bcrypt] ==0.2.1", 148 | "email-validator >=1.1.0,<2.3", 149 | "pyjwt[crypto] ==2.10.1", 150 | "python-multipart ==0.0.20", 151 | "makefun >=1.11.2,<2.0.0", 152 | ] 153 | 154 | [project.optional-dependencies] 155 | sqlalchemy = [ 156 | "fastapi-users-db-sqlalchemy >=7.0.0", 157 | ] 158 | beanie = [ 159 | "fastapi-users-db-beanie >=4.0.0", 160 | ] 161 | oauth = [ 162 | "httpx-oauth >=0.13" 163 | ] 164 | redis = [ 165 | "redis >=4.3.3,<6.0.0", 166 | ] 167 | 168 | [project.urls] 169 | Documentation = "https://fastapi-users.github.io/fastapi-users/" 170 | Source = "https://github.com/fastapi-users/fastapi-users" 171 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 10.1.5 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:fastapi_users/__init__.py] 7 | search = __version__ = "{current_version}" 8 | replace = __version__ = "{new_version}" 9 | 10 | [flake8] 11 | exclude = docs 12 | max-line-length = 88 13 | docstring-convention = numpy 14 | ignore = D1, W503 15 | -------------------------------------------------------------------------------- /test_build.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | import sys 3 | 4 | try: 5 | from fastapi_users import FastAPIUsers 6 | except: 7 | sys.exit(1) 8 | 9 | sys.exit(0) 10 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastapi-users/fastapi-users/9d78b2a35dc7f35c2ffca67232c11f4d27a5db00/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_authentication_authenticator.py: -------------------------------------------------------------------------------- 1 | from collections.abc import AsyncGenerator, Sequence 2 | from typing import Generic, Optional 3 | 4 | import httpx 5 | import pytest 6 | import pytest_asyncio 7 | from fastapi import Depends, FastAPI, Request, status 8 | from fastapi.security.base import SecurityBase 9 | 10 | from fastapi_users import models 11 | from fastapi_users.authentication import AuthenticationBackend, Authenticator 12 | from fastapi_users.authentication.authenticator import DuplicateBackendNamesError 13 | from fastapi_users.authentication.strategy import Strategy 14 | from fastapi_users.authentication.transport import Transport 15 | from fastapi_users.manager import BaseUserManager 16 | from fastapi_users.types import DependencyCallable 17 | from tests.conftest import User, UserModel 18 | 19 | 20 | class MockSecurityScheme(SecurityBase): 21 | def __call__(self, request: Request) -> Optional[str]: 22 | return "mock" 23 | 24 | 25 | class MockTransport(Transport): 26 | scheme: MockSecurityScheme 27 | 28 | def __init__(self): 29 | self.scheme = MockSecurityScheme() 30 | 31 | 32 | class NoneStrategy(Strategy): 33 | async def read_token( 34 | self, token: Optional[str], user_manager: BaseUserManager[models.UP, models.ID] 35 | ) -> Optional[models.UP]: 36 | return None 37 | 38 | 39 | class UserStrategy(Strategy, Generic[models.UP]): 40 | def __init__(self, user: models.UP): 41 | self.user = user 42 | 43 | async def read_token( 44 | self, token: Optional[str], user_manager: BaseUserManager[models.UP, models.ID] 45 | ) -> Optional[models.UP]: 46 | return self.user 47 | 48 | 49 | @pytest.fixture 50 | def get_backend_none(): 51 | def _get_backend_none(name: str = "none"): 52 | return AuthenticationBackend( 53 | name=name, transport=MockTransport(), get_strategy=lambda: NoneStrategy() 54 | ) 55 | 56 | return _get_backend_none 57 | 58 | 59 | @pytest.fixture 60 | def get_backend_user(user: UserModel): 61 | def _get_backend_user(name: str = "user"): 62 | return AuthenticationBackend( 63 | name=name, 64 | transport=MockTransport(), 65 | get_strategy=lambda: UserStrategy(user), 66 | ) 67 | 68 | return _get_backend_user 69 | 70 | 71 | @pytest_asyncio.fixture 72 | def get_test_auth_client(get_user_manager, get_test_client): 73 | async def _get_test_auth_client( 74 | backends: list[AuthenticationBackend], 75 | get_enabled_backends: Optional[ 76 | DependencyCallable[Sequence[AuthenticationBackend]] 77 | ] = None, 78 | ) -> AsyncGenerator[httpx.AsyncClient, None]: 79 | app = FastAPI() 80 | authenticator = Authenticator(backends, get_user_manager) 81 | 82 | @app.get("/test-current-user", response_model=User) 83 | def test_current_user( 84 | user: UserModel = Depends( 85 | authenticator.current_user(get_enabled_backends=get_enabled_backends) 86 | ), 87 | ): 88 | return user 89 | 90 | @app.get("/test-current-active-user", response_model=User) 91 | def test_current_active_user( 92 | user: UserModel = Depends( 93 | authenticator.current_user( 94 | active=True, get_enabled_backends=get_enabled_backends 95 | ) 96 | ), 97 | ): 98 | return user 99 | 100 | @app.get("/test-current-superuser", response_model=User) 101 | def test_current_superuser( 102 | user: UserModel = Depends( 103 | authenticator.current_user( 104 | active=True, 105 | superuser=True, 106 | get_enabled_backends=get_enabled_backends, 107 | ) 108 | ), 109 | ): 110 | return user 111 | 112 | async for client in get_test_client(app): 113 | yield client 114 | 115 | return _get_test_auth_client 116 | 117 | 118 | @pytest.mark.authentication 119 | @pytest.mark.asyncio 120 | async def test_authenticator(get_test_auth_client, get_backend_none, get_backend_user): 121 | async for client in get_test_auth_client([get_backend_none(), get_backend_user()]): 122 | response = await client.get("/test-current-user") 123 | assert response.status_code == status.HTTP_200_OK 124 | 125 | 126 | @pytest.mark.authentication 127 | @pytest.mark.asyncio 128 | async def test_authenticator_none(get_test_auth_client, get_backend_none): 129 | async for client in get_test_auth_client( 130 | [get_backend_none(), get_backend_none(name="none-bis")] 131 | ): 132 | response = await client.get("/test-current-user") 133 | assert response.status_code == status.HTTP_401_UNAUTHORIZED 134 | 135 | 136 | @pytest.mark.authentication 137 | @pytest.mark.asyncio 138 | async def test_authenticator_none_enabled( 139 | get_test_auth_client, get_backend_none, get_backend_user 140 | ): 141 | backend_none = get_backend_none() 142 | backend_user = get_backend_user() 143 | 144 | async def get_enabled_backends(): 145 | return [backend_none] 146 | 147 | async for client in get_test_auth_client( 148 | [backend_none, backend_user], get_enabled_backends 149 | ): 150 | response = await client.get("/test-current-user") 151 | assert response.status_code == status.HTTP_401_UNAUTHORIZED 152 | 153 | 154 | @pytest.mark.authentication 155 | @pytest.mark.asyncio 156 | async def test_authenticators_with_same_name(get_test_auth_client, get_backend_none): 157 | with pytest.raises(DuplicateBackendNamesError): 158 | async for _ in get_test_auth_client([get_backend_none(), get_backend_none()]): 159 | pass 160 | -------------------------------------------------------------------------------- /tests/test_authentication_backend.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Generic, Optional, cast 2 | 3 | import pytest 4 | from fastapi import Response 5 | 6 | from fastapi_users import models 7 | from fastapi_users.authentication import ( 8 | AuthenticationBackend, 9 | BearerTransport, 10 | Strategy, 11 | ) 12 | from fastapi_users.authentication.strategy import StrategyDestroyNotSupportedError 13 | from fastapi_users.authentication.transport.base import Transport 14 | from fastapi_users.manager import BaseUserManager 15 | from tests.conftest import MockStrategy, MockTransport, UserModel 16 | 17 | 18 | class MockTransportLogoutNotSupported(BearerTransport): 19 | pass 20 | 21 | 22 | class MockStrategyDestroyNotSupported(Strategy, Generic[models.UP]): 23 | async def read_token( 24 | self, token: Optional[str], user_manager: BaseUserManager[models.UP, models.ID] 25 | ) -> Optional[models.UP]: 26 | return None 27 | 28 | async def write_token(self, user: models.UP) -> str: 29 | return "TOKEN" 30 | 31 | async def destroy_token(self, token: str, user: models.UP) -> None: 32 | raise StrategyDestroyNotSupportedError 33 | 34 | 35 | @pytest.fixture(params=[MockTransport, MockTransportLogoutNotSupported]) 36 | def transport(request) -> Transport: 37 | transport_class: type[BearerTransport] = request.param 38 | return transport_class(tokenUrl="/login") 39 | 40 | 41 | @pytest.fixture(params=[MockStrategy, MockStrategyDestroyNotSupported]) 42 | def get_strategy(request) -> Callable[..., Strategy]: 43 | strategy_class: type[Strategy] = request.param 44 | return lambda: strategy_class() 45 | 46 | 47 | @pytest.fixture 48 | def backend( 49 | transport: Transport, get_strategy: Callable[..., Strategy] 50 | ) -> AuthenticationBackend: 51 | return AuthenticationBackend( 52 | name="mock", transport=transport, get_strategy=get_strategy 53 | ) 54 | 55 | 56 | @pytest.mark.asyncio 57 | @pytest.mark.authentication 58 | async def test_logout(backend: AuthenticationBackend, user: UserModel): 59 | strategy = cast(Strategy, backend.get_strategy()) 60 | result = await backend.logout(strategy, user, "TOKEN") 61 | assert isinstance(result, Response) 62 | -------------------------------------------------------------------------------- /tests/test_authentication_strategy_db.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | import uuid 3 | from datetime import datetime, timezone 4 | from typing import Any, Optional 5 | 6 | import pytest 7 | 8 | from fastapi_users.authentication.strategy import ( 9 | AccessTokenDatabase, 10 | AccessTokenProtocol, 11 | DatabaseStrategy, 12 | ) 13 | from tests.conftest import IDType, UserModel 14 | 15 | 16 | @dataclasses.dataclass 17 | class AccessTokenModel(AccessTokenProtocol[IDType]): 18 | token: str 19 | user_id: uuid.UUID 20 | id: uuid.UUID = dataclasses.field(default_factory=uuid.uuid4) 21 | created_at: datetime = dataclasses.field( 22 | default_factory=lambda: datetime.now(timezone.utc) 23 | ) 24 | 25 | 26 | class AccessTokenDatabaseMock(AccessTokenDatabase[AccessTokenModel]): 27 | store: dict[str, AccessTokenModel] 28 | 29 | def __init__(self): 30 | self.store = {} 31 | 32 | async def get_by_token( 33 | self, token: str, max_age: Optional[datetime] = None 34 | ) -> Optional[AccessTokenModel]: 35 | try: 36 | access_token = self.store[token] 37 | if max_age is not None and access_token.created_at < max_age: 38 | return None 39 | except KeyError: 40 | return None 41 | else: 42 | return access_token 43 | 44 | async def create(self, create_dict: dict[str, Any]) -> AccessTokenModel: 45 | access_token = AccessTokenModel(**create_dict) 46 | self.store[access_token.token] = access_token 47 | return access_token 48 | 49 | async def update( 50 | self, access_token: AccessTokenModel, update_dict: dict[str, Any] 51 | ) -> AccessTokenModel: 52 | for field, value in update_dict.items(): 53 | setattr(access_token, field, value) 54 | self.store[access_token.token] = access_token 55 | return access_token 56 | 57 | async def delete(self, access_token: AccessTokenModel) -> None: 58 | try: 59 | del self.store[access_token.token] 60 | except KeyError: 61 | pass 62 | 63 | 64 | @pytest.fixture 65 | def access_token_database() -> AccessTokenDatabaseMock: 66 | return AccessTokenDatabaseMock() 67 | 68 | 69 | @pytest.fixture 70 | def database_strategy(access_token_database: AccessTokenDatabaseMock): 71 | return DatabaseStrategy(access_token_database, 3600) 72 | 73 | 74 | @pytest.mark.authentication 75 | class TestReadToken: 76 | @pytest.mark.asyncio 77 | async def test_missing_token( 78 | self, 79 | database_strategy: DatabaseStrategy[UserModel, IDType, AccessTokenModel], 80 | user_manager, 81 | ): 82 | authenticated_user = await database_strategy.read_token(None, user_manager) 83 | assert authenticated_user is None 84 | 85 | @pytest.mark.asyncio 86 | async def test_invalid_token( 87 | self, 88 | database_strategy: DatabaseStrategy[UserModel, IDType, AccessTokenModel], 89 | user_manager, 90 | ): 91 | authenticated_user = await database_strategy.read_token("TOKEN", user_manager) 92 | assert authenticated_user is None 93 | 94 | @pytest.mark.asyncio 95 | async def test_valid_token_not_existing_user( 96 | self, 97 | database_strategy: DatabaseStrategy[UserModel, IDType, AccessTokenModel], 98 | access_token_database: AccessTokenDatabaseMock, 99 | user_manager, 100 | ): 101 | await access_token_database.create( 102 | { 103 | "token": "TOKEN", 104 | "user_id": uuid.UUID("d35d213e-f3d8-4f08-954a-7e0d1bea286f"), 105 | } 106 | ) 107 | authenticated_user = await database_strategy.read_token("TOKEN", user_manager) 108 | assert authenticated_user is None 109 | 110 | @pytest.mark.asyncio 111 | async def test_valid_token( 112 | self, 113 | database_strategy: DatabaseStrategy[UserModel, IDType, AccessTokenModel], 114 | access_token_database: AccessTokenDatabaseMock, 115 | user_manager, 116 | user: UserModel, 117 | ): 118 | await access_token_database.create({"token": "TOKEN", "user_id": user.id}) 119 | authenticated_user = await database_strategy.read_token("TOKEN", user_manager) 120 | assert authenticated_user is not None 121 | assert authenticated_user.id == user.id 122 | 123 | 124 | @pytest.mark.authentication 125 | @pytest.mark.asyncio 126 | async def test_write_token( 127 | database_strategy: DatabaseStrategy[UserModel, IDType, AccessTokenModel], 128 | access_token_database: AccessTokenDatabaseMock, 129 | user: UserModel, 130 | ): 131 | token = await database_strategy.write_token(user) 132 | 133 | access_token = await access_token_database.get_by_token(token) 134 | assert access_token is not None 135 | assert access_token.user_id == user.id 136 | 137 | 138 | @pytest.mark.authentication 139 | @pytest.mark.asyncio 140 | async def test_destroy_token( 141 | database_strategy: DatabaseStrategy[UserModel, IDType, AccessTokenModel], 142 | access_token_database: AccessTokenDatabaseMock, 143 | user: UserModel, 144 | ): 145 | await access_token_database.create({"token": "TOKEN", "user_id": user.id}) 146 | 147 | await database_strategy.destroy_token("TOKEN", user) 148 | 149 | assert await access_token_database.get_by_token("TOKEN") is None 150 | -------------------------------------------------------------------------------- /tests/test_authentication_strategy_redis.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Optional 3 | 4 | import pytest 5 | 6 | from fastapi_users.authentication.strategy import RedisStrategy 7 | from tests.conftest import IDType, UserModel 8 | 9 | 10 | class RedisMock: 11 | store: dict[str, tuple[str, Optional[int]]] 12 | 13 | def __init__(self): 14 | self.store = {} 15 | 16 | async def get(self, key: str) -> Optional[str]: 17 | try: 18 | value, expiration = self.store[key] 19 | if expiration is not None and expiration < datetime.now().timestamp(): 20 | return None 21 | except KeyError: 22 | return None 23 | else: 24 | return value 25 | 26 | async def set(self, key: str, value: str, ex: Optional[int] = None): 27 | expiration = None 28 | if ex is not None: 29 | expiration = int(datetime.now().timestamp() + ex) 30 | self.store[key] = (value, expiration) 31 | 32 | async def delete(self, key: str): 33 | try: 34 | del self.store[key] 35 | except KeyError: 36 | pass 37 | 38 | 39 | @pytest.fixture 40 | def redis() -> RedisMock: 41 | return RedisMock() 42 | 43 | 44 | @pytest.fixture 45 | def redis_strategy(redis): 46 | return RedisStrategy(redis, 3600) 47 | 48 | 49 | @pytest.mark.authentication 50 | class TestReadToken: 51 | @pytest.mark.asyncio 52 | async def test_missing_token( 53 | self, redis_strategy: RedisStrategy[UserModel, IDType], user_manager 54 | ): 55 | authenticated_user = await redis_strategy.read_token(None, user_manager) 56 | assert authenticated_user is None 57 | 58 | @pytest.mark.asyncio 59 | async def test_invalid_token( 60 | self, redis_strategy: RedisStrategy[UserModel, IDType], user_manager 61 | ): 62 | authenticated_user = await redis_strategy.read_token("TOKEN", user_manager) 63 | assert authenticated_user is None 64 | 65 | @pytest.mark.asyncio 66 | async def test_valid_token_invalid_uuid( 67 | self, 68 | redis_strategy: RedisStrategy[UserModel, IDType], 69 | redis: RedisMock, 70 | user_manager, 71 | ): 72 | await redis.set(f"{redis_strategy.key_prefix}TOKEN", "bar") 73 | authenticated_user = await redis_strategy.read_token("TOKEN", user_manager) 74 | assert authenticated_user is None 75 | 76 | @pytest.mark.asyncio 77 | async def test_valid_token_not_existing_user( 78 | self, 79 | redis_strategy: RedisStrategy[UserModel, IDType], 80 | redis: RedisMock, 81 | user_manager, 82 | ): 83 | await redis.set( 84 | f"{redis_strategy.key_prefix}TOKEN", "d35d213e-f3d8-4f08-954a-7e0d1bea286f" 85 | ) 86 | authenticated_user = await redis_strategy.read_token("TOKEN", user_manager) 87 | assert authenticated_user is None 88 | 89 | @pytest.mark.asyncio 90 | async def test_valid_token( 91 | self, 92 | redis_strategy: RedisStrategy[UserModel, IDType], 93 | redis: RedisMock, 94 | user_manager, 95 | user, 96 | ): 97 | await redis.set(f"{redis_strategy.key_prefix}TOKEN", str(user.id)) 98 | authenticated_user = await redis_strategy.read_token("TOKEN", user_manager) 99 | assert authenticated_user is not None 100 | assert authenticated_user.id == user.id 101 | 102 | 103 | @pytest.mark.authentication 104 | @pytest.mark.asyncio 105 | async def test_write_token( 106 | redis_strategy: RedisStrategy[UserModel, IDType], redis: RedisMock, user 107 | ): 108 | token = await redis_strategy.write_token(user) 109 | 110 | value = await redis.get(f"{redis_strategy.key_prefix}{token}") 111 | assert value == str(user.id) 112 | 113 | 114 | @pytest.mark.authentication 115 | @pytest.mark.asyncio 116 | async def test_destroy_token( 117 | redis_strategy: RedisStrategy[UserModel, IDType], redis: RedisMock, user 118 | ): 119 | await redis.set(f"{redis_strategy.key_prefix}TOKEN", str(user.id)) 120 | 121 | await redis_strategy.destroy_token("TOKEN", user) 122 | 123 | assert await redis.get(f"{redis_strategy.key_prefix}TOKEN") is None 124 | -------------------------------------------------------------------------------- /tests/test_authentication_transport_bearer.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from fastapi import status 3 | from fastapi.responses import JSONResponse 4 | 5 | from fastapi_users.authentication.transport import ( 6 | BearerTransport, 7 | TransportLogoutNotSupportedError, 8 | ) 9 | from fastapi_users.authentication.transport.bearer import BearerResponse 10 | 11 | 12 | @pytest.fixture() 13 | def bearer_transport() -> BearerTransport: 14 | return BearerTransport(tokenUrl="/login") 15 | 16 | 17 | @pytest.mark.authentication 18 | @pytest.mark.asyncio 19 | async def test_get_login_response(bearer_transport: BearerTransport): 20 | response = await bearer_transport.get_login_response("TOKEN") 21 | 22 | assert isinstance(response, JSONResponse) 23 | assert response.body == b'{"access_token":"TOKEN","token_type":"bearer"}' 24 | 25 | 26 | @pytest.mark.authentication 27 | @pytest.mark.asyncio 28 | async def test_get_logout_response(bearer_transport: BearerTransport): 29 | with pytest.raises(TransportLogoutNotSupportedError): 30 | await bearer_transport.get_logout_response() 31 | 32 | 33 | @pytest.mark.authentication 34 | @pytest.mark.openapi 35 | def test_get_openapi_login_responses_success(bearer_transport: BearerTransport): 36 | openapi_responses = bearer_transport.get_openapi_login_responses_success() 37 | assert openapi_responses[status.HTTP_200_OK]["model"] == BearerResponse 38 | 39 | 40 | @pytest.mark.authentication 41 | @pytest.mark.openapi 42 | def test_get_openapi_logout_responses_success(bearer_transport: BearerTransport): 43 | openapi_responses = bearer_transport.get_openapi_logout_responses_success() 44 | assert openapi_responses == {} 45 | -------------------------------------------------------------------------------- /tests/test_authentication_transport_cookie.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import pytest 4 | from fastapi import Response, status 5 | 6 | from fastapi_users.authentication.transport import CookieTransport 7 | 8 | COOKIE_MAX_AGE = 3600 9 | COOKIE_NAME = "COOKIE_NAME" 10 | 11 | 12 | @pytest.fixture( 13 | params=[ 14 | ("/", None, True, True), 15 | ("/arthur", None, True, True), 16 | ("/", "camelot.bt", True, True), 17 | ("/", None, False, True), 18 | ("/", None, True, False), 19 | ] 20 | ) 21 | def cookie_transport(request) -> CookieTransport: 22 | path, domain, secure, httponly = request.param 23 | return CookieTransport( 24 | cookie_name=COOKIE_NAME, 25 | cookie_max_age=COOKIE_MAX_AGE, 26 | cookie_path=path, 27 | cookie_domain=domain, 28 | cookie_secure=secure, 29 | cookie_httponly=httponly, 30 | ) 31 | 32 | 33 | @pytest.mark.authentication 34 | @pytest.mark.asyncio 35 | async def test_get_login_response(cookie_transport: CookieTransport): 36 | path = cookie_transport.cookie_path 37 | domain = cookie_transport.cookie_domain 38 | secure = cookie_transport.cookie_secure 39 | httponly = cookie_transport.cookie_httponly 40 | 41 | response = await cookie_transport.get_login_response("TOKEN") 42 | 43 | assert isinstance(response, Response) 44 | assert response.status_code == status.HTTP_204_NO_CONTENT 45 | 46 | cookies = [header for header in response.raw_headers if header[0] == b"set-cookie"] 47 | assert len(cookies) == 1 48 | 49 | cookie = cookies[0][1].decode("latin-1") 50 | 51 | assert f"Max-Age={COOKIE_MAX_AGE}" in cookie 52 | assert f"Path={path}" in cookie 53 | 54 | if domain: 55 | assert f"Domain={domain}" in cookie 56 | else: 57 | assert "Domain=" not in cookie 58 | 59 | if secure: 60 | assert "Secure" in cookie 61 | else: 62 | assert "Secure" not in cookie 63 | 64 | if httponly: 65 | assert "HttpOnly" in cookie 66 | else: 67 | assert "HttpOnly" not in cookie 68 | 69 | cookie_name_value = re.match(r"^(\w+)=([^;]+);", cookie) 70 | assert cookie_name_value is not None 71 | 72 | cookie_name = cookie_name_value[1] 73 | assert cookie_name == COOKIE_NAME 74 | 75 | cookie_value = cookie_name_value[2] 76 | assert cookie_value == "TOKEN" 77 | 78 | 79 | @pytest.mark.authentication 80 | @pytest.mark.asyncio 81 | async def test_get_logout_response(cookie_transport: CookieTransport): 82 | response = await cookie_transport.get_logout_response() 83 | 84 | assert isinstance(response, Response) 85 | assert response.status_code == status.HTTP_204_NO_CONTENT 86 | 87 | cookies = [header for header in response.raw_headers if header[0] == b"set-cookie"] 88 | assert len(cookies) == 1 89 | 90 | cookie = cookies[0][1].decode("latin-1") 91 | 92 | assert "Max-Age=0" in cookie 93 | 94 | 95 | @pytest.mark.authentication 96 | @pytest.mark.openapi 97 | def test_get_openapi_login_responses_success(cookie_transport: CookieTransport): 98 | assert cookie_transport.get_openapi_login_responses_success() == { 99 | status.HTTP_204_NO_CONTENT: {"model": None} 100 | } 101 | 102 | 103 | @pytest.mark.authentication 104 | @pytest.mark.openapi 105 | def test_get_openapi_logout_responses_success(cookie_transport: CookieTransport): 106 | assert cookie_transport.get_openapi_logout_responses_success() == { 107 | status.HTTP_204_NO_CONTENT: {"model": None} 108 | } 109 | -------------------------------------------------------------------------------- /tests/test_db_base.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | import pytest 4 | 5 | from fastapi_users.db import BaseUserDatabase 6 | from tests.conftest import IDType, OAuthAccountModel, UserModel 7 | 8 | 9 | @pytest.mark.asyncio 10 | @pytest.mark.db 11 | async def test_not_implemented_methods( 12 | user: UserModel, oauth_account1: OAuthAccountModel 13 | ): 14 | base_user_db = BaseUserDatabase[UserModel, IDType]() 15 | 16 | with pytest.raises(NotImplementedError): 17 | await base_user_db.get(uuid.uuid4()) 18 | 19 | with pytest.raises(NotImplementedError): 20 | await base_user_db.get_by_email("lancelot@camelot.bt") 21 | 22 | with pytest.raises(NotImplementedError): 23 | await base_user_db.get_by_oauth_account("google", "user_oauth1") 24 | 25 | with pytest.raises(NotImplementedError): 26 | await base_user_db.create({}) 27 | 28 | with pytest.raises(NotImplementedError): 29 | await base_user_db.update(user, {}) 30 | 31 | with pytest.raises(NotImplementedError): 32 | await base_user_db.delete(user) 33 | 34 | with pytest.raises(NotImplementedError): 35 | await base_user_db.add_oauth_account(user, {}) 36 | 37 | with pytest.raises(NotImplementedError): 38 | await base_user_db.update_oauth_account(user, oauth_account1, {}) 39 | -------------------------------------------------------------------------------- /tests/test_jwt.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from fastapi_users.jwt import SecretType, decode_jwt, generate_jwt 4 | 5 | 6 | @pytest.mark.jwt 7 | def test_generate_decode_jwt(secret: SecretType): 8 | audience = "TEST_AUDIENCE" 9 | data = {"foo": "bar", "aud": audience} 10 | 11 | jwt = generate_jwt(data, secret, 3600) 12 | decoded = decode_jwt(jwt, secret, [audience]) 13 | 14 | assert decoded["foo"] == "bar" 15 | assert decoded["aud"] == audience 16 | -------------------------------------------------------------------------------- /tests/test_openapi.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | import pytest 3 | import pytest_asyncio 4 | from fastapi import FastAPI, status 5 | 6 | from fastapi_users.fastapi_users import FastAPIUsers 7 | from tests.conftest import IDType, User, UserCreate, UserModel, UserUpdate 8 | 9 | 10 | @pytest.fixture 11 | def fastapi_users(get_user_manager, mock_authentication) -> FastAPIUsers: 12 | return FastAPIUsers[UserModel, IDType](get_user_manager, [mock_authentication]) 13 | 14 | 15 | @pytest.fixture 16 | def test_app( 17 | fastapi_users: FastAPIUsers, secret, mock_authentication, oauth_client 18 | ) -> FastAPI: 19 | app = FastAPI() 20 | app.include_router(fastapi_users.get_register_router(User, UserCreate)) 21 | app.include_router(fastapi_users.get_reset_password_router()) 22 | app.include_router(fastapi_users.get_auth_router(mock_authentication)) 23 | app.include_router( 24 | fastapi_users.get_oauth_router(oauth_client, mock_authentication, secret) 25 | ) 26 | app.include_router(fastapi_users.get_users_router(User, UserUpdate)) 27 | app.include_router(fastapi_users.get_verify_router(User)) 28 | 29 | return app 30 | 31 | 32 | @pytest_asyncio.fixture 33 | async def test_app_client(test_app, get_test_client): 34 | async for client in get_test_client(test_app): 35 | yield client 36 | 37 | 38 | @pytest.fixture 39 | def openapi_dict(test_app: FastAPI): 40 | return test_app.openapi() 41 | 42 | 43 | @pytest.mark.asyncio 44 | @pytest.mark.openapi 45 | async def test_openapi_route(test_app_client: httpx.AsyncClient): 46 | response = await test_app_client.get("/openapi.json") 47 | assert response.status_code == status.HTTP_200_OK 48 | 49 | 50 | class TestReset: 51 | def test_reset_password_status_codes(self, openapi_dict): 52 | route = openapi_dict["paths"]["/reset-password"]["post"] 53 | assert list(route["responses"].keys()) == ["200", "400", "422"] 54 | 55 | def test_forgot_password_status_codes(self, openapi_dict): 56 | route = openapi_dict["paths"]["/forgot-password"]["post"] 57 | assert list(route["responses"].keys()) == ["202", "422"] 58 | 59 | 60 | class TestUsers: 61 | def test_patch_id_status_codes(self, openapi_dict): 62 | route = openapi_dict["paths"]["/{id}"]["patch"] 63 | assert list(route["responses"].keys()) == [ 64 | "200", 65 | "401", 66 | "403", 67 | "404", 68 | "400", 69 | "422", 70 | ] 71 | 72 | def test_delete_id_status_codes(self, openapi_dict): 73 | route = openapi_dict["paths"]["/{id}"]["delete"] 74 | assert list(route["responses"].keys()) == ["204", "401", "403", "404", "422"] 75 | 76 | def test_get_id_status_codes(self, openapi_dict): 77 | route = openapi_dict["paths"]["/{id}"]["get"] 78 | assert list(route["responses"].keys()) == ["200", "401", "403", "404", "422"] 79 | 80 | def test_patch_me_status_codes(self, openapi_dict): 81 | route = openapi_dict["paths"]["/me"]["patch"] 82 | assert list(route["responses"].keys()) == ["200", "401", "400", "422"] 83 | 84 | def test_get_me_status_codes(self, openapi_dict): 85 | route = openapi_dict["paths"]["/me"]["get"] 86 | assert list(route["responses"].keys()) == ["200", "401"] 87 | 88 | 89 | class TestRegister: 90 | def test_register_status_codes(self, openapi_dict): 91 | route = openapi_dict["paths"]["/register"]["post"] 92 | assert list(route["responses"].keys()) == ["201", "400", "422"] 93 | 94 | 95 | class TestVerify: 96 | def test_verify_status_codes(self, openapi_dict): 97 | route = openapi_dict["paths"]["/verify"]["post"] 98 | assert list(route["responses"].keys()) == ["200", "400", "422"] 99 | 100 | def test_request_verify_status_codes(self, openapi_dict): 101 | route = openapi_dict["paths"]["/request-verify-token"]["post"] 102 | assert list(route["responses"].keys()) == ["202", "422"] 103 | 104 | 105 | class TestOAuth2: 106 | def test_oauth_authorize_status_codes(self, openapi_dict): 107 | route = openapi_dict["paths"]["/authorize"]["get"] 108 | assert list(route["responses"].keys()) == ["200", "422"] 109 | 110 | def test_oauth_callback_status_codes(self, openapi_dict): 111 | route = openapi_dict["paths"]["/callback"]["get"] 112 | assert list(route["responses"].keys()) == ["200", "400", "422"] 113 | -------------------------------------------------------------------------------- /tests/test_router_register.py: -------------------------------------------------------------------------------- 1 | from collections.abc import AsyncGenerator 2 | from typing import Any, cast 3 | 4 | import httpx 5 | import pytest 6 | import pytest_asyncio 7 | from fastapi import FastAPI, status 8 | 9 | from fastapi_users.router import ErrorCode, get_register_router 10 | from tests.conftest import User, UserCreate 11 | 12 | 13 | @pytest_asyncio.fixture 14 | async def test_app_client( 15 | get_user_manager, get_test_client 16 | ) -> AsyncGenerator[httpx.AsyncClient, None]: 17 | register_router = get_register_router( 18 | get_user_manager, 19 | User, 20 | UserCreate, 21 | ) 22 | 23 | app = FastAPI() 24 | app.include_router(register_router) 25 | 26 | async for client in get_test_client(app): 27 | yield client 28 | 29 | 30 | @pytest.mark.router 31 | @pytest.mark.asyncio 32 | class TestRegister: 33 | async def test_empty_body(self, test_app_client: httpx.AsyncClient): 34 | response = await test_app_client.post("/register", json={}) 35 | assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY 36 | 37 | async def test_missing_email(self, test_app_client: httpx.AsyncClient): 38 | json = {"password": "guinevere"} 39 | response = await test_app_client.post("/register", json=json) 40 | assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY 41 | 42 | async def test_missing_password(self, test_app_client: httpx.AsyncClient): 43 | json = {"email": "king.arthur@camelot.bt"} 44 | response = await test_app_client.post("/register", json=json) 45 | assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY 46 | 47 | async def test_wrong_email(self, test_app_client: httpx.AsyncClient): 48 | json = {"email": "king.arthur", "password": "guinevere"} 49 | response = await test_app_client.post("/register", json=json) 50 | assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY 51 | 52 | async def test_invalid_password(self, test_app_client: httpx.AsyncClient): 53 | json = {"email": "king.arthur@camelot.bt", "password": "g"} 54 | response = await test_app_client.post("/register", json=json) 55 | assert response.status_code == status.HTTP_400_BAD_REQUEST 56 | data = cast(dict[str, Any], response.json()) 57 | assert data["detail"] == { 58 | "code": ErrorCode.REGISTER_INVALID_PASSWORD, 59 | "reason": "Password should be at least 3 characters", 60 | } 61 | 62 | @pytest.mark.parametrize( 63 | "email", ["king.arthur@camelot.bt", "King.Arthur@camelot.bt"] 64 | ) 65 | async def test_existing_user(self, email, test_app_client: httpx.AsyncClient): 66 | json = {"email": email, "password": "guinevere"} 67 | response = await test_app_client.post("/register", json=json) 68 | assert response.status_code == status.HTTP_400_BAD_REQUEST 69 | data = cast(dict[str, Any], response.json()) 70 | assert data["detail"] == ErrorCode.REGISTER_USER_ALREADY_EXISTS 71 | 72 | @pytest.mark.parametrize("email", ["lancelot@camelot.bt", "Lancelot@camelot.bt"]) 73 | async def test_valid_body(self, email, test_app_client: httpx.AsyncClient): 74 | json = {"email": email, "password": "guinevere"} 75 | response = await test_app_client.post("/register", json=json) 76 | assert response.status_code == status.HTTP_201_CREATED 77 | 78 | data = cast(dict[str, Any], response.json()) 79 | assert "hashed_password" not in data 80 | assert "password" not in data 81 | assert data["id"] is not None 82 | 83 | async def test_valid_body_is_superuser(self, test_app_client: httpx.AsyncClient): 84 | json = { 85 | "email": "lancelot@camelot.bt", 86 | "password": "guinevere", 87 | "is_superuser": True, 88 | } 89 | response = await test_app_client.post("/register", json=json) 90 | assert response.status_code == status.HTTP_201_CREATED 91 | 92 | data = cast(dict[str, Any], response.json()) 93 | assert data["is_superuser"] is False 94 | 95 | async def test_valid_body_is_active(self, test_app_client: httpx.AsyncClient): 96 | json = { 97 | "email": "lancelot@camelot.bt", 98 | "password": "guinevere", 99 | "is_active": False, 100 | } 101 | response = await test_app_client.post("/register", json=json) 102 | assert response.status_code == status.HTTP_201_CREATED 103 | 104 | data = cast(dict[str, Any], response.json()) 105 | assert data["is_active"] is True 106 | 107 | 108 | @pytest.mark.asyncio 109 | async def test_register_namespace(get_user_manager): 110 | app = FastAPI() 111 | app.include_router( 112 | get_register_router( 113 | get_user_manager, 114 | User, 115 | UserCreate, 116 | ) 117 | ) 118 | assert app.url_path_for("register:register") == "/register" 119 | -------------------------------------------------------------------------------- /tests/test_router_reset.py: -------------------------------------------------------------------------------- 1 | from collections.abc import AsyncGenerator 2 | from typing import Any, cast 3 | 4 | import httpx 5 | import pytest 6 | import pytest_asyncio 7 | from fastapi import FastAPI, status 8 | 9 | from fastapi_users.exceptions import ( 10 | InvalidPasswordException, 11 | InvalidResetPasswordToken, 12 | UserInactive, 13 | UserNotExists, 14 | ) 15 | from fastapi_users.router import ErrorCode, get_reset_password_router 16 | from tests.conftest import AsyncMethodMocker, UserManagerMock 17 | 18 | 19 | @pytest_asyncio.fixture 20 | async def test_app_client( 21 | get_user_manager, get_test_client 22 | ) -> AsyncGenerator[httpx.AsyncClient, None]: 23 | reset_router = get_reset_password_router(get_user_manager) 24 | 25 | app = FastAPI() 26 | app.include_router(reset_router) 27 | 28 | async for client in get_test_client(app): 29 | yield client 30 | 31 | 32 | @pytest.mark.router 33 | @pytest.mark.asyncio 34 | class TestForgotPassword: 35 | async def test_empty_body( 36 | self, test_app_client: httpx.AsyncClient, user_manager: UserManagerMock 37 | ): 38 | response = await test_app_client.post("/forgot-password", json={}) 39 | assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY 40 | assert user_manager.forgot_password.called is False 41 | 42 | async def test_not_existing_user( 43 | self, test_app_client: httpx.AsyncClient, user_manager: UserManagerMock 44 | ): 45 | user_manager.get_by_email.side_effect = UserNotExists() 46 | json = {"email": "lancelot@camelot.bt"} 47 | response = await test_app_client.post("/forgot-password", json=json) 48 | assert response.status_code == status.HTTP_202_ACCEPTED 49 | assert user_manager.forgot_password.called is False 50 | 51 | async def test_inactive_user( 52 | self, test_app_client: httpx.AsyncClient, user_manager: UserManagerMock 53 | ): 54 | user_manager.forgot_password.side_effect = UserInactive() 55 | json = {"email": "percival@camelot.bt"} 56 | response = await test_app_client.post("/forgot-password", json=json) 57 | assert response.status_code == status.HTTP_202_ACCEPTED 58 | 59 | async def test_existing_user( 60 | self, 61 | async_method_mocker: AsyncMethodMocker, 62 | test_app_client: httpx.AsyncClient, 63 | user_manager: UserManagerMock, 64 | ): 65 | async_method_mocker(user_manager, "forgot_password", return_value=None) 66 | json = {"email": "king.arthur@camelot.bt"} 67 | response = await test_app_client.post("/forgot-password", json=json) 68 | assert response.status_code == status.HTTP_202_ACCEPTED 69 | 70 | 71 | @pytest.mark.router 72 | @pytest.mark.asyncio 73 | class TestResetPassword: 74 | async def test_empty_body( 75 | self, 76 | test_app_client: httpx.AsyncClient, 77 | user_manager: UserManagerMock, 78 | ): 79 | response = await test_app_client.post("/reset-password", json={}) 80 | assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY 81 | assert user_manager.reset_password.called is False 82 | 83 | async def test_missing_token( 84 | self, test_app_client: httpx.AsyncClient, user_manager: UserManagerMock 85 | ): 86 | json = {"password": "guinevere"} 87 | response = await test_app_client.post("/reset-password", json=json) 88 | assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY 89 | assert user_manager.reset_password.called is False 90 | 91 | async def test_missing_password( 92 | self, 93 | test_app_client: httpx.AsyncClient, 94 | user_manager: UserManagerMock, 95 | ): 96 | json = {"token": "foo"} 97 | response = await test_app_client.post("/reset-password", json=json) 98 | assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY 99 | assert user_manager.reset_password.called is False 100 | 101 | async def test_invalid_token( 102 | self, 103 | test_app_client: httpx.AsyncClient, 104 | user_manager: UserManagerMock, 105 | ): 106 | user_manager.reset_password.side_effect = InvalidResetPasswordToken() 107 | json = {"token": "foo", "password": "guinevere"} 108 | response = await test_app_client.post("/reset-password", json=json) 109 | assert response.status_code == status.HTTP_400_BAD_REQUEST 110 | data = cast(dict[str, Any], response.json()) 111 | assert data["detail"] == ErrorCode.RESET_PASSWORD_BAD_TOKEN 112 | 113 | async def test_inactive_user( 114 | self, 115 | test_app_client: httpx.AsyncClient, 116 | user_manager: UserManagerMock, 117 | ): 118 | user_manager.reset_password.side_effect = UserInactive() 119 | json = {"token": "foo", "password": "guinevere"} 120 | response = await test_app_client.post("/reset-password", json=json) 121 | assert response.status_code == status.HTTP_400_BAD_REQUEST 122 | data = cast(dict[str, Any], response.json()) 123 | assert data["detail"] == ErrorCode.RESET_PASSWORD_BAD_TOKEN 124 | 125 | async def test_invalid_password( 126 | self, 127 | test_app_client: httpx.AsyncClient, 128 | user_manager: UserManagerMock, 129 | ): 130 | user_manager.reset_password.side_effect = InvalidPasswordException( 131 | reason="Invalid" 132 | ) 133 | json = {"token": "foo", "password": "guinevere"} 134 | response = await test_app_client.post("/reset-password", json=json) 135 | assert response.status_code == status.HTTP_400_BAD_REQUEST 136 | data = cast(dict[str, Any], response.json()) 137 | assert data["detail"] == { 138 | "code": ErrorCode.RESET_PASSWORD_INVALID_PASSWORD, 139 | "reason": "Invalid", 140 | } 141 | 142 | async def test_valid_user_password( 143 | self, 144 | async_method_mocker: AsyncMethodMocker, 145 | test_app_client: httpx.AsyncClient, 146 | user_manager: UserManagerMock, 147 | ): 148 | async_method_mocker(user_manager, "reset_password", return_value=None) 149 | json = {"token": "foo", "password": "guinevere"} 150 | response = await test_app_client.post("/reset-password", json=json) 151 | assert response.status_code == status.HTTP_200_OK 152 | 153 | 154 | @pytest.mark.asyncio 155 | async def test_forgot_password_namespace(get_user_manager): 156 | app = FastAPI() 157 | app.include_router(get_reset_password_router(get_user_manager)) 158 | assert app.url_path_for("reset:forgot_password") == "/forgot-password" 159 | 160 | 161 | @pytest.mark.asyncio 162 | async def test_reset_password_namespace(get_user_manager): 163 | app = FastAPI() 164 | app.include_router(get_reset_password_router(get_user_manager)) 165 | assert app.url_path_for("reset:reset_password") == "/reset-password" 166 | --------------------------------------------------------------------------------