├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── examples └── simple │ ├── README.md │ ├── docker-compose.yml │ ├── fastapi │ ├── .dockerignore │ ├── .gitignore │ ├── Dockerfile │ ├── main.py │ └── requirements.txt │ └── nextjs │ ├── .dockerignore │ ├── .eslintrc.json │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── next-env.d.ts │ ├── next.config.js │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── public │ ├── favicon.ico │ ├── next.svg │ └── vercel.svg │ ├── src │ ├── app │ │ └── api │ │ │ └── auth │ │ │ └── [...nextauth] │ │ │ └── route.ts │ ├── auth.ts │ ├── middleware.ts │ ├── pages │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ ├── api │ │ │ └── hello.ts │ │ └── index.tsx │ └── styles │ │ └── globals.css │ ├── tailwind.config.js │ └── tsconfig.json ├── pyproject.toml ├── src └── fastapi_nextauth_jwt │ ├── __init__.py │ ├── cookies.py │ ├── csrf.py │ ├── exceptions.py │ ├── fastapi_nextauth_jwt.py │ └── operations.py └── tests ├── fastapi ├── test_v4.py ├── test_v5.py ├── v4.py └── v5.py └── unit ├── test_cookies.py ├── test_csrf.py ├── test_operations.py └── test_secret.py /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: 'Run tests and publish fastapi-nextauth-jwt to pypi' 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | 11 | - uses: actions/setup-python@v4 12 | with: 13 | python-version: "3.11" 14 | 15 | - name: Install dependencies 16 | run: | 17 | python -m pip install --upgrade pip 18 | pip install . ".[test]" 19 | 20 | - name: run pytest 21 | run: | 22 | pytest --junitxml=pytest.xml --cov=fastapi_nextauth_jwt tests/ | tee pytest-coverage.txt 23 | 24 | 25 | publish: 26 | runs-on: ubuntu-latest 27 | permissions: 28 | id-token: write 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v3 32 | 33 | - uses: actions/setup-python@v4 34 | with: 35 | python-version: '3.11' 36 | 37 | - name: Setup flit 38 | run: python3 -m pip install flit 39 | 40 | - name: Build package 41 | run: flit build 42 | 43 | - name: Publish package distributions to PyPI 44 | uses: pypa/gh-action-pypi-publish@release/v1 45 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: 'Run pytest' 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - '*' 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | pull-requests: write 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - uses: actions/setup-python@v4 17 | with: 18 | python-version: "3.11" 19 | 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install . ".[test]" 24 | 25 | - name: run pytest 26 | run: | 27 | pytest --junitxml=pytest.xml --cov=fastapi_nextauth_jwt tests/ | tee pytest-coverage.txt 28 | 29 | - name: Pytest comment 30 | uses: MishaKav/pytest-coverage-comment@main 31 | with: 32 | pytest-coverage-path: ./pytest-coverage.txt 33 | junitxml-path: ./pytest.xml -------------------------------------------------------------------------------- /.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 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | .idea/ 161 | /pytest.xml 162 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Tom Catshoek 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fastapi-nextauth-jwt 2 | 3 | [![PyPI version](https://badge.fury.io/py/fastapi-nextauth-jwt.svg)](https://badge.fury.io/py/fastapi-nextauth-jwt) 4 | [![PyPI Downloads](https://img.shields.io/pypi/dm/fastapi-nextauth-jwt)](https://pypi.org/project/fastapi-nextauth-jwt/) 5 | [![License](https://img.shields.io/pypi/l/fastapi-nextauth-jwt)](https://github.com/yourusername/fastapi-nextauth-jwt/blob/main/LICENSE) 6 | [![Contributors](https://img.shields.io/github/contributors/TCatshoek/fastapi-nextauth-jwt)](https://github.com/TCatshoek/fastapi-nextauth-jwt/graphs/contributors) 7 | 8 | This project provides a FastAPI dependency for decrypting and validating JWTs generated by Auth.js. It is designed to facilitate the integration of a FastAPI backend with Next.js and NextAuth/Auth.js on the frontend. 9 | 10 | > [!NOTE] 11 | > Using Auth.js with frameworks other than Next.js may work but has not been tested 12 | 13 | ## Features 14 | 15 | - **JWT Decryption & Validation**: Seamlessly decrypt and validate JWTs generated by Auth.js 16 | - **CSRF Protection**: Built-in Auth.js-compatible CSRF protection with configurable HTTP methods 17 | - **Flexible Configuration**: Extensive customization options for encryption algorithms, cookie names, and security settings 18 | - **NextAuth.js v4 Compatibility**: Includes a compatibility layer for NextAuth.js v4 through `NextAuthJWTv4` 19 | 20 | ## Installation 21 | 22 | ```shell 23 | pip install fastapi-nextauth-jwt 24 | ``` 25 | 26 | ## Basic Usage 27 | 28 | ```python 29 | from typing import Annotated 30 | from fastapi import FastAPI, Depends 31 | from fastapi_nextauth_jwt import NextAuthJWT 32 | 33 | app = FastAPI() 34 | 35 | JWT = NextAuthJWT( 36 | secret="y0uR_SuP3r_s3cr37_$3cr3t", # Leave this out to automatically read the AUTH_SECRET env var 37 | ) 38 | 39 | @app.get("/") 40 | async def return_jwt(jwt: Annotated[dict, Depends(JWT)]): 41 | return jwt 42 | ``` 43 | 44 | ## Configuration Options 45 | 46 | ### Essential Settings 47 | 48 | - **secret** (str): The secret key used for JWT operations. Should match `AUTH_SECRET` in your Next.js app. Leave this out to automatically read the `AUTH_SECRET` environment variable. 49 | ```python 50 | JWT = NextAuthJWT(secret=os.getenv("YOUR_SECRET_ENV_VAR_NAME"))) 51 | ``` 52 | 53 | ### Additional Options 54 | If your auth.js settings are left at their defaults, you shouldn't need to touch these. 55 | 56 | #### Security Options 57 | 58 | - **csrf_prevention_enabled** (bool): Enable CSRF protection 59 | - Defaults to `False` in development (`ENV=dev`), `True` otherwise 60 | 61 | - **csrf_methods** (Set[str]): HTTP methods requiring CSRF protection 62 | - Default: `{'POST', 'PUT', 'PATCH', 'DELETE'}` 63 | 64 | #### Cookie Configuration 65 | 66 | - **secure_cookie** (bool): Enable secure cookie attributes 67 | - Default: `True` (when `AUTH_URL` starts with https) 68 | 69 | - **cookie_name** (str): Session token cookie name 70 | - Default: `"__Secure-authjs.session-token"` (when secure_cookie is True) 71 | - Default: `"authjs.session-token"` (when secure_cookie is False) 72 | 73 | - **csrf_cookie_name** (str): CSRF token cookie name 74 | - Default: `"__Host-authjs.csrf-token"` (when secure_cookie is True) 75 | - Default: `"authjs.csrf-token"` (when secure_cookie is False) 76 | 77 | 78 | #### Advanced Options 79 | 80 | - **encryption_algorithm** (str): JWT encryption algorithm 81 | - Supported: `"A256CBC-HS512"` (default), `"A256GCM"` 82 | 83 | - **check_expiry** (bool): Enable JWT expiration validation 84 | - Default: `True` 85 | 86 | ## NextAuth.js v4 Compatibility 87 | 88 | For NextAuth.js v4 applications, use the `NextAuthJWTv4` class: 89 | 90 | ```python 91 | from fastapi_nextauth_jwt import NextAuthJWTv4 92 | 93 | JWT = NextAuthJWTv4( 94 | secret=os.getenv("AUTH_SECRET") 95 | ) 96 | ``` 97 | 98 | This provides compatibility with the v4 token format and default settings 99 | 100 | ## Best Practices 101 | 102 | 1. **Environment Variables**: Always use environment variables for sensitive values: 103 | ```python 104 | JWT = NextAuthJWT( 105 | secret=os.getenv("AUTH_SECRET"), 106 | ) 107 | ``` 108 | 109 | 2. **HTTPS in Production**: Ensure `AUTH_URL` starts with `https://` in production to enable secure cookies 110 | 111 | 3. **CSRF Protection**: Keep CSRF protection enabled in production environments 112 | 113 | ## Examples 114 | 115 | A [simple example](https://github.com/TCatshoek/fastapi-nextauth-jwt/tree/main/examples/simple) is available in the examples folder. It demonstrates: 116 | - Using Next.js URL rewrites to route requests to FastAPI 117 | - Basic JWT validation setup 118 | - CSRF protection configuration 119 | 120 | You can also place both the backend and frontend behind a reverse proxy like nginx, as long as the auth.js cookies reach FastAPI. 121 | 122 | ## Environment Variables 123 | 124 | - `AUTH_SECRET`: The secret key used for JWT operations (required) 125 | - `AUTH_URL`: The URL of your application (affects secure cookie settings) 126 | - `ENV`: Set to `"dev"` to disable CSRF protection in development 127 | -------------------------------------------------------------------------------- /examples/simple/README.md: -------------------------------------------------------------------------------- 1 | # Simple Example 2 | 3 | This example shows off a very basic setup using Next.js, NextAuth, 4 | and a FastAPI backend. 5 | 6 | It uses [Next.js url rewriting](https://nextjs.org/docs/pages/api-reference/next-config-js/rewrites) 7 | to direct requests starting with `/fastapi` to the FastAPI backend. 8 | 9 | ## Setup 10 | 11 | ### With docker compose: 12 | ```shell 13 | docker-compose up 14 | ``` 15 | 16 | ### Without docker: 17 | 18 | #### FastAPI 19 | ```shell 20 | cd fastapi 21 | python -m venv venv 22 | source venv/bin/activate 23 | pip install -r requirements.txt 24 | uvicorn main:app --reload 25 | ``` 26 | 27 | #### Next.js 28 | ```shell 29 | cd nextjs 30 | npm install 31 | npm run dev 32 | ``` 33 | 34 | ## Usage 35 | Open the Next.js page in your browser. It should prompt you to log in. 36 | It is set up in such a way that it accepts any credentials, so no need 37 | to create an account first. 38 | 39 | Then, click the blue button and see the response from FastAPI! -------------------------------------------------------------------------------- /examples/simple/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | fastapi: 3 | build: 4 | context: ../../ 5 | dockerfile: examples/simple/fastapi/Dockerfile 6 | network_mode: "host" 7 | volumes: 8 | - ./fastapi:/code 9 | nextjs: 10 | build: ./nextjs 11 | network_mode: "host" 12 | volumes: 13 | - ./nextjs:/app 14 | - nodemodules:/app/node_modules 15 | 16 | volumes: 17 | nodemodules: {} -------------------------------------------------------------------------------- /examples/simple/fastapi/.dockerignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | venv/ -------------------------------------------------------------------------------- /examples/simple/fastapi/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ -------------------------------------------------------------------------------- /examples/simple/fastapi/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11 2 | WORKDIR /code 3 | COPY examples/simple/fastapi/requirements.txt /code/requirements.txt 4 | RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt 5 | COPY . /fastapi-nextauth-jwt 6 | RUN pip install --no-cache-dir /fastapi-nextauth-jwt 7 | COPY . /code 8 | 9 | CMD ["uvicorn", "main:app", "--reload"] 10 | -------------------------------------------------------------------------------- /examples/simple/fastapi/main.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | from fastapi import FastAPI, Depends 3 | from fastapi_nextauth_jwt import NextAuthJWT 4 | 5 | app = FastAPI() 6 | 7 | JWT = NextAuthJWT( 8 | secret="y0uR_SuP3r_s3cr37_$3cr3t", 9 | ) 10 | 11 | 12 | @app.get("/") 13 | async def return_jwt(jwt: Annotated[dict, Depends(JWT)]): 14 | return {"message": f"Hi {jwt['name']}. Greetings from fastapi!"} 15 | 16 | # For CSRF protection testing 17 | @app.post("/") 18 | async def return_jwt(jwt: Annotated[dict, Depends(JWT)]): 19 | return {"message": f"Hi {jwt['name']}. Greetings from fastapi!"} -------------------------------------------------------------------------------- /examples/simple/fastapi/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.100.0 2 | uvicorn==0.23.0 3 | -------------------------------------------------------------------------------- /examples/simple/nextjs/.dockerignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .next/ 3 | node_modules/ -------------------------------------------------------------------------------- /examples/simple/nextjs/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/simple/nextjs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | .idea 38 | -------------------------------------------------------------------------------- /examples/simple/nextjs/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | RUN apk add --no-cache libc6-compat 4 | WORKDIR /app 5 | 6 | # Install dependencies 7 | COPY package.json package-lock.json* ./ 8 | RUN npm ci 9 | 10 | COPY . ./ 11 | 12 | EXPOSE 3000 13 | 14 | ENV NEXT_TELEMETRY_DISABLED 1 15 | ENV PORT 3000 16 | ENV HOSTNAME localhost 17 | 18 | CMD ["npm", "run", "dev"] 19 | -------------------------------------------------------------------------------- /examples/simple/nextjs/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 18 | 19 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 20 | 21 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 22 | 23 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 24 | 25 | ## Learn More 26 | 27 | To learn more about Next.js, take a look at the following resources: 28 | 29 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 30 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 31 | 32 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 33 | 34 | ## Deploy on Vercel 35 | 36 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 37 | 38 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 39 | -------------------------------------------------------------------------------- /examples/simple/nextjs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /examples/simple/nextjs/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | async rewrites() { 5 | return [ 6 | { 7 | source: '/fastapi', 8 | destination: 'http://127.0.0.1:8000', 9 | }, 10 | ] 11 | }, 12 | } 13 | 14 | module.exports = nextConfig 15 | -------------------------------------------------------------------------------- /examples/simple/nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@types/node": "^20.12.7", 13 | "@types/react": "^18.3.0", 14 | "@types/react-dom": "^18.3.0", 15 | "autoprefixer": "^10.4.19", 16 | "eslint": "^8.57.0", 17 | "eslint-config-next": "^14.2.3", 18 | "next": "^14.2.3", 19 | "next-auth": "^5.0.0-beta.17", 20 | "postcss": "^8.4.38", 21 | "react": "^18.3.0", 22 | "react-dom": "^18.3.0", 23 | "tailwindcss": "^3.4.3", 24 | "typescript": "^5.4.5" 25 | }, 26 | "devDependencies": { 27 | "prettier": "^3.2.5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/simple/nextjs/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /examples/simple/nextjs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TCatshoek/fastapi-nextauth-jwt/5ad8a90eec0fc73866f0bc81950ce1801d639be0/examples/simple/nextjs/public/favicon.ico -------------------------------------------------------------------------------- /examples/simple/nextjs/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/simple/nextjs/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/simple/nextjs/src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from "@/auth" 2 | export const { GET, POST } = handlers -------------------------------------------------------------------------------- /examples/simple/nextjs/src/auth.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth" 2 | import CredentialsProvider from "next-auth/providers/credentials" 3 | 4 | export const { auth, handlers, signIn, signOut } = NextAuth({ 5 | providers: [ 6 | CredentialsProvider({ 7 | name: 'Credentials', 8 | credentials: { 9 | username: {label: "Username", type: "text", placeholder: "jsmith"}, 10 | password: {label: "Password", type: "password"} 11 | }, 12 | async authorize(credentials, req) { 13 | if (credentials) { 14 | return {id: "1", name: credentials.username, email: "test@test.nl"} 15 | } 16 | // Return null if user data could not be retrieved 17 | return null 18 | } 19 | }) 20 | ], 21 | secret: "y0uR_SuP3r_s3cr37_$3cr3t", 22 | }) 23 | -------------------------------------------------------------------------------- /examples/simple/nextjs/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "./auth" 2 | 3 | export default auth((req) => { 4 | // req.auth 5 | }) -------------------------------------------------------------------------------- /examples/simple/nextjs/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.css' 2 | import type {AppProps} from 'next/app' 3 | import {SessionProvider} from "next-auth/react" 4 | import {Session} from "next-auth"; 5 | 6 | export default function App({Component, pageProps}: AppProps<{ 7 | session: Session; 8 | }>) { 9 | return ( 10 | 11 | 12 | 13 | ) 14 | } -------------------------------------------------------------------------------- /examples/simple/nextjs/src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import {Head, Html, Main, NextScript} from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /examples/simple/nextjs/src/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type {NextApiRequest, NextApiResponse} from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({name: 'John Doe'}) 13 | } 14 | -------------------------------------------------------------------------------- /examples/simple/nextjs/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import {Inter} from 'next/font/google' 2 | import {getCsrfToken, useSession} from "next-auth/react"; 3 | import {useState} from "react"; 4 | 5 | const inter = Inter({subsets: ['latin']}) 6 | 7 | const buttonStyles = "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full" 8 | 9 | export default function Home() { 10 | const [response, setResponse] = useState(); 11 | 12 | const buttonHandler = async () => { 13 | try { 14 | // Normally you'd use GET here, but we want to show how to do CSRF protection too, 15 | // Which with the default configuration doesn't happen on GET requests 16 | const csrfToken = await getCsrfToken() 17 | 18 | if (!csrfToken) { 19 | throw new Error("No csrf token") 20 | } 21 | 22 | const res = await fetch("fastapi/", { 23 | method: "POST", 24 | headers: { 25 | "X-XSRF-Token": csrfToken 26 | } 27 | }) 28 | const res_json = await res.json() 29 | setResponse(res_json.message) 30 | } catch (e) { 31 | setResponse("error") 32 | } 33 | } 34 | 35 | const {data: session} = useSession({ 36 | required: true, 37 | }) 38 | 39 | return ( 40 |
43 |
44 | Hello {session?.user?.name} 45 | 48 | {response ?
 {response} 
: null} 49 |
50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /examples/simple/nextjs/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) rgb(var(--background-start-rgb)); 26 | } 27 | -------------------------------------------------------------------------------- /examples/simple/nextjs/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 5 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 6 | './src/app/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | theme: { 9 | extend: { 10 | backgroundImage: { 11 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 12 | 'gradient-conic': 13 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | } 19 | -------------------------------------------------------------------------------- /examples/simple/nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "paths": { 22 | "@/*": [ 23 | "./src/*" 24 | ] 25 | }, 26 | "plugins": [ 27 | { 28 | "name": "next" 29 | } 30 | ] 31 | }, 32 | "include": [ 33 | "next-env.d.ts", 34 | "**/*.ts", 35 | "**/*.tsx", 36 | ".next/types/**/*.ts" 37 | ], 38 | "exclude": [ 39 | "node_modules" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "fastapi_nextauth_jwt" 7 | authors = [{name = "Tom Catshoek"}] 8 | readme = "README.md" 9 | license = {file = "LICENSE"} 10 | classifiers = ["License :: OSI Approved :: MIT License"] 11 | dynamic = ["version", "description"] 12 | dependencies = [ 13 | "fastapi", 14 | "cryptography", 15 | "python-jose[cryptography] >=3.4" 16 | ] 17 | 18 | [project.optional-dependencies] 19 | test = [ 20 | "pytest >=2.7.3", 21 | "pytest-cov", 22 | "httpx" 23 | ] 24 | 25 | [project.urls] 26 | Home = "https://github.com/TCatshoek/fastapi-nextauth-jwt" 27 | 28 | [tool.flit.sdist] 29 | exclude = [ 30 | "tests/", 31 | ".github/", 32 | ".examples/", 33 | ".gitignore" 34 | ] -------------------------------------------------------------------------------- /src/fastapi_nextauth_jwt/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | A fastapi dependency used to decode jwt tokens generated by nextauth, 3 | for use in nextjs/nextauth and fastapi mixed projects 4 | """ 5 | 6 | __version__ = "2.1.1" 7 | 8 | from fastapi_nextauth_jwt.fastapi_nextauth_jwt import NextAuthJWT, NextAuthJWTv4 9 | -------------------------------------------------------------------------------- /src/fastapi_nextauth_jwt/cookies.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | from fastapi_nextauth_jwt.exceptions import MissingTokenError 3 | 4 | 5 | def extract_token(cookies: Dict[str, str], cookie_name: str): 6 | """ 7 | Extracts a potentially chunked token from the cookies of a request. 8 | It may be in a single cookie, or chunked (with suffixes 0...n) 9 | :param req: The request to extract the token from 10 | :return: The encrypted nextauth session token 11 | """ 12 | encrypted_token = "" 13 | 14 | # Do we have a session cookie with the expected name? 15 | if cookie_name in cookies: 16 | encrypted_token = cookies[cookie_name] 17 | 18 | # Or maybe a chunked session cookie? 19 | elif f"{cookie_name}.0" in cookies: 20 | counter = 0 21 | while f"{cookie_name}.{counter}" in cookies: 22 | encrypted_token += cookies[f"{cookie_name}.{counter}"] 23 | counter += 1 24 | 25 | # Or no cookie at all 26 | else: 27 | raise MissingTokenError(status_code=401, message=f"Missing cookie: {cookie_name}") 28 | 29 | return encrypted_token 30 | -------------------------------------------------------------------------------- /src/fastapi_nextauth_jwt/csrf.py: -------------------------------------------------------------------------------- 1 | import urllib.parse 2 | from cryptography.hazmat.primitives import hashes 3 | from fastapi_nextauth_jwt.exceptions import InvalidTokenError 4 | 5 | 6 | def extract_csrf_info(csrf_string: str) -> [str, str]: 7 | csrf_token_unquoted = urllib.parse.unquote(csrf_string) 8 | if "|" not in csrf_token_unquoted: 9 | raise InvalidTokenError(status_code=401, message="Unrecognized CSRF token format") 10 | csrf_cookie_token, csrf_cookie_hash = csrf_token_unquoted.split("|") 11 | 12 | return csrf_cookie_token, csrf_cookie_hash 13 | 14 | 15 | def validate_csrf_info(secret: str, csrf_token: str, expected_hash: str): 16 | csrf_token_bytes = bytes(csrf_token, "ascii") 17 | secret_bytes = bytes(secret, "ascii") 18 | 19 | hasher = hashes.Hash(hashes.SHA256()) 20 | hasher.update(csrf_token_bytes) 21 | hasher.update(secret_bytes) 22 | actual_hash = hasher.finalize().hex() 23 | 24 | if expected_hash != actual_hash: 25 | raise InvalidTokenError(status_code=401, message="CSRF hash mismatch") 26 | -------------------------------------------------------------------------------- /src/fastapi_nextauth_jwt/exceptions.py: -------------------------------------------------------------------------------- 1 | class NextAuthJWTException(Exception): 2 | def __init__(self, *args: object): 3 | super().__init__(args) 4 | self.message = None 5 | self.status_code = None 6 | 7 | 8 | class MissingTokenError(NextAuthJWTException): 9 | def __init__(self, status_code: int, message: str): 10 | self.status_code = status_code 11 | self.message = message 12 | 13 | 14 | class InvalidTokenError(NextAuthJWTException): 15 | def __init__(self, status_code: int, message: str): 16 | self.status_code = status_code 17 | self.message = message 18 | 19 | 20 | class CSRFMismatchError(NextAuthJWTException): 21 | def __init__(self, status_code: int, message: str): 22 | self.status_code = status_code 23 | self.message = message 24 | 25 | 26 | class TokenExpiredException(NextAuthJWTException): 27 | def __init__(self, status_code: int, message: str): 28 | self.status_code = status_code 29 | self.message = message 30 | 31 | 32 | class UnsupportedEncryptionAlgorithmException(NextAuthJWTException): 33 | def __init__(self, status_code: int, message: str): 34 | self.status_code = status_code 35 | self.message = message 36 | -------------------------------------------------------------------------------- /src/fastapi_nextauth_jwt/fastapi_nextauth_jwt.py: -------------------------------------------------------------------------------- 1 | import json 2 | import typing 3 | import warnings 4 | from functools import partial 5 | from json import JSONDecodeError 6 | 7 | import os 8 | from typing import Set, Any, Literal, List 9 | 10 | from starlette.requests import Request 11 | 12 | from jose import jwe 13 | from jose.exceptions import JWEError 14 | from cryptography.hazmat.primitives import hashes 15 | 16 | from fastapi_nextauth_jwt.operations import derive_key, check_expiry 17 | from fastapi_nextauth_jwt.cookies import extract_token 18 | from fastapi_nextauth_jwt.csrf import extract_csrf_info, validate_csrf_info 19 | from fastapi_nextauth_jwt.exceptions import InvalidTokenError, MissingTokenError, CSRFMismatchError, \ 20 | UnsupportedEncryptionAlgorithmException 21 | 22 | EncAlgs = Literal["A256CBC-HS512", "A256GCM"] 23 | _supported_encryption_algs = list(typing.get_args(EncAlgs)) 24 | 25 | 26 | class NextAuthJWT: 27 | def __init__(self, 28 | secret: str = None, 29 | cookie_name: str = None, 30 | secure_cookie: bool = None, 31 | csrf_cookie_name: str = None, 32 | csrf_header_name: str = "X-XSRF-Token", 33 | info: bytes = b"Auth.js Generated Encryption Key", 34 | salt: typing.Union[bytes, None] = None, 35 | auto_append_salt: bool = True, 36 | hash_algorithm: Any = hashes.SHA256(), 37 | encryption_algorithm: EncAlgs = "A256CBC-HS512", 38 | csrf_prevention_enabled: bool = None, 39 | csrf_methods: Set[str] = None, 40 | check_expiry: bool = True): 41 | """ 42 | Initializes a new instance of the NextAuthJWT class. 43 | 44 | Args: 45 | secret (str): The secret used for key derivation. If not set, will be obtained from AUTH_SECRET env var. 46 | 47 | cookie_name (str, optional): The name of the session cookie. Defaults to "__Secure-next-auth.session-token" 48 | if using secure cookies, otherwise "next-auth.session-token" 49 | 50 | secure_cookie (bool, optional): Indicates if the session cookie is a secure cookie. Defaults to True 51 | if AUTH_URL starts with https://. else False. 52 | 53 | csrf_cookie_name (str, optional): The name of the CSRF token cookie. Defaults to 54 | "__Host-next-auth.csrf-token" if using secure cookies, else "next-auth.csrf-token". 55 | 56 | csrf_header_name (str, optional): The name of the CSRF token header. Defaults to "X-XSRF-Token". 57 | info (bytes, optional): The context for key derivation. Defaults to b"NextAuth.js Generated Encryption Key". 58 | salt (bytes, optional): The salt used for key derivation. Defaults to b"". 59 | hash_algorithm (Any, optional): The hash algorithm used for key derivation. Defaults to hashes.SHA256(). 60 | 61 | csrf_prevention_enabled (bool, optional): Indicates if CSRF prevention is enabled. 62 | Defaults to True if ENV == "dev, else False. 63 | 64 | csrf_methods (Set[str], optional): The HTTP methods that require CSRF protection. 65 | Defaults to {'POST', 'PUT', 'PATCH', 'DELETE'}. 66 | 67 | check_expiry (bool, optional): Whether or not to check the token for expiry. Defaults to True 68 | 69 | Example: 70 | >>> auth = NextAuthJWT(secret=os.getenv("AUTH_SECRET")) 71 | """ 72 | 73 | if secret is not None: 74 | self.secret = secret 75 | else: 76 | env_secret = os.getenv("AUTH_SECRET") 77 | if env_secret is None: 78 | if env_secret := os.getenv("NEXTAUTH_SECRET"): 79 | warnings.warn("'NEXTAUTH_SECRET' is deprecated; use 'AUTH_SECRET' instead", DeprecationWarning) 80 | else: 81 | raise ValueError("Secret not set") 82 | self.secret = env_secret 83 | 84 | if secure_cookie is None: 85 | auth_url = os.getenv("AUTH_URL") 86 | if auth_url is None: 87 | if auth_url := os.getenv("NEXTAUTH_URL"): 88 | warnings.warn("'NEXTAUTH_URL' is deprecated; use 'AUTH_URL' instead", DeprecationWarning) 89 | else: 90 | warnings.warn("AUTH_URL not set", RuntimeWarning) 91 | secure_cookie = (auth_url or "").startswith("https://") 92 | 93 | if cookie_name is None: 94 | self.cookie_name = "__Secure-authjs.session-token" if secure_cookie else "authjs.session-token" 95 | else: 96 | self.cookie_name = cookie_name 97 | 98 | if csrf_cookie_name is None: 99 | self.csrf_cookie_name = "__Host-authjs.csrf-token" if secure_cookie else "authjs.csrf-token" 100 | else: 101 | self.csrf_cookie_name = csrf_cookie_name 102 | 103 | if salt is None: 104 | salt = bytes(self.cookie_name, "ascii") 105 | 106 | self.csrf_header_name = csrf_header_name 107 | 108 | if encryption_algorithm not in _supported_encryption_algs: 109 | raise UnsupportedEncryptionAlgorithmException(status_code=500, message=encryption_algorithm) 110 | 111 | self.encryption_algorithm = encryption_algorithm 112 | 113 | key_length = 64 if self.encryption_algorithm == "A256CBC-HS512" else 32 114 | 115 | self.key = derive_key( 116 | secret=self.secret, 117 | length=key_length, 118 | salt=salt, 119 | algorithm=hash_algorithm, 120 | context=info + b" (" + salt + b")" if auto_append_salt else info 121 | ) 122 | 123 | if csrf_prevention_enabled is None: 124 | self.csrf_prevention_enabled = False if os.environ.get("ENV") == "dev" else True 125 | else: 126 | self.csrf_prevention_enabled = csrf_prevention_enabled 127 | 128 | if csrf_methods is None: 129 | self.csrf_methods = {'POST', 'PUT', 'PATCH', 'DELETE'} 130 | else: 131 | self.csrf_methods = csrf_methods 132 | 133 | self.check_expiry = check_expiry 134 | 135 | def __call__(self, req: Request = None): 136 | encrypted_token = extract_token(req.cookies, self.cookie_name) 137 | 138 | if self.csrf_prevention_enabled: 139 | self.check_csrf_token(req) 140 | 141 | try: 142 | decrypted_token_string = jwe.decrypt(encrypted_token, self.key) 143 | token = json.loads(decrypted_token_string) 144 | except (JWEError, JSONDecodeError) as e: 145 | print(e) 146 | raise InvalidTokenError(status_code=401, message="Invalid JWT format") 147 | 148 | if self.check_expiry: 149 | if "exp" not in token: 150 | raise InvalidTokenError(status_code=401, message="Invalid JWT format, missing exp") 151 | check_expiry(token['exp']) 152 | 153 | return token 154 | 155 | def check_csrf_token(self, req: Request): 156 | if req.method not in self.csrf_methods: 157 | return 158 | 159 | if self.csrf_cookie_name not in req.cookies: 160 | raise MissingTokenError(status_code=401, message=f"Missing CSRF token: {self.csrf_cookie_name}") 161 | if self.csrf_header_name not in req.headers: 162 | raise MissingTokenError(status_code=401, message=f"Missing CSRF header: {self.csrf_header_name}") 163 | 164 | csrf_cookie_token, csrf_cookie_hash = extract_csrf_info(req.cookies[self.csrf_cookie_name]) 165 | 166 | # Validate if it was indeed set by the server 167 | # See https://github.com/nextauthjs/next-auth/blob/50fe115df6379fffe3f24408a1c8271284af660b/src/core/lib/csrf-token.ts 168 | # for info on how the CSRF cookie is created 169 | validate_csrf_info(self.secret, csrf_cookie_token, csrf_cookie_hash) 170 | 171 | # Check if the CSRF token in the headers matches the one in the cookie 172 | csrf_header_token = req.headers[self.csrf_header_name] 173 | 174 | if csrf_header_token != csrf_cookie_token: 175 | raise CSRFMismatchError(status_code=401, message="CSRF Token mismatch") 176 | 177 | 178 | NextAuthJWTv4 = partial( 179 | NextAuthJWT, 180 | info=b"NextAuth.js Generated Encryption Key", 181 | salt=b"", 182 | auto_append_salt=False, 183 | encryption_algorithm="A256GCM", 184 | cookie_name="__Secure-next-auth.session-token"\ 185 | if os.getenv("AUTH_URL", "").startswith("https://")\ 186 | else "next-auth.session-token", 187 | csrf_cookie_name="__Host-next-auth.csrf-token"\ 188 | if os.getenv("AUTH_URL", "").startswith("https://")\ 189 | else "next-auth.csrf-token" 190 | ) 191 | -------------------------------------------------------------------------------- /src/fastapi_nextauth_jwt/operations.py: -------------------------------------------------------------------------------- 1 | from cryptography.hazmat.primitives.kdf.hkdf import HKDF 2 | import time 3 | 4 | from fastapi_nextauth_jwt.exceptions import TokenExpiredException 5 | 6 | 7 | def derive_key(secret: str, length: int, salt: bytes, algorithm, context: bytes) -> bytes: 8 | hkdf = HKDF( 9 | algorithm=algorithm, 10 | length=length, 11 | salt=salt, 12 | info=context 13 | ) 14 | return hkdf.derive(bytes(secret, "ascii")) 15 | 16 | 17 | def check_expiry(exp: int, cur_time: int = None): 18 | if cur_time is None: 19 | cur_time = time.time() 20 | if exp < cur_time: 21 | raise TokenExpiredException(403, "Token Expired") 22 | -------------------------------------------------------------------------------- /tests/fastapi/test_v4.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from fastapi.testclient import TestClient 3 | 4 | from fastapi_nextauth_jwt.exceptions import MissingTokenError, InvalidTokenError, TokenExpiredException 5 | from v4 import app 6 | 7 | client = TestClient(app) 8 | 9 | cookies = { 10 | "next-auth.session-token.0": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..95CNrOPTvSJ7jhfv.xIqTgbOdms-gLkyDoOfhkdEaTVnqDkQiXPXcf6hhxHsdbYZIiQysxQBEA76SmPy5h6pk_bHKXqkFBjtLHmb2Pj-Ed4UEErI2csSxSvayy0SiunIA9H-bTkGuAp2ae6Z3vHHEPISOtVVU44Xlp3EvllRosIIkhWjMaSIfWhsv_agzSumU6dwA253xiI5pEoO4JyNd83vkdGC9RR7XPSNkJV2RZ-TUedJiqekd3Laacq8ZwkY3aHODOhvaRYnvvC5TGJqRNoHfo8_XzqNme5lw-jlidIjkF0q-5vXFp9E5ZnHmClql6lDS5xWLqyNNmaG_HvBKh8IfpCJJdweYY9nH_Dl8K89CIQACi9wvlVBw72_28wv6IjvIEOoZUX0yjGOY1bgULlJdfDIIZJ01dbPxoTpm1yI-5pu_1DkL_vdUA_pH3wqoRakKfNBJvdrRDG0AK7ZnnJ12SGOP0kymWuOc9D1rOxO3ac53IQ4XxN0AyZwF9VgfDtT3jW_oO69mn-dKzbq80mFk8bZo4Nd0WxlZm53SWsPTMaNZaJ5pR1IEd5Yxws8l6A1iR8WFYF9st2rYLV1euTx4ahpDFaNPn9IeosZWNodfy6644rXh-K5OLo57_YG9I-I8RQGz0sVs6m3gEUxtL7URCPQWcY_NAkdo5YTJZ3LI_zqDNqoMMpavqkls6nPfM6Rnb8qw2N-0krfEuvGithp2zhghuKpG2BPBOJAXCk9lU4WyYOJ3RcPk4hl0L_Hfp_5jjliRP0217HIKfduAEgG-qrhY_UG-e49ntXjrkFNwqYEi_hH7x8yq7d_1Y7Fg7hdJAPGZcaLuWPNc8ZNGQGnQrpVYDooM7YnCAMlqZVvkZjE631xrlvgJfhDd8ILs11Rylju22r1Jw7eNcnodBjRCSk7LVDsiqg6YU0I8tM2nTKMW9KL30lDKY0AI_0IsHzfR3wwlsiZVrjm7AuZ3rLoAhF5pua51q-RCI9yAsFTWa0_r3sylEK2Rms-9NjkOVzBZQWYKqnXX7ZGpboMrKwy1mfTNxo0M7Gdr1_Rw93UWAXfdFJzqj8foVr3JncLRrbMCCYiCJgYWUkbWFJCcef-KjF1gIJ8_fIR83HSDvQq-UrBCSXWYh8iL6zvkcpDvUUT6ECtaeR2c3Boe1XqsqcAjeu8TVtAC-QbXd91LglhHy32cn9rbqnEcrBhHa7f_MevfyVtEsyQbfwT6jsV8GJbH8Q92oQ_EhxVtLwXv8vuaGdINFQ0I0-1MUUpu6rRd7LOBp9C7-vfQYuqybmH75YycNXzTIT6DXa5c66gXUWrfnGgAteqIRWG9dPn2TUNBcPv8IDkHEoutgP7IiCigFw6QyvpGSZ2YrMuFiDLJMG1qnDFgFjbWOEC173ZGACv4z4NJr6dwlUJaisx4_At42SsDxQN5lwUekDGSARCOwyO7KkezM2UIPcvexdYhTVazH3Piut3Ih1wmP2ruujZRdVaQSp1-i-JigE0yplmxBVJXZFgNaiHMmnnLmod3N2sq8Gs9gSXN2bNgdZx8MT0wPrh0NlBjAWSOyPCd9ZU5EeWwd1CahgMly9AqHDHhHyAigfzgMAvMNhqv01hOJu6D_j46QA6xYqzTobOg_pHIdcnjpjkZHamLlk60GUwLkIugqsn0glQiUWPi0VjLPYmoaL2n8bZ_DGqrJZQyyoz6VvI2zhCXBj0GoebTVBQAT7djeFPoESo8Mp-ae-yqnusbZGdpcC68HWDeK1JmIunigG0DQqaatIg4JZqYOPKhKX-utQKujt1jAe_7UBiWgWAPgLkgU07sYEEhEqKkTNfzrY5_5AStww5d9pSdFsRHKRrtOsElcuII0WuAbbnzI7j7ISZS34aAfIuOBkrvJeoTun12amonBxtaifYXcPWydNwlsz0R0giZQ3VdqLAwqU-v1OcTbTsLaJO8G0RHcsrxRGNpcKl45pN8rMwvOR1NyAHVCRvH-cnWIQeFrO3qimRI5CWhmMZy7LBI-Jmk6SiF7ze8x1IOwEFZuQ8IMoP8ZmipwiEZAYk88J-zAe2tTA5lazi7JensWlLe3uRwhCVg0-OKGexYz_MKxvLe4QeJDIHEyyLFjXJVzUn1xQO4h6F1Z6W9G6eWygjgQA3dpVf5Xg2_5akVmhCeQ9HcFOFx8DoT6wxb1DNb1x26thPp5Cj7ScagvVeL_Z0dpFM6Py5N8qiHu8WCzHhOgdWIWpY_Vlrn0m3g5mQzt6i26qGW5MMDpsneUpe--_M21YzbHfhYqIWbKsjfVFDspWHUA-6uKgJLNn3UpE2U9uUY54HsEEeUxTSutzSFmZpl-YOzZYToKRKGgKoyoLKaFIju-XUlrPcSL7324z0U-0g7Zfe5axO9TFQIyoa-yG2Iub8-CujPqfg5NaKa853ESkmqeFf929XFD0I4LX6j2-I-8n0357X6qjwnx9EQQ1P5KTxWGJ3v5Hxajj4bqV8NhwJmCV4jiDYGAGjl6KRWNcDHfhb9nSAQd6w12FFWkxVQCEujkFh6ZlVR0a8wfdEMqio-UldCTgx51sAn00_n8TmAv8ovc_6DCBcXiOxZuBqOnqkGFH9LEkE4TDOkUOpRLvAex9qMxKKodNZWRWF4Mmp_g4yQmY5A10Xtpe33ofdoSQWQ0YSHcyjX1WHyJ-bQ_B_kUu5ttpkCa7r4YG_zNK7dQQszi1EgfmuLKYdJ26wRuTvasynybBgA_RTNVVAQQeGTgaVvnhX0dSOtIsounIpPtydujZwAux1sxcYDK9ocfGr7rmWuz4xmtw-_DvgFQg5eM2g9s921L0c_8TDiZuIyiDk72XydTz_2R_4zeo-w133BITMRK1hH9IcjHqLYV1qx8HsUqN85fdSzhQWcv7jeQt_emXOb8A5JlYqO4aVP8q3RxCupiA0NvBNS8mJI3qVvmutFpfIR00DkhbJlK9zc63TILSdtYk3ljIph5s6bV1tTHwuHX4fdHZ4ny-Tx2wPkjIiqn_EX9znG5luDXc8LPmAG_J4UsiBrLfktjBZSIWErO8dDF3JoGkR71GZNJi_7wYphVP9e7cB4_YB3royTnS3o1qUXUma3eRVZztLkQDMbGrj9QiDAPsGGpAW005Fmvs4w5cZECGgxbbhTQ9GLBuXyI9ba4cLB5hcL1wf3krowAAjGI9vXr4vlakTuiTmbo0OflEblWq2D996CALd9UC5viyP3zFlc25U4cYRn97HKcxaeFa6noODigtnF3rMEjzQdVi2IZKoKHI1AG0F84tF_05Mbzm1R1cvEvUD7NfQPAt2J0OsmcK6oqWBNBlS06jC_BAG2eoZ78LZOsXo1zvjimez-fKun0w_u7TR-SVg2RqRCVICPpN7l8UtO4JTWy86Wnu5JynyKGyRVaLHCvChEF4mS2ohIcGeymB-pVpnH6jHMGgUJ9JEGU2uD9yxJc4CiDP6aBOEmwu4kVcGFbWbNQTXga7e6AuqtWBjmDuopKuYX_3cGYYYq_y8iMI6f1VoQPc6r0l_cs6E2PfHGhVZfrKRN3tFvELHqXBMGuiI7dfpOuWUxDvGuD4qa5sFiUXZSdDvns2LDX6tmuquFPXnhZZJnQhdjfLegBwbdj1J07UMUXDXlz5PzFyT43_yG8ZthP_dzgKYVi5lHSal_2UFPAtvICxa0uVNAiDorEjiUphTp1u_kSvJVzeFnaDWbdk6AtLByH0p6Bf-A-luibzqMQWC7UdiMZFaDVsypSBiRVO1znr1p9HBHEkvi1fs60gIYcbeum4qWsIF1dlQFqGZQNj5r9tze-r1Cwy7KGoPeRCzr3Pxh9nw8nrO13NmRg8YW7_wJctrVw3CV31VTCuUtBCMXvlqOw0f8wW8XLx3zpMe81fvvL7WPxkZ", 11 | "next-auth.session-token.1": "Wbm5RpcYwdLRwd6CdfaKBR4n_bYW_fIIXWrsOfwnqBhWOC7DwALBOlO4MD9Ms6yqf_7HnRlrtnpy9WZVbZlGxvKbQF3wjJ7jTUTqo6dRsQJVUiJrfGFAUkJvv6_gTUd-K-id1.wdD2KVszoMv3uvO1RK_X9A", 12 | } 13 | 14 | cookies_w_csrf = { 15 | "next-auth.session-token.0": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..95CNrOPTvSJ7jhfv.xIqTgbOdms-gLkyDoOfhkdEaTVnqDkQiXPXcf6hhxHsdbYZIiQysxQBEA76SmPy5h6pk_bHKXqkFBjtLHmb2Pj-Ed4UEErI2csSxSvayy0SiunIA9H-bTkGuAp2ae6Z3vHHEPISOtVVU44Xlp3EvllRosIIkhWjMaSIfWhsv_agzSumU6dwA253xiI5pEoO4JyNd83vkdGC9RR7XPSNkJV2RZ-TUedJiqekd3Laacq8ZwkY3aHODOhvaRYnvvC5TGJqRNoHfo8_XzqNme5lw-jlidIjkF0q-5vXFp9E5ZnHmClql6lDS5xWLqyNNmaG_HvBKh8IfpCJJdweYY9nH_Dl8K89CIQACi9wvlVBw72_28wv6IjvIEOoZUX0yjGOY1bgULlJdfDIIZJ01dbPxoTpm1yI-5pu_1DkL_vdUA_pH3wqoRakKfNBJvdrRDG0AK7ZnnJ12SGOP0kymWuOc9D1rOxO3ac53IQ4XxN0AyZwF9VgfDtT3jW_oO69mn-dKzbq80mFk8bZo4Nd0WxlZm53SWsPTMaNZaJ5pR1IEd5Yxws8l6A1iR8WFYF9st2rYLV1euTx4ahpDFaNPn9IeosZWNodfy6644rXh-K5OLo57_YG9I-I8RQGz0sVs6m3gEUxtL7URCPQWcY_NAkdo5YTJZ3LI_zqDNqoMMpavqkls6nPfM6Rnb8qw2N-0krfEuvGithp2zhghuKpG2BPBOJAXCk9lU4WyYOJ3RcPk4hl0L_Hfp_5jjliRP0217HIKfduAEgG-qrhY_UG-e49ntXjrkFNwqYEi_hH7x8yq7d_1Y7Fg7hdJAPGZcaLuWPNc8ZNGQGnQrpVYDooM7YnCAMlqZVvkZjE631xrlvgJfhDd8ILs11Rylju22r1Jw7eNcnodBjRCSk7LVDsiqg6YU0I8tM2nTKMW9KL30lDKY0AI_0IsHzfR3wwlsiZVrjm7AuZ3rLoAhF5pua51q-RCI9yAsFTWa0_r3sylEK2Rms-9NjkOVzBZQWYKqnXX7ZGpboMrKwy1mfTNxo0M7Gdr1_Rw93UWAXfdFJzqj8foVr3JncLRrbMCCYiCJgYWUkbWFJCcef-KjF1gIJ8_fIR83HSDvQq-UrBCSXWYh8iL6zvkcpDvUUT6ECtaeR2c3Boe1XqsqcAjeu8TVtAC-QbXd91LglhHy32cn9rbqnEcrBhHa7f_MevfyVtEsyQbfwT6jsV8GJbH8Q92oQ_EhxVtLwXv8vuaGdINFQ0I0-1MUUpu6rRd7LOBp9C7-vfQYuqybmH75YycNXzTIT6DXa5c66gXUWrfnGgAteqIRWG9dPn2TUNBcPv8IDkHEoutgP7IiCigFw6QyvpGSZ2YrMuFiDLJMG1qnDFgFjbWOEC173ZGACv4z4NJr6dwlUJaisx4_At42SsDxQN5lwUekDGSARCOwyO7KkezM2UIPcvexdYhTVazH3Piut3Ih1wmP2ruujZRdVaQSp1-i-JigE0yplmxBVJXZFgNaiHMmnnLmod3N2sq8Gs9gSXN2bNgdZx8MT0wPrh0NlBjAWSOyPCd9ZU5EeWwd1CahgMly9AqHDHhHyAigfzgMAvMNhqv01hOJu6D_j46QA6xYqzTobOg_pHIdcnjpjkZHamLlk60GUwLkIugqsn0glQiUWPi0VjLPYmoaL2n8bZ_DGqrJZQyyoz6VvI2zhCXBj0GoebTVBQAT7djeFPoESo8Mp-ae-yqnusbZGdpcC68HWDeK1JmIunigG0DQqaatIg4JZqYOPKhKX-utQKujt1jAe_7UBiWgWAPgLkgU07sYEEhEqKkTNfzrY5_5AStww5d9pSdFsRHKRrtOsElcuII0WuAbbnzI7j7ISZS34aAfIuOBkrvJeoTun12amonBxtaifYXcPWydNwlsz0R0giZQ3VdqLAwqU-v1OcTbTsLaJO8G0RHcsrxRGNpcKl45pN8rMwvOR1NyAHVCRvH-cnWIQeFrO3qimRI5CWhmMZy7LBI-Jmk6SiF7ze8x1IOwEFZuQ8IMoP8ZmipwiEZAYk88J-zAe2tTA5lazi7JensWlLe3uRwhCVg0-OKGexYz_MKxvLe4QeJDIHEyyLFjXJVzUn1xQO4h6F1Z6W9G6eWygjgQA3dpVf5Xg2_5akVmhCeQ9HcFOFx8DoT6wxb1DNb1x26thPp5Cj7ScagvVeL_Z0dpFM6Py5N8qiHu8WCzHhOgdWIWpY_Vlrn0m3g5mQzt6i26qGW5MMDpsneUpe--_M21YzbHfhYqIWbKsjfVFDspWHUA-6uKgJLNn3UpE2U9uUY54HsEEeUxTSutzSFmZpl-YOzZYToKRKGgKoyoLKaFIju-XUlrPcSL7324z0U-0g7Zfe5axO9TFQIyoa-yG2Iub8-CujPqfg5NaKa853ESkmqeFf929XFD0I4LX6j2-I-8n0357X6qjwnx9EQQ1P5KTxWGJ3v5Hxajj4bqV8NhwJmCV4jiDYGAGjl6KRWNcDHfhb9nSAQd6w12FFWkxVQCEujkFh6ZlVR0a8wfdEMqio-UldCTgx51sAn00_n8TmAv8ovc_6DCBcXiOxZuBqOnqkGFH9LEkE4TDOkUOpRLvAex9qMxKKodNZWRWF4Mmp_g4yQmY5A10Xtpe33ofdoSQWQ0YSHcyjX1WHyJ-bQ_B_kUu5ttpkCa7r4YG_zNK7dQQszi1EgfmuLKYdJ26wRuTvasynybBgA_RTNVVAQQeGTgaVvnhX0dSOtIsounIpPtydujZwAux1sxcYDK9ocfGr7rmWuz4xmtw-_DvgFQg5eM2g9s921L0c_8TDiZuIyiDk72XydTz_2R_4zeo-w133BITMRK1hH9IcjHqLYV1qx8HsUqN85fdSzhQWcv7jeQt_emXOb8A5JlYqO4aVP8q3RxCupiA0NvBNS8mJI3qVvmutFpfIR00DkhbJlK9zc63TILSdtYk3ljIph5s6bV1tTHwuHX4fdHZ4ny-Tx2wPkjIiqn_EX9znG5luDXc8LPmAG_J4UsiBrLfktjBZSIWErO8dDF3JoGkR71GZNJi_7wYphVP9e7cB4_YB3royTnS3o1qUXUma3eRVZztLkQDMbGrj9QiDAPsGGpAW005Fmvs4w5cZECGgxbbhTQ9GLBuXyI9ba4cLB5hcL1wf3krowAAjGI9vXr4vlakTuiTmbo0OflEblWq2D996CALd9UC5viyP3zFlc25U4cYRn97HKcxaeFa6noODigtnF3rMEjzQdVi2IZKoKHI1AG0F84tF_05Mbzm1R1cvEvUD7NfQPAt2J0OsmcK6oqWBNBlS06jC_BAG2eoZ78LZOsXo1zvjimez-fKun0w_u7TR-SVg2RqRCVICPpN7l8UtO4JTWy86Wnu5JynyKGyRVaLHCvChEF4mS2ohIcGeymB-pVpnH6jHMGgUJ9JEGU2uD9yxJc4CiDP6aBOEmwu4kVcGFbWbNQTXga7e6AuqtWBjmDuopKuYX_3cGYYYq_y8iMI6f1VoQPc6r0l_cs6E2PfHGhVZfrKRN3tFvELHqXBMGuiI7dfpOuWUxDvGuD4qa5sFiUXZSdDvns2LDX6tmuquFPXnhZZJnQhdjfLegBwbdj1J07UMUXDXlz5PzFyT43_yG8ZthP_dzgKYVi5lHSal_2UFPAtvICxa0uVNAiDorEjiUphTp1u_kSvJVzeFnaDWbdk6AtLByH0p6Bf-A-luibzqMQWC7UdiMZFaDVsypSBiRVO1znr1p9HBHEkvi1fs60gIYcbeum4qWsIF1dlQFqGZQNj5r9tze-r1Cwy7KGoPeRCzr3Pxh9nw8nrO13NmRg8YW7_wJctrVw3CV31VTCuUtBCMXvlqOw0f8wW8XLx3zpMe81fvvL7WPxkZ", 16 | "next-auth.session-token.1": "Wbm5RpcYwdLRwd6CdfaKBR4n_bYW_fIIXWrsOfwnqBhWOC7DwALBOlO4MD9Ms6yqf_7HnRlrtnpy9WZVbZlGxvKbQF3wjJ7jTUTqo6dRsQJVUiJrfGFAUkJvv6_gTUd-K-id1.wdD2KVszoMv3uvO1RK_X9A", 17 | "next-auth.csrf-token": "89f032cc1b6e570b4c5631e1ecae0541e2c6edd42ee47ab143cc55294b4486f3%7Ca7f3c2b6ea7188ced2697febf582f0bdf5b94459c39087b074d939c66d5357f9" 18 | } 19 | 20 | cookies_invalid = { 21 | "next-auth.session-token.0": "eyJhbGciObJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..95CNrOPTvSJ7jhfv.xIqTgbOdms-gLkyDoOfhkdEaTVnqDkQiXPXcf6hhxHsdbYZIiQysxQBEA76SmPy5h6pk_bHKXqkFBjtLHmb2Pj-Ed4UEErI2csSxSvayy0SiunIA9H-bTkGuAp2ae6Z3vHHEPISOtVVU44Xlp3EvllRosIIkhWjMaSIfWhsv_agzSumU6dwA253xiI5pEoO4JyNd83vkdGC9RR7XPSNkJV2RZ-TUedJiqekd3Laacq8ZwkY3aHODOhvaRYnvvC5TGJqRNoHfo8_XzqNme5lw-jlidIjkF0q-5vXFp9E5ZnHmClql6lDS5xWLqyNNmaG_HvBKh8IfpCJJdweYY9nH_Dl8K89CIQACi9wvlVBw72_28wv6IjvIEOoZUX0yjGOY1bgULlJdfDIIZJ01dbPxoTpm1yI-5pu_1DkL_vdUA_pH3wqoRakKfNBJvdrRDG0AK7ZnnJ12SGOP0kymWuOc9D1rOxO3ac53IQ4XxN0AyZwF9VgfDtT3jW_oO69mn-dKzbq80mFk8bZo4Nd0WxlZm53SWsPTMaNZaJ5pR1IEd5Yxws8l6A1iR8WFYF9st2rYLV1euTx4ahpDFaNPn9IeosZWNodfy6644rXh-K5OLo57_YG9I-I8RQGz0sVs6m3gEUxtL7URCPQWcY_NAkdo5YTJZ3LI_zqDNqoMMpavqkls6nPfM6Rnb8qw2N-0krfEuvGithp2zhghuKpG2BPBOJAXCk9lU4WyYOJ3RcPk4hl0L_Hfp_5jjliRP0217HIKfduAEgG-qrhY_UG-e49ntXjrkFNwqYEi_hH7x8yq7d_1Y7Fg7hdJAPGZcaLuWPNc8ZNGQGnQrpVYDooM7YnCAMlqZVvkZjE631xrlvgJfhDd8ILs11Rylju22r1Jw7eNcnodBjRCSk7LVDsiqg6YU0I8tM2nTKMW9KL30lDKY0AI_0IsHzfR3wwlsiZVrjm7AuZ3rLoAhF5pua51q-RCI9yAsFTWa0_r3sylEK2Rms-9NjkOVzBZQWYKqnXX7ZGpboMrKwy1mfTNxo0M7Gdr1_Rw93UWAXfdFJzqj8foVr3JncLRrbMCCYiCJgYWUkbWFJCcef-KjF1gIJ8_fIR83HSDvQq-UrBCSXWYh8iL6zvkcpDvUUT6ECtaeR2c3Boe1XqsqcAjeu8TVtAC-QbXd91LglhHy32cn9rbqnEcrBhHa7f_MevfyVtEsyQbfwT6jsV8GJbH8Q92oQ_EhxVtLwXv8vuaGdINFQ0I0-1MUUpu6rRd7LOBp9C7-vfQYuqybmH75YycNXzTIT6DXa5c66gXUWrfnGgAteqIRWG9dPn2TUNBcPv8IDkHEoutgP7IiCigFw6QyvpGSZ2YrMuFiDLJMG1qnDFgFjbWOEC173ZGACv4z4NJr6dwlUJaisx4_At42SsDxQN5lwUekDGSARCOwyO7KkezM2UIPcvexdYhTVazH3Piut3Ih1wmP2ruujZRdVaQSp1-i-JigE0yplmxBVJXZFgNaiHMmnnLmod3N2sq8Gs9gSXN2bNgdZx8MT0wPrh0NlBjAWSOyPCd9ZU5EeWwd1CahgMly9AqHDHhHyAigfzgMAvMNhqv01hOJu6D_j46QA6xYqzTobOg_pHIdcnjpjkZHamLlk60GUwLkIugqsn0glQiUWPi0VjLPYmoaL2n8bZ_DGqrJZQyyoz6VvI2zhCXBj0GoebTVBQAT7djeFPoESo8Mp-ae-yqnusbZGdpcC68HWDeK1JmIunigG0DQqaatIg4JZqYOPKhKX-utQKujt1jAe_7UBiWgWAPgLkgU07sYEEhEqKkTNfzrY5_5AStww5d9pSdFsRHKRrtOsElcuII0WuAbbnzI7j7ISZS34aAfIuOBkrvJeoTun12amonBxtaifYXcPWydNwlsz0R0giZQ3VdqLAwqU-v1OcTbTsLaJO8G0RHcsrxRGNpcKl45pN8rMwvOR1NyAHVCRvH-cnWIQeFrO3qimRI5CWhmMZy7LBI-Jmk6SiF7ze8x1IOwEFZuQ8IMoP8ZmipwiEZAYk88J-zAe2tTA5lazi7JensWlLe3uRwhCVg0-OKGexYz_MKxvLe4QeJDIHEyyLFjXJVzUn1xQO4h6F1Z6W9G6eWygjgQA3dpVf5Xg2_5akVmhCeQ9HcFOFx8DoT6wxb1DNb1x26thPp5Cj7ScagvVeL_Z0dpFM6Py5N8qiHu8WCzHhOgdWIWpY_Vlrn0m3g5mQzt6i26qGW5MMDpsneUpe--_M21YzbHfhYqIWbKsjfVFDspWHUA-6uKgJLNn3UpE2U9uUY54HsEEeUxTSutzSFmZpl-YOzZYToKRKGgKoyoLKaFIju-XUlrPcSL7324z0U-0g7Zfe5axO9TFQIyoa-yG2Iub8-CujPqfg5NaKa853ESkmqeFf929XFD0I4LX6j2-I-8n0357X6qjwnx9EQQ1P5KTxWGJ3v5Hxajj4bqV8NhwJmCV4jiDYGAGjl6KRWNcDHfhb9nSAQd6w12FFWkxVQCEujkFh6ZlVR0a8wfdEMqio-UldCTgx51sAn00_n8TmAv8ovc_6DCBcXiOxZuBqOnqkGFH9LEkE4TDOkUOpRLvAex9qMxKKodNZWRWF4Mmp_g4yQmY5A10Xtpe33ofdoSQWQ0YSHcyjX1WHyJ-bQ_B_kUu5ttpkCa7r4YG_zNK7dQQszi1EgfmuLKYdJ26wRuTvasynybBgA_RTNVVAQQeGTgaVvnhX0dSOtIsounIpPtydujZwAux1sxcYDK9ocfGr7rmWuz4xmtw-_DvgFQg5eM2g9s921L0c_8TDiZuIyiDk72XydTz_2R_4zeo-w133BITMRK1hH9IcjHqLYV1qx8HsUqN85fdSzhQWcv7jeQt_emXOb8A5JlYqO4aVP8q3RxCupiA0NvBNS8mJI3qVvmutFpfIR00DkhbJlK9zc63TILSdtYk3ljIph5s6bV1tTHwuHX4fdHZ4ny-Tx2wPkjIiqn_EX9znG5luDXc8LPmAG_J4UsiBrLfktjBZSIWErO8dDF3JoGkR71GZNJi_7wYphVP9e7cB4_YB3royTnS3o1qUXUma3eRVZztLkQDMbGrj9QiDAPsGGpAW005Fmvs4w5cZECGgxbbhTQ9GLBuXyI9ba4cLB5hcL1wf3krowAAjGI9vXr4vlakTuiTmbo0OflEblWq2D996CALd9UC5viyP3zFlc25U4cYRn97HKcxaeFa6noODigtnF3rMEjzQdVi2IZKoKHI1AG0F84tF_05Mbzm1R1cvEvUD7NfQPAt2J0OsmcK6oqWBNBlS06jC_BAG2eoZ78LZOsXo1zvjimez-fKun0w_u7TR-SVg2RqRCVICPpN7l8UtO4JTWy86Wnu5JynyKGyRVaLHCvChEF4mS2ohIcGeymB-pVpnH6jHMGgUJ9JEGU2uD9yxJc4CiDP6aBOEmwu4kVcGFbWbNQTXga7e6AuqtWBjmDuopKuYX_3cGYYYq_y8iMI6f1VoQPc6r0l_cs6E2PfHGhVZfrKRN3tFvELHqXBMGuiI7dfpOuWUxDvGuD4qa5sFiUXZSdDvns2LDX6tmuquFPXnhZZJnQhdjfLegBwbdj1J07UMUXDXlz5PzFyT43_yG8ZthP_dzgKYVi5lHSal_2UFPAtvICxa0uVNAiDorEjiUphTp1u_kSvJVzeFnaDWbdk6AtLByH0p6Bf-A-luibzqMQWC7UdiMZFaDVsypSBiRVO1znr1p9HBHEkvi1fs60gIYcbeum4qWsIF1dlQFqGZQNj5r9tze-r1Cwy7KGoPeRCzr3Pxh9nw8nrO13NmRg8YW7_wJctrVw3CV31VTCuUtBCMXvlqOw0f8wW8XLx3zpMe81fvvL7WPxkZ", 22 | "next-auth.session-token.1": "Wbm5RpcYwdLRwd6CdfaKBR4n_bYW_fIIXWrsOfwnqBhWOC7DwALBOlO4MD9Ms6yqf_7HnRlrtnpy9WZVbZlGxvKbQF3wjJ7jTUTqo6dRsQJVUiJrfGFAUkJvv6_gTUd-K-id1.wdD2KVszoMv3uvO1RK_X9A", 23 | } 24 | 25 | expected_jwt = {'email': 'test@test.nl', 26 | 'sub': '84c8b3a1-7358-4211-a0f8-d5428375b3ae', 27 | 'accessToken': 'eyJraWQiOiJ6ZnVyQ2prWk50OUs0SzYwdGQ3cDhnVEloNG40OXZWN0IyNzhFNlFHUjRJPSIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiI4NGM4YjNhMS03MzU4LTQyMTEtYTBmOC1kNTQyODM3NWIzYWUiLCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAuZXUtd2VzdC0xLmFtYXpvbmF3cy5jb21cL2V1LXdlc3QtMV90bUJnUUxITkMiLCJ2ZXJzaW9uIjoyLCJjbGllbnRfaWQiOiI0ZGI1NmJkOWFsZGRycmpibG45NTV0bzFzOCIsIm9yaWdpbl9qdGkiOiI0MGJjNjYyNS1mMjk4LTQwODUtOTEzOS05MmI1YmQ3Zjk3YTUiLCJldmVudF9pZCI6ImYyNjc0MDc2LTc1MGMtNDIyNC1iMjNkLTJkZjc4YjExODVmZCIsInRva2VuX3VzZSI6ImFjY2VzcyIsInNjb3BlIjoib3BlbmlkIiwiYXV0aF90aW1lIjoxNjg5MjczMzA3LCJleHAiOjE2ODkyNzY5MDcsImlhdCI6MTY4OTI3MzMwNywianRpIjoiZTM2YTFkNTItNGI1YS00NjJmLWJmODctY2NjZDk0NTBmY2ZjIiwidXNlcm5hbWUiOiI4NGM4YjNhMS03MzU4LTQyMTEtYTBmOC1kNTQyODM3NWIzYWUifQ.I8rXQOMgz2hdQRb1vIiTjXig6kQX1ItntqW9wPr-Dr19wf_lU2zW_9TkzasP1_n6-Sl4nYfwixbcbhtez2XNPyNlF_oUZ3TccI6X2mp7-6c4hPUzuWeqrGGGBM0zoN5HJXXf-M2WKBZ5VF5TDQv4-Cl-BfbuYVbdS7zNdHhoAagCTRjCNg3d3p2ubZFWdzAI8cwl1qosYxKd8zy1qFmHBCuOjPz_HhAz-70gk4L4bcuU5BJkOIXy3GiHjiTloXkEmkF0uPPj3wjGDTab_h5yT8u0Cgh3Nrbaa1-f7xeTNhrUWd-eze50cTGC-rrl6nLF9K34eZso9o1jtfiwz_yUXQ', 28 | 'refreshToken': 'eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAifQ.jTJoyxspsiO93uKWyI03nEumiyc_X4o0_n-gZpDBpWeaQXXayHrOk3UwzMoDD9PvZOgD1F77lJ9tQ38T_f0XyhTscLWBuolicxwHUfhWQNHhngVKGs9zFQgbyxipgJih_eoioRnFv9u_UQ7NshBkf1AG72goYaHYMgaOTlnSD0aR96ysfyedSCEZTIB4nPwKEvOc_cRZFeUnT5Any-LJyEZbTiO1hcdA8WWtuI18mDmaJP_5rI3jTMxoMtnIwapo2kP2fkqDDBukJAoTOU1BzXsmSOvw3s-ztT95qUN_Ni8Wlcv0YpceupCXPnGbHkPbgMQKCzny9dO6WEiCS7r7Wg.FGYQIB7zvKzRm9GG.rdQi4z18GY-e5we2Qz9l2EChKatT8uSpn_LvSJVm6kcvfORukZrT8Ox-KVVSdK0J9mh0NWnW5AdN2q2O5luSqJZyti-_hj73CqfBgKMrThvMW45cu6N7gKP3HlkFQIx4y0vherAxP3imCC66Lo-c-vCNPJJBZjgfQ_-VmIJ49AGk3Im39_B7_y5xwPHi42-VPST6AYBtQeAcAmOcZGzXStWl8-iKatWNjBw16QHtWoawI9UCydLrV4dn-WbtkiO7-MQwADKM0Ql8O9S7Fm9qx_DItBkayTJGrabDRMrbc9QsYttvRyXuWaFwxPqRK1zJsSQ8WFkXYSZTcMZ74u9axbIkqZtQvqK24nhUFTfdz2XVkIyNPhaV_JmL8SRV7dXhKrcdK-5B4tBSNcw1d5AkhoK75JmeBQOlHUBbAQGga1oeFxZTKNrXKrgsVNaCYi3KomhQcvjCNvzpCyn36EXwQkNId2a7Rd_Jvh4gxfVZGlHDmQ-ONDnlPs9EsILhD87nFRIhR900X7-VUK_MG1GoWwFRKaWG_N6npBNVg8tLVFB-7d-n8CDJJuI0sWh3jNqlfek12FWZerh5TEyUOAN6Wcl8ov-ocLfkrnOgKBjCz2_-POJhWUIPwr2_nunqO-sgZ-vPitU8R-7OB6sdgG7ZQNYLboyC2NrB7TPY4CpDrM-Vx7xEiWOVWG0biS30dE9xK8E5DhddC5aUs6yt-P3lsrHeZ-ttIyOwJ-asrZRSe3uU7t4ItPqDeqCjXU5ib8YyOSk8OJwhyKTddvhCs0m4PdjKj9uA4uWRmg7VdMNS6g-BX0uGDP5sLdV0WzADyqJ0NEkyCybTiCebSi_4vqHm4ZsAcnkgtMgd5xp--0v0f4lOc5W_6HqDWCkoqxmIKgAve4cV3LoqkmIret1MnuZQoZt3Z-BXr4z4jk8oXGHzL292567zG5eKJVVWvm-V_Pch3pl4wEnbeiOzf01h9C4Yym9OSjXZd3Xjc26UtMoAyW2ERD8TUQy5ZOuboknjy2eH-hJRUOB-C28IzW3_MavRjhv8_jyI3InzPnzqmuBB8grkEprQ5-_bnOYGcBQpB1F1tWOSNzmfEH0ksVMDCktP5ph4XyBIdAz2O662Vxub5ouZDp5GPCWCXZMLOC3yrVLkCiG0AD5vepXIN6U6tFBIk--d6YOZZRAo9zuDKFTgC65KQE8xMZzq9zzrqVY-igaZZ8NqlY_8M7Xfxdlch-X4tx5zHY_Ku9zYmx14drB1Vuj98wP7m-p0KRqwJDYcpDhjc_XYRuG8uunjgHEJ3C_s0LJlDQcEalvJfQzcPsX0.sj7jXlqlX0lm4AJzId1EDw', 29 | 'iat': 1689273309, 30 | 'exp': 1691865309, 31 | 'jti': '2b8540dd-4d7c-400f-ad65-7d112ac5a07a'} 32 | 33 | 34 | @pytest.fixture(autouse=True) 35 | def patch_current_time(monkeypatch): 36 | # Monkeypatch the current time so tests don't depend on it 37 | monkeypatch.setattr("fastapi_nextauth_jwt.operations.check_expiry.__defaults__", (1691865300,)) 38 | 39 | 40 | def test_no_csrf(): 41 | client.cookies = cookies 42 | response = client.get("/") 43 | 44 | assert response.status_code == 200 45 | assert response.json() == expected_jwt 46 | 47 | 48 | def test_csrf(): 49 | client.cookies = cookies_w_csrf 50 | response = client.post("/csrf", 51 | headers={ 52 | "X-XSRF-Token": "89f032cc1b6e570b4c5631e1ecae0541e2c6edd42ee47ab143cc55294b4486f3" 53 | }) 54 | 55 | assert response.status_code == 200 56 | assert response.json() == expected_jwt 57 | 58 | 59 | def test_csrf_missing_token(): 60 | with pytest.raises(MissingTokenError) as exc_info: 61 | client.cookies = cookies 62 | client.post("/csrf") 63 | assert exc_info.value.message == "Missing CSRF token: next-auth.csrf-token" 64 | 65 | 66 | def test_csrf_missing_header(): 67 | with pytest.raises(MissingTokenError) as exc_info: 68 | client.cookies = cookies_w_csrf 69 | client.post("/csrf") 70 | assert exc_info.value.message == "Missing CSRF header: X-XSRF-Token" 71 | 72 | 73 | def test_csrf_no_csrf_method(): 74 | client.cookies = cookies 75 | response = client.get("/csrf") 76 | 77 | assert response.status_code == 200 78 | assert response.json() == expected_jwt 79 | 80 | 81 | def test_invalid_jwt(): 82 | with pytest.raises(InvalidTokenError) as exc_info: 83 | client.cookies = cookies_invalid 84 | client.get("/") 85 | assert exc_info.value.message == "Invalid JWT format" 86 | 87 | 88 | def test_expiry(monkeypatch): 89 | # In this case, we patch the current time to be after the token expiry time 90 | monkeypatch.setattr("fastapi_nextauth_jwt.operations.check_expiry.__defaults__", (1691865320,)) 91 | 92 | with pytest.raises(TokenExpiredException) as exc_info: 93 | client.cookies = cookies 94 | client.get("/") 95 | assert exc_info.value.message == "Token Expired" 96 | -------------------------------------------------------------------------------- /tests/fastapi/test_v5.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from fastapi.testclient import TestClient 3 | 4 | from fastapi_nextauth_jwt.exceptions import MissingTokenError, InvalidTokenError, TokenExpiredException 5 | from v5 import app 6 | 7 | client = TestClient(app) 8 | 9 | cookies = { 10 | "authjs.session-token": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwia2lkIjoidDBOWWk4TExkYWVjNlctdlcwN3BRekdUR2dwSmgtaTBLRXlKcHFGcjRqSEkySkRtdDJNTnpqQ0Uwcjc0bDBFT240NmZOMUdMcEpsa09QY0NYZ2JNR3cifQ..VKK_QKVTc0-UxFoOD6ZxZg.pHmOvrG1kCq4IApuJD6lCplq5TBjhxGf_rd43h43kXddPGDwjSEUeRYbcSO-sSfXl8DnXw9Q9e1zJPMlxl1maZRaBV2kAla8kBebL19DPgEDHNVTmW_ujgidlSHk3bbNhOO1U1fXNdvUbQqHOAScjxv60CPJpVd-9CaL6Zw_Teg.S2KOuWV72JtSZca8VhOhQvSFofpKJKVb_jjf_Ld-zWA", 11 | } 12 | 13 | cookies_w_csrf = { 14 | "authjs.session-token": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwia2lkIjoidDBOWWk4TExkYWVjNlctdlcwN3BRekdUR2dwSmgtaTBLRXlKcHFGcjRqSEkySkRtdDJNTnpqQ0Uwcjc0bDBFT240NmZOMUdMcEpsa09QY0NYZ2JNR3cifQ..VKK_QKVTc0-UxFoOD6ZxZg.pHmOvrG1kCq4IApuJD6lCplq5TBjhxGf_rd43h43kXddPGDwjSEUeRYbcSO-sSfXl8DnXw9Q9e1zJPMlxl1maZRaBV2kAla8kBebL19DPgEDHNVTmW_ujgidlSHk3bbNhOO1U1fXNdvUbQqHOAScjxv60CPJpVd-9CaL6Zw_Teg.S2KOuWV72JtSZca8VhOhQvSFofpKJKVb_jjf_Ld-zWA", 15 | "authjs.csrf-token": "53e18023db04541f0ffbe3c5f7683d2388806401eb46020f74889fa723a2623b%7C0a44296fabc59e85e37195731d6f132c78bc7884d33594ded089706f215c3647" 16 | } 17 | 18 | cookies_invalid = { 19 | "authjs.session-token": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwia2lkIjoidDBOWWk4TExkYWVjNlctdlcwN3BRekdUR2dwSmgtaTBLRXlKcHFGcjRqSEkySkRtdDJNTnpqQ0Uwcjc0bDBFT240NmZOMUdMcEpsa09QY0NYZ2JNR3cifQ..VKK_QKVTc0-UxFoOD6ZxZg.pHmOvrG1kCq4IApuJD6lCplq5TBjhxGf_rd43h43kXddPGDwjSEUeRYbcSO-sSfXl8DnXw9Q9e1zJPMlxl1maZRaBV2kAla8kBebL19DPgEDHNVTmW_ujgidlSHk3bbNhOO1U1fXNdvUbQqHOAScjxv60CPJpVd-9CaL6Zw_Teg.S2KOuWV72JtSZca8VhOhQvSFofpFJKVb_jjf_Ld-zWA", 20 | } 21 | 22 | expected_jwt = { 23 | 'name': 'asdf', 24 | 'email': 'test@test.nl', 25 | 'sub': '1', 26 | 'iat': 1714146974, 27 | 'exp': 1716738974, 28 | 'jti': '9e8f6368-9236-458d-ba23-2bb95fdbfdbd' 29 | } 30 | 31 | 32 | @pytest.fixture(autouse=True) 33 | def patch_current_time(monkeypatch): 34 | # Monkeypatch the current time so tests don't depend on it 35 | monkeypatch.setattr("fastapi_nextauth_jwt.operations.check_expiry.__defaults__", (1714146975,)) 36 | 37 | 38 | def test_no_csrf(): 39 | client.cookies = cookies 40 | response = client.get("/") 41 | 42 | assert response.status_code == 200 43 | assert response.json() == expected_jwt 44 | 45 | 46 | def test_csrf(): 47 | client.cookies = cookies_w_csrf 48 | response = client.post("/csrf", 49 | headers={ 50 | "X-XSRF-Token": "53e18023db04541f0ffbe3c5f7683d2388806401eb46020f74889fa723a2623b" 51 | }) 52 | 53 | assert response.status_code == 200 54 | assert response.json() == expected_jwt 55 | 56 | 57 | def test_csrf_missing_token(): 58 | with pytest.raises(MissingTokenError) as exc_info: 59 | client.cookies = cookies 60 | client.post("/csrf") 61 | assert exc_info.value.message == "Missing CSRF token: next-auth.csrf-token" 62 | 63 | 64 | def test_csrf_missing_header(): 65 | with pytest.raises(MissingTokenError) as exc_info: 66 | client.cookies = cookies_w_csrf 67 | client.post("/csrf") 68 | assert exc_info.value.message == "Missing CSRF header: X-XSRF-Token" 69 | 70 | 71 | def test_csrf_no_csrf_method(): 72 | client.cookies = cookies 73 | response = client.get("/csrf") 74 | 75 | assert response.status_code == 200 76 | assert response.json() == expected_jwt 77 | 78 | 79 | def test_invalid_jwt(): 80 | with pytest.raises(InvalidTokenError) as exc_info: 81 | client.cookies = cookies_invalid 82 | client.get("/") 83 | assert exc_info.value.message == "Invalid JWT format" 84 | 85 | 86 | def test_expiry(monkeypatch): 87 | # In this case, we patch the current time to be after the token expiry time 88 | monkeypatch.setattr("fastapi_nextauth_jwt.operations.check_expiry.__defaults__", (1716738975,)) 89 | 90 | with pytest.raises(TokenExpiredException) as exc_info: 91 | client.cookies = cookies 92 | client.get("/") 93 | assert exc_info.value.message == "Token Expired" 94 | -------------------------------------------------------------------------------- /tests/fastapi/v4.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | 3 | from fastapi import FastAPI, Depends 4 | from fastapi_nextauth_jwt import NextAuthJWTv4 5 | 6 | app = FastAPI() 7 | 8 | JWT = NextAuthJWTv4( 9 | secret="6dDnFiDpUlKlbJciCusuFKNYmcf4WpIigldzX/Wb/FA=", 10 | csrf_prevention_enabled=False, 11 | ) 12 | 13 | JWTwCSRF = NextAuthJWTv4( 14 | secret="6dDnFiDpUlKlbJciCusuFKNYmcf4WpIigldzX/Wb/FA=", 15 | csrf_prevention_enabled=True, 16 | ) 17 | 18 | 19 | @app.get("/") 20 | async def read_main(jwt: Annotated[dict, Depends(JWT)]): 21 | return jwt 22 | 23 | 24 | @app.post("/csrf") 25 | async def read_main(jwt: Annotated[dict, Depends(JWTwCSRF)]): 26 | return jwt 27 | 28 | 29 | @app.get("/csrf") 30 | async def read_main(jwt: Annotated[dict, Depends(JWTwCSRF)]): 31 | return jwt 32 | -------------------------------------------------------------------------------- /tests/fastapi/v5.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | 3 | from fastapi import FastAPI, Depends 4 | from fastapi_nextauth_jwt import NextAuthJWT 5 | 6 | app = FastAPI() 7 | 8 | JWT = NextAuthJWT( 9 | secret="y0uR_SuP3r_s3cr37_$3cr3t", 10 | csrf_prevention_enabled=False, 11 | ) 12 | 13 | JWTwCSRF = NextAuthJWT( 14 | secret="y0uR_SuP3r_s3cr37_$3cr3t", 15 | csrf_prevention_enabled=True, 16 | ) 17 | 18 | 19 | @app.get("/") 20 | async def read_main(jwt: Annotated[dict, Depends(JWT)]): 21 | return jwt 22 | 23 | 24 | @app.post("/csrf") 25 | async def read_main(jwt: Annotated[dict, Depends(JWTwCSRF)]): 26 | return jwt 27 | 28 | 29 | @app.get("/csrf") 30 | async def read_main(jwt: Annotated[dict, Depends(JWTwCSRF)]): 31 | return jwt 32 | -------------------------------------------------------------------------------- /tests/unit/test_cookies.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from fastapi_nextauth_jwt.cookies import extract_token 3 | from fastapi_nextauth_jwt.exceptions import MissingTokenError 4 | 5 | 6 | def test_extract_cookie_single(): 7 | cookies = { 8 | "next-auth.session-token": "token123" 9 | } 10 | cookie_name = "next-auth.session-token" 11 | 12 | token = extract_token(cookies, cookie_name) 13 | 14 | assert token == "token123" 15 | 16 | 17 | def test_extract_cookie_multi(): 18 | cookies = { 19 | "next-auth.session-token.0": "token", 20 | "next-auth.session-token.1": "1", 21 | "next-auth.session-token.2": "2", 22 | "next-auth.session-token.3": "3", 23 | } 24 | cookie_name = "next-auth.session-token" 25 | 26 | token = extract_token(cookies, cookie_name) 27 | 28 | assert token == "token123" 29 | 30 | def test_extract_cookie_missing(): 31 | cookies = { 32 | "no-cookie": "for-you" 33 | } 34 | cookie_name = "next-auth.session-token" 35 | 36 | with pytest.raises(MissingTokenError) as exc_info: 37 | extract_token(cookies, cookie_name) 38 | assert exc_info.value.message == f"Missing cookie: {cookie_name}" 39 | -------------------------------------------------------------------------------- /tests/unit/test_csrf.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from fastapi_nextauth_jwt.csrf import extract_csrf_info, validate_csrf_info 4 | from fastapi_nextauth_jwt.exceptions import InvalidTokenError 5 | 6 | 7 | def test_extract_csrf(): 8 | csrf_string = "89f032cc1b6e570b4c5631e1ecae0541e2c6edd42ee47ab143cc55294b4486f3%7Ca7f3c2b6ea7188ced2697febf582f0bdf5b94459c39087b074d939c66d5357f9" 9 | csrf_cookie_token, csrf_cookie_hash = extract_csrf_info(csrf_string) 10 | 11 | assert csrf_cookie_token == "89f032cc1b6e570b4c5631e1ecae0541e2c6edd42ee47ab143cc55294b4486f3" 12 | assert csrf_cookie_hash == "a7f3c2b6ea7188ced2697febf582f0bdf5b94459c39087b074d939c66d5357f9" 13 | 14 | 15 | def test_extract_invalid_csrf(): 16 | csrf_string = "89f032cc1b6e570b4c5631e1ecae0541e2c6edd42ee47ab143cc55294b6ea7188ced2697febf582f0bdf5b94459c39087b074d939c66d5357f9" 17 | 18 | with pytest.raises(InvalidTokenError) as exc_info: 19 | extract_csrf_info(csrf_string) 20 | assert exc_info.value.message == "Unrecognized CSRF token format" 21 | 22 | 23 | def test_validate_csrf(): 24 | csrf_token = "89f032cc1b6e570b4c5631e1ecae0541e2c6edd42ee47ab143cc55294b4486f3" 25 | csrf_hash = "a7f3c2b6ea7188ced2697febf582f0bdf5b94459c39087b074d939c66d5357f9" 26 | secret = "6dDnFiDpUlKlbJciCusuFKNYmcf4WpIigldzX/Wb/FA=" 27 | validate_csrf_info(secret, csrf_token, csrf_hash) 28 | 29 | 30 | def test_validate_csrf_incorrect(): 31 | csrf_token = "89f032cc1b6e570b4c5631e1ecae0541e2c6edd42ee47ab143cc55294b4486f3" 32 | csrf_hash = "a7f3c2b6ea7188ced2697febf582f0bdf5b94459c39087b074d939c66d5357f9" 33 | secret = "teehee" 34 | with pytest.raises(InvalidTokenError) as exc_info: 35 | validate_csrf_info(secret, csrf_token, csrf_hash) 36 | assert exc_info.value.message == "CSRF hash mismatch" 37 | -------------------------------------------------------------------------------- /tests/unit/test_operations.py: -------------------------------------------------------------------------------- 1 | from fastapi_nextauth_jwt import operations 2 | from cryptography.hazmat.primitives import hashes 3 | 4 | 5 | def test_derive_key(): 6 | secret = "aUblGYhT507qxin/mQ+UlvyUjuR5dI9I8yKm5ZeVWDQ=" 7 | 8 | derived_key = operations.derive_key( 9 | secret, 10 | 32, 11 | b"", 12 | hashes.SHA256(), 13 | b"NextAuth.js Generated Encryption Key" 14 | ) 15 | 16 | expected_key = b'\x80\x00\xd1\x07*=\xa0}\xaa\x18\xeb\xee\x9c\x95\xbcXzr\xc3\x17\x98\x9f\xbc\xd9\xbfU\xbay0\xcfh\x01' 17 | 18 | assert expected_key == derived_key 19 | -------------------------------------------------------------------------------- /tests/unit/test_secret.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from fastapi_nextauth_jwt import NextAuthJWT 4 | 5 | 6 | def test_obtain_secret_from_argument(): 7 | secret = "foo" 8 | JWT = NextAuthJWT(secret=secret) 9 | assert JWT.secret == secret 10 | 11 | 12 | def test_obtain_secret_from_env(monkeypatch): 13 | secret = "foo" 14 | monkeypatch.setenv("AUTH_SECRET", secret) 15 | JWT = NextAuthJWT() 16 | assert JWT.secret == secret 17 | 18 | 19 | def test_obtain_secret_precedence(monkeypatch): 20 | secret1 = "foo" 21 | monkeypatch.setenv("AUTH_SECRET", secret1) 22 | 23 | secret2 = "bar" 24 | 25 | JWT = NextAuthJWT(secret=secret2) 26 | assert JWT.secret == secret2 27 | 28 | 29 | def test_obtain_secret_not_set(): 30 | with pytest.raises(ValueError) as exc_info: 31 | NextAuthJWT() 32 | assert exc_info.value.message == "Secret not set" 33 | 34 | 35 | @pytest.mark.filterwarnings("error") 36 | def test_secret_auth_secret_no_url(monkeypatch): 37 | monkeypatch.setenv("AUTH_SECRET", "foo") 38 | with pytest.warns(RuntimeWarning, match="AUTH_URL not set"): 39 | NextAuthJWT() 40 | 41 | def test_secret_nextauth_secret_deprecation_warning(monkeypatch): 42 | monkeypatch.setenv("NEXTAUTH_SECRET", "foo") 43 | with pytest.deprecated_call(): 44 | NextAuthJWT() 45 | 46 | def test_secret_nextauth_url_deprecation_warning(monkeypatch): 47 | monkeypatch.setenv("AUTH_SECRET", "foo") 48 | monkeypatch.setenv("NEXTAUTH_URL", "bar") 49 | with pytest.deprecated_call(): 50 | NextAuthJWT() --------------------------------------------------------------------------------