Build and Secure a FastAPI Server with Auth0

Learn the basics of FastAPI, how to quickly set up a server, and secure endpoints with Auth0.
Read more…

:writing_hand:t2: Brough to you by Mark Halpin

4 Likes

Hi, thanks for the nice tutorial, it helped a lot. After using it in development, I found some performance improvement that I think is worth sharing:

In the presented code, a new object of VerifyToken is created on each API call. In the init of VerifyToken, an object of PyJWKClient is created. This client makes a call to the well-known page of the authentication backend. What that mean is, for each call to the API, the well-known page is requested. In production cases, thats quite some overhead an unnecessary network traffic.

A way to improove that is to have a global VerifyToken object in main.py that buffers the authentication keys and checks the tokens of all api calls accross all endpoints. To do that, first shift the token input in the VerifyToken class from init to verify:

# in utils.py
class VerifyToken:

    def __init__(self):     # remove token input here
        self.config = set_up()
        jwks_url = f'https://{self.config["DOMAIN"]}/.well-known/jwks.json'
        self.jwks_client = PyJWKClient(jwks_url)

    def verify(self, token_unclean: str):   # have token input here
        # btw: I remove the 'Bearer ' prefix in my version here
        token = token_unclean.replace('Bearer ', '')    
        ...     # remember to remove 'self.' from 'self.token' now...

Now that VerifyToken can handle multiple tokens, have one object of it in main.py and reuse it:

# main.py
token_verifier = VerifyToken()

@app.get("/api/private")
def private(response: Response, token: str = Depends(token_auth_scheme)):
    """A valid access token is required to access this route"""
 
    result = token_verifier.verify(token) # uses the global verifier now
    ...

The PyJWKClient already cares about the buffering, now that it is persistent accross multiple api calls. It only calls the well-known page if it gets a token with an unkown kid to verify.

Hi @tepeit, glad you enjoyed the tutorial.

It is true that it would be possible to optimize the number of calls to retrieve the jwks, however, the caching method should be evaluated on a project basis.

For example, Auth0 supports signing key rotation triggered by tenant admins, and your cache system should retrieve the latest values otherwise newly created tokens won’t be accepted, and the old ones, which should be deprecated would still work while your cache is up.

There are other examples, and use cases that our colleague Nicolas explains well on this thread: Caching JWKS signing key

In the sample code you provided, there seems to be no way to update the cache other than restarting the app, you may wanna evaluate that for your needs.

Thanks again for your response, and looking forward to reading how you decided to go about caching for your app, and seeing more Auth0 implementations for FastAPI, such a fantastic framework!

Hi Juan,

sorry for not replying, I got busy with some other stuff inbetween. Thanks for your reply and pointing out some more infos on the caching topic.

It turns out, that the present version of the pyjwt library (2.6.0) has quite some new features that address the given issues. At present, the documentation does not contain these features (there is a TODO note in the docs about that). However, it seems the library is now capable of caching keys for a given lifetime. You init the PyJWKClient like that:

jwks_client = PyJWKClient(jwks_url, cache_jwk_set=True, lifespan=300)

The lifespan is given in secconds, so it will cache for a maximum of 5 minutes like that. (cache_jwk_set=True and lifespan=300 are also the default parameter, so you might as well just skip them).

That way, the jwks will be requested only every 5 minutes and corrupted keys are removed after 5 minutes at latest. New keys are beeing requested as soon as an unkown kid is sent to the client. That way, the issue with DDOS attacks with an unkown kid remains - but honestly, if you sent DDOS attacks to my backend, you don’t need such tricks to get it down… :slight_smile:

Hi @tepeit, thanks for sharing.

That is correct, up from version 2.5.0 a level of caching is added by default.

It is not well documented, but I saw the implementation of the code.

That solution would suffice most use cases, it is really nice to see it implemented that way.

Thanks for sharing!

Hi @juan.martinez I am trying to replicate this on my own project but I am receiving an error at the very last step:

Internal Server Error

ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Hostname mismatch, certificate is not valid for ‘.us.domain.auth0.com’. (_ssl.c:1129)

I copied my Auth0 values just as instructed in the post and youtube video.

Any thoughts on how to fix?

Thanks,
Nick

Hi @nick23, welcome to the Auth0 community, how are you?

What gets my attention in the error message you shared is that the domain seems incomplete. Did you enter the right domain name in your settings? Normally it follows the pattern {tenant-name}.region.auth0.com.

I’ll follow up also with the internal teams to see what else could it be as I haven’t seen that error message before and I’ll let you know as soon as I get feedback.

Thanks,
Juan

Hey @bajcmartinez thanks for the quick reply. The .config file is set up as instructed on my specific values. I removed my domain name before “us.domain.auth0.com/” for privacy sake. Let me know if I should send the name of my instance?

Please let me know what you find out. Everything is 1:1 with the blog/video.

Hi @nick23, I would need a bit more information, particularly, I’d like to see your settings as you have them in your .config file, and if possible, you can remove the client secret from the file, but leave the rest as is please. Also can you share which platform are you using, is it windows? linux? or mac?

I’ll send you a private message with my contact details.

Thanks!

1 Like

Hello,

I am not sure, but I remember that the python SDK has API implementations for the token validation.

Another thing to consider is a python openid-connect client with auto-discover of allowed signing schemes, token and authorization endpoint.

Not sure if there’s one already…

Anyway, I think the article could be updated with this approach.

Another thing to apply to the article is the pydantic-powered settings loading strategy, either from JSON, Yaml or directly from environment variables. All of these cases are possible with pydantic BaseSettings base class.

hi @francipvb, now that you mention it, a new version of this article is on the works, it is already written and will go live very soon! It doesn’t make use of the python SDK for the token validation, but it has a more fastapi way of doing things.

Thanks for your feedback! I’ll message here again when the new version goes live.

Is the article already published? I’m using Litestar so I’d be interested in one that may work for both of them.

Hi @saphyel , how are you? The article is up to date now using the latest from FastAPI, please let me know if this works for you.

I have to work with Libestar yet, but based on what I work, you can implement a similar solution for that framework, and let me know if you have some questions or feedback along the way.

Thanks!

Hello! I am getting 403 errors when using access token for private endpoint, this step:

curl -X 'GET' \
--url '<http://127.0.0.1:8000/api/private>' \
--header  'Authorization: Bearer <YOUR_BEARER_TOKEN>'

Is there step by step sample for how to set up the Auth0 side such that this tutorial code will work? For example what is the Auth0 issuer vs. Auth0 domain in the .env file. Not sure if this maps to my 403 error.

Hello, thank you much for this tutorial. I followed this to the “t” and somehow I’m getting a 403 error with {“detail”:“Invalid issuer”}. I double checked the configuration values (domain, audience, issuer) and I cannot find the mistake. Any pointers?

Thank you again.

Hi @jy1, sorry for the delayed response. Let me try to clarify the different configuration variables, as it is skipped in the article (I’ll correct that later).

AUTH0_DOMAIN

This is the tenant domain and it can be found in your Tenant’s settings or in any application settings page.

AUTH0_ISSUER

This represents the URI under which Auth0 issued the token, commonly is the base URL of your domain, hence https://<your-domain>/, so for example, if your tenant’s domain is: abc.auth0.com, your issuer would be https://abc.auth0.com/.

AUTH0_API_AUDIENCE

This parameters defines the intended consumer of the token, which in this case, is the API. You define the audience when creating your API on the Auth0 dashboard.

Here is a video that explains audience in detail:

AUTH0_ALGORITHMS

When you create an API in the Auth0 dashboard, you can specify the Signing Algorithm, which uses RS256 by default, though you could opt to use HS256 instead. You should specify in this field the same algorithm you have chosen for your API.

Hope this helps, let me know if you have any further questions.

Thanks

1 Like

hi @hector.vazquez , how are you?

That is typically an issue with the configuration settings. I’d suggest the following, visit https://jwt.io/ and paste there your access token. The site will automatically decode your token and give you information about its payload on the right side of the screen.

Validate the audience and issuer values on your settings with the fields iss (issuer), and aud (audience). They have to be the exact same values, including the last forward slash if any /.

Let me know if you had any differences in your settings and if that fixed the issue.

Thanks,
Juan