Fastapi: [Question] using external oAuth providers

Created on 24 Jan 2019  路  27Comments  路  Source: tiangolo/fastapi

Hi,

First of all, thank you for all the amazing work you've done with FastAPI. I'm just scratching the surface and I'm already amazed by what you're able to do right out of the box.

As for my question, At my institute we use a centralized authentication service for all webapps base on ORY Hydra. Is there an easy way built in to integrate an external server to authenticate again and use the token query the API?

Thanks
M

confirmed docs

Most helpful comment

First of all, thank you for all the amazing work you've done with FastAPI. I'm just scratching the surface and I'm already amazed by what you're able to do right out of the box.

That's awesome to hear @matthdsm !

Also, thanks for taking the time to create the diagram and explain the use case so clearly.

@rcox771 Awesome response! Thanks for sticking around and helping here :smile: :taco:

It's asynchronous time-traveling chain-lightning black magic and it totally rocks imo.

Loved that, I'm citing it :joy: :grin:


Some more info:

Pretty much what @rcox771 explained.

Let's say the function get_user doesn't take the parameters in the example, and actually goes and calls Hydra's OpenId Connect UserInfo endpoint, most probably using Requests, and then returns a Pydantic model with the user data (if you use a Pydantic model you can have all the completion, type checks, etc). Then you pretty much have the rest already from those examples.

One little detail: as Requests doesn't use async in the current versions, you would declare def get_current_user with a normal def instead of async def.


More technical details (this is about to get pretty technical with specs stuff):

Specifically for your use case that is a bit more "elaborate", FastAPI has some other utilities that you can use for advanced OAuth2 flows, that are not in the tutorials but are there available for these cases.

The tutorials use the OAuth2 Password Flow. Which is the one appropriate when the same FastAPI application is the "Authorization Server" and the "Resource Server" (using OAuth 2.0 spec terms). That's the simplest case that works for the simplest/more common scenarios.

In your case, your FastAPI application would be the "Resource Server", but Hydra would be the "Authorization Server".

In this case, you would probably use the "Authorization Code" or the "Impicit" OAuth 2.0 flows.

As Hydra is OpenId Connect compatible, you might want to use fastapi.security.open_id_connect_url.OpenIdConnect.

And instead of using OAuth2PasswordBearer, you would use OAuth2 directly.

OAuth2 receives a flows parameter with your OAuth 2.0 flows, you can define them as a simple dictionary, but also using the included Pydantic models...

FastAPI, although not documented, includes Pydantic models for the whole OpenAPI 3.0 spec, this might help you if you want to use completion, etc during development. And it includes, of course, all the models for OAuth 2.0 flows, etc. You can use them from here: https://github.com/tiangolo/fastapi/blob/master/fastapi/openapi/models.py#L291

There are models for all the OAuth 2.0 flows, also made to match the OpenAPI 3.0 spec (and the way it's sub-divided in sections):

  • OAuthFlowImplicit: :heavy_check_mark: this is the one where your frontend gets the token directly with the authentication. And then sends it to the FastAPI backend.
  • OAuthFlowPassword: :x: this probably doesn't apply to you, because the frontend sends the password to Hydra directly, not to FastAPI.
  • OAuthFlowClientCredentials: :x: I guess this doesn't apply either, as your public frontend is the one authenticating your users, not a secure backend.
  • OAuthFlowAuthorizationCode: :heavy_check_mark: this is when your frontend gets a "code" that sends to your FastAPI backend, and then your FastAPI backend uses that code with secret client credentials to call Hydra to get a token that it can actually use to perform operations (again calling Hydra) like authenticating and getting user info. This is the most complex flow.

All this OAuth 2.0 stuff gets hairy, abstract and not very easy to understand quickly. If you are an expert in OAuth 2.0 with all it covers already (wow, congrats), I'm probably talking (writing) too much already and you know what to do.

But I personally don't know anyone that knows and understands deeply all OAuth 2.0 with all its abstractions, flows, etc. :joy:

My suggestion is, build something quick that you can throw away using the basic OAuth 2.0 tutorial. Just to get your hands dirty and check that you have the basics working (debug any unrelated errors). This should take you 15 minutes (less?) if you do the OAuth 2.0 tutorial in FastAPI's docs.

Then, after that, use the OAuthFlowImplicit (the simplest one that applies for your case).

If needed, then implement the OAuthFlowAuthorizationCode, but I think that's not very common.

All 27 comments

To clarify, the app structure should look a bit like this (super simplified)

image

Cheers
M

Hey @matthdsm ,

This particular set of docs and examples should get you pretty far:

https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/
https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/

You also get an interactive way to test all this out too.
image
(taken from the docs above)

The only piece missing (I assume, based on your chart) would be an async def that implements a request to your external service to check the token and user. If it were me, I'd swap out the parts I need from this guy:

async def get_current_user(token: str = Security(oauth2_scheme)):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        token_data = TokenPayload(**payload)
    except PyJWTError:
        raise HTTPException(
            status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials"
        )

    #use something like requests.get and pass your payload here probably
    user = get_user(fake_users_db, username=token_data.username)
    return user

Side note: if you're coming from Django or Flask, most people reuse or enforce auth using the decorator pattern (i.e. @requires_auth). @tiangolo 's examples above show a much easier and traceable way using Depends. It's _asynchronous time-traveling chain-lightning black magic_ and it totally rocks imo. I bring this up because you may want to reuse parts of it in your auth flow too.

Hope that helps, but I'll stick around if you run into probs or have more specific questions.

First of all, thank you for all the amazing work you've done with FastAPI. I'm just scratching the surface and I'm already amazed by what you're able to do right out of the box.

That's awesome to hear @matthdsm !

Also, thanks for taking the time to create the diagram and explain the use case so clearly.

@rcox771 Awesome response! Thanks for sticking around and helping here :smile: :taco:

It's asynchronous time-traveling chain-lightning black magic and it totally rocks imo.

Loved that, I'm citing it :joy: :grin:


Some more info:

Pretty much what @rcox771 explained.

Let's say the function get_user doesn't take the parameters in the example, and actually goes and calls Hydra's OpenId Connect UserInfo endpoint, most probably using Requests, and then returns a Pydantic model with the user data (if you use a Pydantic model you can have all the completion, type checks, etc). Then you pretty much have the rest already from those examples.

One little detail: as Requests doesn't use async in the current versions, you would declare def get_current_user with a normal def instead of async def.


More technical details (this is about to get pretty technical with specs stuff):

Specifically for your use case that is a bit more "elaborate", FastAPI has some other utilities that you can use for advanced OAuth2 flows, that are not in the tutorials but are there available for these cases.

The tutorials use the OAuth2 Password Flow. Which is the one appropriate when the same FastAPI application is the "Authorization Server" and the "Resource Server" (using OAuth 2.0 spec terms). That's the simplest case that works for the simplest/more common scenarios.

In your case, your FastAPI application would be the "Resource Server", but Hydra would be the "Authorization Server".

In this case, you would probably use the "Authorization Code" or the "Impicit" OAuth 2.0 flows.

As Hydra is OpenId Connect compatible, you might want to use fastapi.security.open_id_connect_url.OpenIdConnect.

And instead of using OAuth2PasswordBearer, you would use OAuth2 directly.

OAuth2 receives a flows parameter with your OAuth 2.0 flows, you can define them as a simple dictionary, but also using the included Pydantic models...

FastAPI, although not documented, includes Pydantic models for the whole OpenAPI 3.0 spec, this might help you if you want to use completion, etc during development. And it includes, of course, all the models for OAuth 2.0 flows, etc. You can use them from here: https://github.com/tiangolo/fastapi/blob/master/fastapi/openapi/models.py#L291

There are models for all the OAuth 2.0 flows, also made to match the OpenAPI 3.0 spec (and the way it's sub-divided in sections):

  • OAuthFlowImplicit: :heavy_check_mark: this is the one where your frontend gets the token directly with the authentication. And then sends it to the FastAPI backend.
  • OAuthFlowPassword: :x: this probably doesn't apply to you, because the frontend sends the password to Hydra directly, not to FastAPI.
  • OAuthFlowClientCredentials: :x: I guess this doesn't apply either, as your public frontend is the one authenticating your users, not a secure backend.
  • OAuthFlowAuthorizationCode: :heavy_check_mark: this is when your frontend gets a "code" that sends to your FastAPI backend, and then your FastAPI backend uses that code with secret client credentials to call Hydra to get a token that it can actually use to perform operations (again calling Hydra) like authenticating and getting user info. This is the most complex flow.

All this OAuth 2.0 stuff gets hairy, abstract and not very easy to understand quickly. If you are an expert in OAuth 2.0 with all it covers already (wow, congrats), I'm probably talking (writing) too much already and you know what to do.

But I personally don't know anyone that knows and understands deeply all OAuth 2.0 with all its abstractions, flows, etc. :joy:

My suggestion is, build something quick that you can throw away using the basic OAuth 2.0 tutorial. Just to get your hands dirty and check that you have the basics working (debug any unrelated errors). This should take you 15 minutes (less?) if you do the OAuth 2.0 tutorial in FastAPI's docs.

Then, after that, use the OAuthFlowImplicit (the simplest one that applies for your case).

If needed, then implement the OAuthFlowAuthorizationCode, but I think that's not very common.

I guess what I'm wondering, without going too far down the OAuth rabbits hole, is how to enable both a password bearer scheme for local authentication as well as an OpenID Connect scheme for federated authentication.

So for example to let someone login either via Google or local username/password bearer, would I define one def async with two token arguments, one based on OAuth2PasswordBearer (but that wouldn't raise an error on no token being there), one based on regular OAuth2?

@kkinder OAuth2PasswordBearer is just a utility that inherits from OAuth2: https://github.com/tiangolo/fastapi/blob/master/fastapi/security/oauth2.py#L130, extracts the bearer token and sets the OAuth2 flows to password.

In your case, you could create a custom CustomOAuth2 (or similar) that inherits from OAuth2 directly and that gets the token from the request, checks if it's a password token and if not, checks if it's an OAuth2 token generated from an OpenID service.

For it, you would create the flows dictionary including password and the other type of flow used by your service (defined by the OpenID provider), e.g. implicit.

To parse the token in the case it was a password token, you can copy code fragments from the OAuth2PasswordBearer (the same file referenced above).

Also, have in mind that:

  • OpenID Connect is based on OAuth2, and in OpenAPI, it just defines the URL of an OAuth2 endpoint, you still have to have the OAuth2 endpoints.
  • OpenID Connect is adopted by Google. But Facebook, Twitter, GitHub, and others use their own OAuth2 flavors.

Thanks, @tiangolo , that's a really helpful summary.

Nice discussion! I have about the same requirement and I am working on a solution to support this.
I am using Auth0 as a Authentication provider and have been using this with an Angular frontend and Hug backend.

I am trying the following:

  1. Based on the tutorial scripts, login with a user (from fake_user_db) and receive a token. Use this token for further requests. This works!
  2. Send an Auth0 token to an endpoint I have created and access the API. With the implemented checking code I already had with Hug, this works as well!

Now I want to create logic to test either the jwt.decode based on the tutorial scripts or jwt.decode based on the Auth0 token.

I have come up with two approaches:

  1. When sending the Auth0 token, add a header, which can be checked and returned from a custom OAuth2PasswordBearer class to be used in the get_current_user method.
  2. Change the get_current_user method and perform two try and except clauses to check if one of the two succeeds.

What do you think is a good approach?

Guys, there's a new version that should facilitate all this a bit more: 0.11.0.

Allowing you to use the classes directly in many cases, instead of having to subclass them.

The PR is here: https://github.com/tiangolo/fastapi/pull/134

In summary, you can now pass a parameter auto_error=False to the security utils, e.g.:

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token", auto_error=False)

Then, in your dependency, it's the same:

async def get_current_user(token: str = Security(oauth2_scheme)):
    if token is None:
        # Not authenticated with oauth2_scheme, maybe with other scheme?
        # Raise here, or check for other options, or allow a simpler version for the anonymous users
        raise HTTPException(status_code=403, detail="Not authenticated")
    ...

But now, when the used didn't authenticate using this oauth2_scheme, it won't give an HTTP error 403 "Not authenticated" automatically to the client, instead, it will set the parameter token to None. Then you can check other security schemes, allow for anonymous users or anything else.

With this, you should be able to define multiple Security dependencies in get_current_user, allowing user/password and any other schemes.

@tiangolo Thanks for the update and your work! I will try to test and implement this and report the results back.

Great! Thanks.

There's a few more things I'm struggling with, but I'm really wondering if maybe a more thorough example might help out the broader community.

Specifically, I'm looking at the OAuthFlows model:

class OAuthFlows(BaseModel):
    implicit: Optional[OAuthFlowImplicit] = None
    password: Optional[OAuthFlowPassword] = None
    clientCredentials: Optional[OAuthFlowClientCredentials] = None
    authorizationCode: Optional[OAuthFlowAuthorizationCode] = None

And per its layout, there can only be one of each flow. One implicit, one password, etc. So if you want multiple flows available for different providers, that breaks the above model. That is to say, you can't really do this:

class APISecurityScheme(OAuth2):
    def __init__(self, ...):
        ...
        flows = OAuthFlowsModel(
            password_provider_1={"tokenUrl": ...}
            password_provider_2={"tokenUrl": ...}
        )

You can define multiple schemes, it seems, but that also gets messy, especially in terms of injecting them into dependencies.

Does anyone know of a project currently implementing FastAPI's OAuth2 support for a variety of providers and flows? Especially, perhaps, one that implements Google, Github, and local auth?

@kkinder So, right to the point:

Let's say I want to have Facebook, Google, GitHub, and any other of these systems as valid login providers. And I'm actually not interested in interacting with those providers directly, at least for this example. So, I don't want to create GitHub repos or write Facebook posts in the name of my users. I just want them to be able to login to my app.

What I would do, and I plan on writing about it and including it in the project generators, is create a normal username (email)/password security scheme.

Then I would have a frontend with a login page having the buttons for the social logins I want, let's say Facebook, and also the normal email/password login option. It would perform the social login in the frontend directly. Underneath, my frontend JS would be using the implicit flow of Facebook, but that's not declared in my app, only in Facebook's. When the user clicks the Facebook login button, it would do all the OAuth2 dance with Facebook, automatically handled by the frontend facebook SDK. In the final step of that, my JS would receive a Facebook access_token (the same for Google, GitHub, etc).

Here's the key part:

Then my frontend would send that Facebook acess_token to a special path operation in my app. This path operation receives the social access_token, communicates with the social login provider with my app's client_id and client_secret (provided by Facebook) and verifies the Facebook token from the user. Then it gets the Facebook's user information for this user. From that, I can find that same user in my app, and get my app's user information, including the permissions this user has, roles, etc. all those things that Facebook doesn't know about (because they belong to my app directly). And then, I would generate an app access_token, independent of the Facebook access_token and return it to the frontend. This app access_token would have all the same information as the password/login access token. Including the app-dependent scopes (that Facebook doesn't even know about).

And then, my frontend would perform all the interactions with my app/API using the token generated from my app itself. Even with its own expiration date. That could be independent of the original expiration date of the Facebook acess_token.

For this to work I wouldn't even need to declare in my app an additional OAuth2 flow, the password one would be enough. But if we want to be strict with the OpenAPI/OAuth2 standard and have all that flow "properly" documented, I would create an OAuth2 implicit flow, with the authorizationUrl pointing to that frontend login URL. This would be an actual correct OAuth2 implicit flow, that lives beside the password flow. It's just that in the frontend side authorization page it actually goes and communicates with other providers. But my app keeps being a standalone fully compliant OAuth2 system.


Now, technical details about your previous questions, having only one type of flow per security scheme is part of OpenAPI, is not a limitation at the FastAPI level.

Nevertheless, you can have multiple security schemes with different flows. And all of them can be added to a single path operation, making any of them be a valid security scheme to access your API.

Now, all this OAuth2 stuff and declarations in OpenAPI is actually targeted at the OAuth2 provider, not the clients.

So, social login could be used independent of OpenAPI, FastAPI, how you handle authentication in your app, and even if your app has any OAuth2 related feature or not.

All these OAuth2 features that get integrated into your OpenAPI (generated automatically by FastAPI) are targeted mainly at the OAuth2 authentication provider. That means that, with FastAPI, you can build your own OAuth2 provider equivalent to Facebook, Google, GitHub, etc. You could have third-party applications that do social login using Facebook, Google, GitHub, AND your app.

Or you could use your FastAPI app as a central single-sign-on place for other applications (that can be built with FastAPI or anything else).

It also means that if you use the scopes, you would have a fully documented API, including permissions, using the standards with all the features. In fact, probably a bit more compliant than some of these providers :wink:

Or you could even create more exotic features, for example, having a way to allow your users to generate access tokens that are not even necessarily associated with them, but that have some permissions (scopes) that the users' themselves decide, allowing a third party (let's say, a robot) to access some resources that the user has some control of, while still having an expiration time. Imagine allowing the Amazon robot to deliver the package for you in your garage, not allowing him to enter anywhere else in the house, and only for a limited time frame, all remotely.

I know that example might probably seem too weird and maybe complex. But by being able to create a fully compliant OAuth2 application, with FastAPI doing the heavy lifting for you, it would be fairly easy to achieve.

@tiangolo Thanks for the elaborate comment!

I do have a question about this and that is how you know when to check which authentication provider:

Then my frontend would send that Facebook acess_token to a special path operation in my app. This path operation receives the social access_token, communicates with the social login provider with my app's client_id and client_secret (provided by Facebook) and verifies the Facebook token from the user.

For example is this something like: /facebook-login and /google-login ?

Later on, I see this:

Nevertheless, you can have multiple security schemes with different flows. And all of them can be added to a single path operation, making any of them be a valid security scheme to access your API.

So if I understand correctly, I don't need a specific path operation and can use one endpoint like:
/social-login which can support Facebook, Google, GitHub login?

But how do you know which provider should be checked when the endpoint gets a request?

Is this provided by a header from the frontend?

[...] (the same for Google, GitHub, etc).

Sadly GitHub doesn't allow for Implicit flow so Authorization Code is required.

I'd like to make an app fully based on GitHub authentication, I couldn't get it to work with Swagger UI. I would expect the following:

  1. When I click on Authorize, Swagger redirects to GitHub with the proper client id and URLs
  2. GitHub auth is taking place, then browser redirects to the callback endpoint
  3. In the callback endpoint, the code is used to get the token which is given back to swagger so that Swagger never sees the client secret of the backend

However, I cannot inject the clientId in 1. (edit: it looks like it is possible) and 3. is linked to an endpoint that just let Swagger do the job. GitHub is returning 404 on OPTIONS on the access_token endpoint due to CORS.
Maybe I can try to wrap all that in my own authentication layer but that defeats the purpose a little...

Is there a working Social connect example out there that I would've missed?

Thanks

Hi Diaoul!
I write an article about Google as an external authentication provider: https://medium.com/data-rebels/fastapi-google-as-an-external-authentication-provider-3a527672cf33

It does not include integration with the Swagger UI login button, but it can be a start!

Great article, I've already read it. However I'm focusing on having Swagger doing the Authorization Code flow properly: as the user agent, not the client app.

If you take a look at the picture below, you'll notice that currently Swagger behaves as the Client App and the User Agent, it tries to perform the whole dance with the code which is not the way it is supposed to be used.

image

Things start to fail at the step Follow redirect to the Web Server, Swagger does it and the endpoint is here. What this code does is tell Swagger to do the job itself (i.e. Present Authorization Code step is from the User Agent instead of the Web Server). GitHub does not allow that (for good reasons: CORS) and I don't think this is the way it is supposed to be used. Unless maybe the Client App is also the Authorization Server.
Then I guess the Access Token could be sent to Swagger for other requests or it could be used by the Client App to generate some user model and another level of token.

@tiangolo I am curious to know your thoughts on integrating Authlib with FastAPI for handling Oauth stuff: it presents a very simple API and is well tested. I think it could remove a lot of the complexity in the discussion above.

https://authlib.org/

authlib documentation already has a section about Using FastAPI 馃帀

@mandarvaze @jonathanunderwood The preceding works for the auth flow Google uses, but not the one GitHub uses. What's still unknown is a solution for using GitHub's flow with FastAPI's OAuthFlowAuthorizationCode.
What's more, with authlib, as suggested, there's an open issue with the AuthorizationCode flow: https://github.com/lepture/authlib/issues/175

Hi @ekalosak, the latest Authlib v0.14.3 should fixed your problem.

And here we have a documentation section for FastAPI: https://docs.authlib.org/en/latest/client/fastapi.html

It contains examples of OAuth 1.0 (via Twitter) and OpenID Connect / OAuth 2.0 (via Google).

Hi @lepture,

The Authlib integration for starlette works great. Sadly it does not integrate with the openAPI schema. Perhaps you have some pointers on that front?

Cheers
M

Is there a way to integrate the authlib with the openapi schema?

I wish I had visited this thread after June since I see there are now more options. Sadly I was working on this on July and had to figure out how to do it without Authlib.. All the comments above were fantastic help!

I managed to get the authorisation code flow work with Google and Azure and created a demo here in case someone might find it useful. It's based on @tiangolo's suggestion above, with the only difference that it uses the authorisation code flow to authenticate with Google/Azure instead of the implicit one.

Here's the diagram.
Login

Keep up the great work all.

Thanks for response, this is really helpful!!

Has there been any solution in integrating Authlib with the OpenAPI schema?

@tiangolo I have a question related to this subject.

So I get the bearer token from an external OAuth server and I juste give the token to fastapi.

I wanted that in openapi UI with the Authorize button I have just to give only the token and not having to give a user/password.

I tried to create a Custom OAuth implementation like:

class CustomOAuth2(OAuth2):
    def __init__(self, tokenUrl: str):
        flows = OAuthFlows(clientCredentials={"tokenUrl": tokenUrl})
        super().__init__(flows=flows, scheme_name=None, auto_error=False)

but for some reason it doesn't work.

There is a Fastapi model implementation for my use case ?

I'm struggling with that ... Any idea ?

Was this page helpful?
0 / 5 - 0 ratings