using external oAuth providers
matthdsm opened this issue ยท 52 comments
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
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.
(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 ๐ ๐ฎ
It's asynchronous time-traveling chain-lightning black magic and it totally rocks imo.
Loved that, I'm citing it ๐ ๐
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
: โ๏ธ this is the one where your frontend gets the token directly with the authentication. And then sends it to the FastAPI backend.OAuthFlowPassword
: โ this probably doesn't apply to you, because the frontend sends the password to Hydra directly, not to FastAPI.OAuthFlowClientCredentials
: โ I guess this doesn't apply either, as your public frontend is the one authenticating your users, not a secure backend.OAuthFlowAuthorizationCode
: โ๏ธ 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. ๐
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.
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:
- 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!
- 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:
- 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.
- 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: #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 ๐
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:
- When I click on Authorize, Swagger redirects to GitHub with the proper client id and URLs
- GitHub auth is taking place, then browser redirects to the callback endpoint
- 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.
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.
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: lepture/authlib#175
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.
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 ?
@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.
I don't think authlib should be integrated with FastAPI. Not because it isn't great, simply because it costs money for commercial usage: https://authlib.org/plans. Would be a shame if that was necessary for FastAPI too then imo.
[...] (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:
- When I click on Authorize, Swagger redirects to GitHub with the proper client id and URLs
- GitHub auth is taking place, then browser redirects to the callback endpoint
- 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
@Diaoul did you find a solution yet? I'm facing the same issue; that the redirect endpoint is managed by swagger ui
No I did not sadly.
Have a look at this https://github.com/dorinclisu/fastapi-auth0
It integrates with auth0, and you can add any social provider you want with a few clicks in auth0 dashboard.
@Diaoul, I was able to get the authorization code flow to work with a Microsoft Azure Identity Platform App Registration. Like GitHub (which I guess is also Microsoft), Microsoft Azure suggests against the implicit flow pattern.
There were a couple subtle features of both FastAPI and MS that I had to use, maybe these will work for GitHub:
-
The FastAPI class exposes the Swagger UI OAuth 2.0 configuration with the
swagger_ui_init_oauth
optional parameter, defined here: https://github.com/tiangolo/fastapi/blob/33be5fc8baa02bae949e1986f1821dae8f84f487/fastapi/applications.py#L48 This is great, because you can tweak the OAuth2 params to your liking. Yet another incredible example of the customization @tiangolo has thought to include by exposing the libraries under-the-hood! -
On the MS side, I had to specify that my client (Swagger UI) is a single-page app (spa) for the purposes of the redirect URI. I also had to whitelist the redirect URI, e.g. for local testing that's
http://localhost/docs/oauth2-redirect
. Note that the redirect path is also a configuration parameter in theFastAPI
class and is automatically relative to the base path of the app.Specifying the spa redirect allows the CORS on the MS side (not sure why that is-- hence the subtle nature of this feature), but it didn't immediately work because Azure also requires a PKCE
-
Luckily, enabling a PKCE is one of the options that the Swagger config offers and FastAPI exposes. Once I did that, the Swagger OAuth2 dance just worked.
In the end, the configuration that works for me in FastAPI is:
from fastapi import FastAPI
from fastapi.security import OAuth2AuthorizationCodeBearer
from app.core.config import settings
oauth2_scheme = OAuth2AuthorizationCodeBearer(
authorizationUrl=settings.AUTHORIZATION_URL,
tokenUrl=settings.TOKEN_URL
)
app = FastAPI(
title=settings.PROJECT_NAME, openapi_url=f"{settings.API_V1_STR}/openapi.json",
swagger_ui_init_oauth = {
"usePkceWithAuthorizationCodeGrant": True,
"clientId": settings.CLIENT_ID,
"scopes": settings.OAUTH_SCOPES
}
)
Where I have put all the OAuth2 urls and client_id in my Pydantic settings class. I choose to pass the client id and scopes directly to Swagger because, in my case, there is no need to expose them for the user to select in the Swagger UI.
If you have granular scopes, you should pass those to the OAuth2AuthorizationCodeBearer
class instead, and FastAPI will make the nice Swagger UI check boxes for your user to select from.
Let me know if this works, perhaps I can make a docs PR for the swagger_ui_init_oauth
parameter if there isn't one already.
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.
@rcox771 Just wanted you to know, I was curious about Depends
, so I googled it. Just Depends
, no context. Pray for me and all of the targeted ads I'm about to get for incontinence. ๐
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.@rcox771 Just wanted you to know, I was curious about
Depends
, so I googled it. JustDepends
, no context. Pray for me and all of the targeted ads I'm about to get for incontinence. ๐
Whoa! I should've thought to cite @Tiango's docs. Sorry for sending you into the diaper dimension @adamhp . Diapers are cool and all, but Dependency Injection is on another level. Here are the references you seek:
Getting Started
Advanced Guide
Hope that helps!
I can confirm @nadirsidi's code works with an external OAuth and OpenID providers like Azure AD:
oauth2_scheme = OAuth2AuthorizationCodeBearer(
authorizationUrl=settings.AUTH_URL,
tokenUrl=settings.TOKEN_URL
)
Should work with other OAuth and OpenID providers as well, for as long as an access token is returned from the token endpoint. Without access_token
being returned from the provider's token endpoint though, requests made from the /docs site will not include any token, so might want to keep a look out on this issue because it did get me wondering why my API did not receive a token...
I can confirm @nadirsidi's code works with an external OAuth and OpenID providers like Azure AD:
oauth2_scheme = OAuth2AuthorizationCodeBearer( authorizationUrl=settings.AUTH_URL, tokenUrl=settings.TOKEN_URL )Should work with other OAuth and OpenID providers as well, for as long as an access token is returned from the token endpoint. Without
access_token
being returned from the provider's token endpoint though, requests made from the /docs site will not include any token, so might want to keep a look out on this issue because it did get me wondering why my API did not receive a token...
@ionizer, glad that configuration is working for you!
Regarding the access token, and I'm no OAuth2 expert, I think you may be confusing authentication with authorization-- good summary of the difference here.
For the purposes of obtaining a token to enforce endpoint authorization, you want to follow the full, Authorization Code Flow, see also one of the references from that page-- What is the OAuth 2.0 Authorization Code Grant Type?. This is a multiple step process that will first get an authentication token from the OpenID Connect provider (e.g. MS Azure Active Directory), which you then use in a subsequent request to get the access token for authorization purposes. Swagger/FastAPI does this dance for you if configured correctly. However, it's then up to you to validate the tokens or integrate with a service (e.g. API Gateway v2) that will check the tokens on your behave.
The authentication versus authorization split is a good thing, because you can have people in your org who can authenticate but may not be authorized to access certain endpoints. By configuring your token scopes and assigning roles in your IAM system, you can obtain this granular control.
The fact remains that Swagger looks for an access token when making the request to a protected API, so it is required to make sure that an access_token
is being returned after exchanging the authorization code for token(s). Otherwise, Swagger will just send the request like so (note on the Authorization header):
curl -X 'GET' \
'http://localhost:8000/protected' \
-H 'accept: application/json' \
-H 'Authorization: Bearer undefined'
And in case of Azure AD B2C, they don't return access token by default unless a client id is specified in the scope (because of defaulting to OpenID flow?). So for example, to get an access token from Azure AD B2C:
oauth2_scheme = OAuth2AuthorizationCodeBearer(
authorizationUrl=AZURE_AUTH_URL,
tokenUrl=AZURE_TOKEN_URL,
# Reference: https://docs.microsoft.com/en-us/azure/active-directory-b2c/access-tokens#openid-connect-scopes
scopes=["<AZURE_AD_CLIENT_ID>", "openid", "offline_access", "profile"]
)
Thinking back about it, I think it would probably be nice if we can do PKCE flow, or being able to support multiple auth providers ๐
I agree, you need to have an access_token
to use in the Authorization
header.
In my project, using Azure AD, I have found I can define the application client ID, scope, and PKCE flow through the FastAPI instance. These settings then flow through to Swagger. The swagger_ui_init_oauth
exposes the Swagger OAuth 2.0 configuration, but most of the time the FastAPI constructor sets them to None
-- Source.
Here is the Swagger docs that list what keys you can define in that dictionary.
Based upon that doc, here's the configuration that works well for me:
app = FastAPI(
title=settings.PROJECT_NAME,
openapi_url=f"{settings.API_V1_STR}/openapi.json",
swagger_ui_init_oauth={
"usePkceWithAuthorizationCodeGrant": True,
"clientId": settings.CLIENT_ID,
"scopes": settings.OAUTH_SCOPES,
},
)
I also do the dependency injection of the oauth2_scheme
on my router:
app.include_router(
api_router, prefix=settings.API_V1_STR, dependencies=[Depends(oauth2_scheme)]
)
I can't speak to whether this configuration works for B2C, but I don't see why it wouldn't. My use-case is a tenant-specific application, so all our users are members of our tenant's active directory. I am also doing the token validation in AWS at the API Gateway v2 ("HTTP API") layer, so in FastAPI-land I only need to make sure:
- There's a token passed in the header, which satisfies the dependency-- doesn't matter what the token is. For testing I can just pass through a token like
Bearer foo
. - The Swagger Authorization Flow works so I can hit my API manually from the docs.
The actual token verification is done before my FastAPI is invoked by AWS & MS on my behalf. It's only tangentially related, but happy to share that configuration if anyone is interested.
If you weren't doing that, you should just have to add the token verification as per the FastAPI docs. All the PKCE/OAuth stuff is just to get the token on the app-side for a single-page app.
Hi @nadirsidi .
Thanks a lot for sharing this info !
Unfortunately, seems like I can't get this to work with my application.
When I click on the Authorize button on the swagger UI, I get redirected to my Auth provider login page.
I then enter my credentials, and when redirected to the swagger, the following error pops up : Auth error - TypeError: Failed to fetch
.
From what I see in the browser console, this seems to be a CORS related issue but I don't understand how I can get rid of it.
Any idea ?
Hi @nadirsidi .
Thanks a lot for sharing this info !Unfortunately, seems like I can't get this to work with my application.
When I click on the Authorize button on the swagger UI, I get redirected to my Auth provider login page.
I then enter my credentials, and when redirected to the swagger, the following error pops up :Auth error - TypeError: Failed to fetch
.From what I see in the browser console, this seems to be a CORS related issue but I don't understand how I can get rid of it.
Any idea ?
Hi @jmereaux, I was also recently up against a CORS issue. Learned a lot, so maybe I can help.
The Mozilla Foundation has some great information on CORS, I recommended checking that out. Here's the first page that is the entry point to the rabbit hole that is CORS.
It's good news that the preflight is working. I believe that means your host is authorized to call some resources at the cross-origin domain. Take a look at the response from the OPTIONS
preflight and make sure it lists your originating domain and any HTTP methods you'll use on subsequent calls. Note that there are certain headers, specifically authorization
in our specific case that requires CORS options to NOT use the *
wildcard for certain properties. For example, you need to make sure you're originating domain is specifically whitelisted by the backend CORS configuration.
EDIT: It may seem silly, but if you're on localhost, make sure you've temporarily enabled your backend CORS config to allow http://localhost:80-- Note that the protocol, domain, and port constitute the unique origin.
Then I would look into the raw request for that "fetch CORS error" thing. Hopefully you'll see that your Swagger app is trying to make a request that your backend hasn't configured for CORS. Then the thing to do would be to update your backend CORS configuration.
What is your backend in this case?
Good luck, here be dragons in my experience. Lost a lot of time figuring this stuff out.
Hi @nadirsidi and thanks for taking the time to answer.
In my case, the backend is my FastAPI application and the authentication provider is IBM Cloud AppId.
Below is the CORS configuration I put in the FastAPI app :
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["*"]
)
On the IBM Cloud AppID side, I've declared the FastAPI app in the redirect urls section (http://localhost:8000/*):
Here is more details regarding the OPTIONS preflight request :
A couple things. For the authorization
header to work on the subsequent request, you'll need to explicitly list http://localhost:8000 as an allowed_origin
. This is a special requirement for requests with credentials using CORS, see Requests with credentials specifically the sub-section on Credentialed requests and wildcards.
I also specify allow_headers
to be ["Authorization"]
in my API Gateway CORS configuration. You may need the same here.
The other thing I'll warn you is that I believe the FastAPI / Starlette CORS middleware has some bugs. I tried to use it at one point, but I found it didn't work as expected on my router. Then I dug into the GitHub Issues and it looks like there are some open issues in both projects regarding CORS. There seem to be plenty of workarounds, and I am sure people are using it successfully, just a heads up. I can only vouch for CORS as I have it working on AWS API Gateway, which is an abstraction layer above my FastAPI.
Where will you ultimately deploy your FastAPI? You may find it simplifies your Python code to handle CORS in your infrastructure configuration.
Nadir
@nadirsidi I'm a bit confused. As far as I understand from the error message below, this is the Auth Provider (IBM Cloud AppId in my case) that raises the CORS error :
Access to fetch at 'https://eu-de.appid.cloud.ibm.com/oauth/v4/<tenant_id>/token' from origin 'http://localhost:8000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
@jmereaux, I compared what I can see from your preflight response to my preflight response ahead of the /token
request to Azure, and they look the same.
Based upon that message, it sounds like you need to configure CORS in your Auth Provider. In MS Azure, simply adding the redirect URI seems to handle this for me. Perhaps with IBM there is an additional step to get this to work?
I would look around. I suspect that error is not specific to your FastAPI implementation. Sorry I can't be more helpful!
@nadirsidi , how does your Authenticate
button look with the usePkceWithAuthorizationCodeGrant
settings? Mine still asks for a Client Secret :/
EDIT: I see, it just stays there, but it's no longer a required field.
Yes, @JonasKs, I unfortunately see the same. I wish I could suppress both the client id and secret fields, but as you said it works if you leave the secret blank.
There might be some Swagger setting somewhere to change the UI, but I have never bothered to look since our docs are typically only used internally.
No worries, I'll put some docs or overwrite css. Thank you for responding. ๐
I've created an Azure AD authentication (FastAPI-Azure-Auth) package for FastAPI, if someone need inspiration/a package to deal with Azure AD.
The current main
branch only support single tenant apps (with v1
tokens), but when intility/fastapi-azure-auth#16 is merged, multi-tenant support and support for v2
tokens will be available. I'm writing docs as we speak, so hopefully it will be merged soon. (Please keep in mind that this branch contains breaking changes, if you want to implement this, it might be worth waiting a few days)
I would like to use firebase auth as the oAuth provider, and preferably, allow the swagger UI to understand endpoints that require authentication and allow me to auth through the UI.
The firebase auth all occurs client-side, and my API would just accept a token that it would decode and ensure was valid, preferably without any outgoing calls to firebase from the fastapi server.
Is OAuthFlowImplicit
the right approach for this?
Here is my workaround:
- Make endpoints for every social login providers (
/login/twitter
,/login/google
, ...). In these endpoints, use Authlib to get access token from those social login providers. Authlib handles all OAuth client stuff (redirection, OAuth xxx flow work, ...). - Make endpoints (
/auth/twitter
,/auth/google
) for receiving access token from Twitter / Google. - After getting an access code, redirect the user to FastAPI OAuth endpoint (
/auth
), and issue our JWT to the user. The FastAPI OAuth endpoint is what we defined in afastapi.security.OAuth2
class instance. This makes OpenAPI auth working. - Users (browser) don't access Twitter / Google API directly. Users access FastAPI API only. FastAPI accesses Twitter / Google API.
I've created an Azure AD authentication (FastAPI-Azure-Auth) package for FastAPI, if someone need inspiration/a package to deal with Azure AD.
Just got a notification on this thread so thought Iโd update this comment.
This package now has a full tutorial on how to set up everything from scratch and support both single- and multi-tenant applications. ๐
Hi guys,
I'm new to coding, but met the similar problem.
I wonder if there's a way like specifying the tokenUrl
so that received token are checked, like this:
# port 8000 is the base fastapi app
# the code following is in another fastapi app, but use the same user database
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="http://127.0.0.1:8000/api/users/token")
BTW, auth0 seems great, but I just thought the above may be more simple.
I tested, but it doesn't work. Any guidance how to proceed?
Thank you.