# JWTdown for FastAPI

We wrote `jwtdown-fastapi` to help you reduce the
boilerplate authentication code written for FastAPI. It's
based on the code that you can find at [OAuth2 with Password
(and hashing), Bearer with JWT
tokens](https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/).

## Quick start

You can use this package to easily create the endpoints for
the login and logout functionality using a hybrid cookie and
JSON version of token distribution to ease your use on the
front-end.

The easiest way to use JWTdown for FastAPI is to create an
instance of the
[Authenticator](./api-reference.html#jwtdown_fastapi.authentication.Authenticator).

### Installation and customization

1. Install the package.

    ```sh
    pip install jwtdown-fastapi
    ```

1. Create a file to hold your specialized `Authenticator`
   that uses whatever way your application gets account
   information from a username/email.

   In the following code snippet, the `AccountRepo` class is
   an object that implements the [repository
   pattern](https://www.cosmicpython.com/book/chapter_02_repository.html).
   It can be anything, really, as long as it returns a
   dictionary or a Pydantic model when called with the
   `username`.

   In the following code snippet, `Account` and `AccountOut`
   are Pydantic models used in your application.

    ```python
    # authenticator.py
    import os
    from fastapi import Depends
    from jwtdown_fastapi.authentication import Authenticator
    from queries.accounts import AccountRepo, AccountOut, Account


    class MyAuthenticator(Authenticator):
        async def get_account_data(
            self,
            username: str,
            accounts: AccountRepo,
        ):
            # Use your repo to get the account based on the
            # username (which could be an email)
            return accounts.get(username)

        def get_account_getter(
            self,
            accounts: AccountRepo = Depends(),
        ):
            # Return the accounts. That's it.
            return accounts

        def get_hashed_password(self, account: Account):
            # Return the encrypted password value from your
            # account object
            return account.hashed_password

        def get_account_data_for_cookie(self, account: Account):
            # Return the username and the data for the cookie.
            # You must return TWO values from this method.
            return account.username, AccountOut(**account.dict())


    authenticator = MyAuthenticator(os.environ["SIGNING_KEY"])
    ```

    You **MUST** implement the first three methods. If you
    do not have an `email` property on your account model,
    you must implement the last method, too, to return the
    unique identifier for the account.

### Signing Key

The SIGNING_KEY mentioned above should ideally be 20-40
characters long. `jwtdown-fastapi` uses HS256 as its default
algorithm. HS256 is based on symmetric encryption. This
means that all of your microservices will need to use the
same string value if they all way to use the same JWT.

```{danger} Signing keys are private
You MUST not share your signing key in your repository or
anywhere someone can get it. Secret management is the
hardest part of having secrets.
```

### Easy log in and log out

1. In your Python module that declares your FastAPI object,
   import JWTdown for FastAPI, import your `Authenticator`
   object, and include its routers.

    ```python
    # main.py
    from authenticator import authenticator
    from fastapi import APIRouter

    app = FastAPI()
    app.include_router(authenticator.router)
    ```

    Now, by default, your application has two routes:

    * `POST /token` to log in using form data, which sets a
       cookie _and_ returns a JSON token for you to use in
       your API calls
    * `DELETE /token` to log out, which deletes the cookie
      set with logging in

### Requiring a valid token

In any of your endpoints that you want to secure to a
logged-in user, use the
``authenticator.get_current_account_data`` method as an
injected dependency.

```python
# router.py
from authenticator import authenticator
from fastapi import APIRouter

router = APIRouter()


@router.post("/api/things")
async def create_thing(
    account_data: dict = Depends(authenticator.get_current_account_data),
):
    pass
```

The ``get_current_account_data`` will look for a bearer
token in the ``Authorization`` header, first. If it doesn't
find one there, then it will look for a cookie named
"fastapi_token" (or whatever you configured it to be). When
it finds one of those, it validates the value as a token.

### Getting the current account data, if they exist

In any of your endpoints that you want the current account
data, but it's optional, use
``authenticator.try_get_current_account_data``.

```python
# router.py
from typing import Optional
from authenticator import authenticator
from fastapi import APIRouter

router = APIRouter()


@router.get("/api/things")
async def get_things(
    account_data: Optional[dict] = Depends(authenticator.try_get_current_account_data),
):
    if account_data:
        return personalized_list
    return general_list
```

The ``get_current_account_data`` will look for a bearer
token in the ``Authorization`` header, first. If it doesn't
find one there, then it will look for a cookie named
"fastapi_token" (or whatever you configured it to be). When
it finds one of those, it validates the value as a token.

### Getting tokens from HTTP-only cookies

Let's say that you want to use a `fetch` that includes your
browser's cookies. That way, you can use the server to
decode the data that you send it.

To do that, use the `try_get_current_account_data` and
`authenticator.cookie_name` to send back a payload that
contains the JWT for use in `fetch` calls to
non-authenticating services, as well as the decoded account
data from the JWT stored in the data that was returned from
the `get_account_data_for_cookie` call when the person
logged in.

```{warning} Only for fetch with included credentials
This will only work if you set `credentials: 'include'`
as part of the options for your `fetch` or RTK Query `query`.
```

```python
# router.py
from jwtdown_fastapi.authentication import Token
from .auth import authenticator


class AccountToken(Token):
    account: AccountOut


@router.get("/token", response_model=AccountToken | None)
async def get_token(
    request: Request,
    account: Account = Depends(authenticator.try_get_current_account_data)
) -> AccountToken | None:
    if account and authenticator.cookie_name in request.cookies:
        return {
            "access_token": request.cookies[authenticator.cookie_name],
            "type": "Bearer",
            "account": account,
        }
```

### Letting people sign up for new accounts

If you allow people to sign up for an account, you'll want
to hash their passwords and use the login functionality of
the ``Authenticator``.

To make sure the passwords are compatible with the
``Authenticator``, you should use the ``hash_password``
method.

Then, you should use the ``login`` method of the
``Authenticator``. You'll need to create a form Pydantic
model to hold the form information that was submitted to
your sign-up endpoint.

```python
# router.py
from fastapi import (
    Depends,
    HTTPException,
    status,
    Response,
    APIRouter,
    Request,
)
from jwtdown_fastapi.authentication import Token
from .auth import authenticator

from pydantic import BaseModel

from queries.accounts import (
    AccountIn,
    AccountOut,
    AccountRepo,
    DuplicateAccountError,
)

class AccountForm(BaseModel):
    username: str
    password: str

class AccountToken(Token):
    account: AccountOut

class HttpError(BaseModel):
    detail: str

router = APIRouter()


@router.post("/api/accounts", response_model=AccountToken | HttpError)
async def create_account(
    info: AccountIn,
    request: Request,
    response: Response,
    repo: AccountRepo = Depends(),
):
    hashed_password = authenticator.hash_password(info.password)
    try:
        account = repo.create(info, hashed_password)
    except DuplicateAccountError:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Cannot create an account with those credentials",
        )
    form = AccountForm(username=info.email, password=info.password)
    token = await authenticator.login(response, request, form, repo)
    return AccountToken(account=account, **token.dict())
```

In the above code example, the code hashes the plain text
password. Then, the ``accounts.create`` method creates a new
account in the data store using the account information and
the hashed password. It does _not_ save the plain password
to the data store because that Would Be Bad<sup>TM</sup>.

Then, the code creates an ``AccountForm`` object that has
the ``username`` and ``password`` attributes. All of that
is passed to the ``login`` method of the ``Authenticator``,
which returns a ``Token`` object. We take that ``Token``
object and turn it into our own ``AccountToken`` object.
