├── .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 | [](https://badge.fury.io/py/fastapi-nextauth-jwt)
4 | [](https://pypi.org/project/fastapi-nextauth-jwt/)
5 | [](https://github.com/yourusername/fastapi-nextauth-jwt/blob/main/LICENSE)
6 | [](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 |
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()
--------------------------------------------------------------------------------