<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>FastAPI on Naveen Kannan</title><link>https://naveenkannan.dev/tags/fastapi/</link><description>Recent content in FastAPI on Naveen Kannan</description><generator>Hugo</generator><language>en</language><lastBuildDate>Sun, 26 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://naveenkannan.dev/tags/fastapi/index.xml" rel="self" type="application/rss+xml"/><item><title>Configuring FastAPI for use with PowerBI when using Entra ID for OAuth2.</title><link>https://naveenkannan.dev/posts/fastapi_entraid_redirect/</link><pubDate>Sun, 26 Jul 2026 00:00:00 +0000</pubDate><guid>https://naveenkannan.dev/posts/fastapi_entraid_redirect/</guid><description>A guide to enabling a FastAPI integrated with Entra ID to be accessible via PowerBI.</description><content:encoded><![CDATA[<h1 id="introduction">Introduction</h1>
<p><a href="https://fastapi.tiangolo.com/">FastAPI</a> is a framework for building APIs with Python based on standard Python type hints. It&rsquo;s fast and easy to stand up, and as a Python developer, it was relatively easy to pick up and get started with it.</p>
<p>As part of deploying a FastAPI instance in production, I integrated FastAPI with Entra ID for OAuth2 via <a href="https://vibber-ai.github.io/fastapi-azure-auth/">this really cool library, FastAPI-Azure-Auth.</a></p>
<p>After integrating this library into my FastAPI instance, I was able to secure access to the API server. However, I quickly ran into an interesting problem. While trying to connect a PowerBI instance to the API, I found that the API settings needed some more configuration to enable interactive logging in via Microsoft SSO within PowerBI.</p>
<h2 id="some-context">Some context</h2>
<p>FastAPI needs to be configured to return a <code>WWW-Authenticate</code> header when a user attempts to access the API without authorization (a <code>HTTP 401 Unauthorized</code>) error.</p>
<p>When an unauthenticated access request is made to a secure API, it can be configured to return a header called <code>WWW-Authenticate</code> within it&rsquo;s response when it returns a <code>HTTP 401 Unauthorized</code> error.</p>
<p>The <code>WWW-Authenticate</code> header returns information that can be used for the user to authenticate themselves and return to the API with a valid, unexpired authentication token.</p>
<p>As there is no element of interaction with a HTTP API, the onus is on the application/user that is querying the API to extract the information returned in the <code>WWW-Authenticate</code> header and to proceed to the identity provider (in this case, Microsoft Entra ID.)</p>
<p>In our case, when PowerBI (via Power Query) requests access to a Web API via an Organizational account, it sends a request to the endpoint to the URL with an empty bearer token. It then expects a <code>WWW-Authenticate</code> header along with the Microsoft Entra ID authorization URI to use. This authorization URI should return the tenant that is to be used for the OAuth2 process. Therefore, all we need to do is configure an error handler within the FastAPI instance to return a <code>WWW-Authenticate</code> header when it returns a <code>HTTP 401 Unauthorized</code> error.</p>
<p>The <a href="https://learn.microsoft.com/en-us/power-query/connector-authentication">Power Query documentation</a> documents the supported workflow when trying to access a Web API via an Organizational account.</p>
<p>From the documentation, this is the reponse expected by Power Query when it is given a <code>HTTP 401 Unauthorized</code> error.</p>
<pre tabindex="0"><code>HTTP/1.1 401 Unauthorized
Cache-Control: private
Content-Type: text/html
Server:
WWW-Authenticate: Bearer authorization_uri=https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/oauth2/authorize
Date: Wed, 15 Aug 2018 15:02:04 GMT
Content-Length: 49
</code></pre><p>So to sum it up, we need to configure FastAPI to return a <code>WWW-Authenticate</code> header when it returns a <code>HTTP 401 Unauthorized</code> error, and this header needs to return the authorization URI associated with the tenant that is able to access the API service via OAuth2.</p>
<h2 id="the-solution">The Solution</h2>
<p>Note that this guide assumes that you have already configured your FastAPI instance to work with Entra ID for OAuth2 flows.</p>
<p>As a general example (which may differ from your set-up), I usually have my code that handles the Entra ID authentication flow within a folder called <code>modules</code> in the project root, in a file called <code>azure.py</code>, which I can then import into my <code>main.py</code> file.</p>
<p>Following from the basic example within the FastAPI-Azure-Auth documentation, here&rsquo;s what <code>azure.py</code> looks like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> fastapi_azure_auth <span style="color:#f92672">import</span> SingleTenantAzureAuthorizationCodeBearer
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> pydantic <span style="color:#f92672">import</span> AnyHttpUrl, computed_field
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> pydantic_settings <span style="color:#f92672">import</span> BaseSettings
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Settings</span>(BaseSettings):
</span></span><span style="display:flex;"><span>    BACKEND_CORS_ORIGINS: list[str <span style="color:#f92672">|</span> AnyHttpUrl] <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;https://your.server.com&#34;</span>,
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>    OPENAPI_CLIENT_ID: str <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    APP_CLIENT_ID: str <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    TENANT_ID: str <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    SCOPE_DESCRIPTION: str <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;user_impersonation&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@computed_field</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@property</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">SCOPE_NAME</span>(self) <span style="color:#f92672">-&gt;</span> str:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;api://</span><span style="color:#e6db74">{</span>self<span style="color:#f92672">.</span>APP_CLIENT_ID<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">{</span>self<span style="color:#f92672">.</span>SCOPE_DESCRIPTION<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@computed_field</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@property</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">SCOPES</span>(self) <span style="color:#f92672">-&gt;</span> dict:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> {
</span></span><span style="display:flex;"><span>            self<span style="color:#f92672">.</span>SCOPE_NAME: self<span style="color:#f92672">.</span>SCOPE_DESCRIPTION,
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@computed_field</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@property</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">OPENAPI_AUTHORIZATION_URL</span>(self) <span style="color:#f92672">-&gt;</span> str:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;https://login.microsoftonline.com/</span><span style="color:#e6db74">{</span>self<span style="color:#f92672">.</span>TENANT_ID<span style="color:#e6db74">}</span><span style="color:#e6db74">/oauth2/v2.0/authorize&#34;</span>
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@computed_field</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@property</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">OPENAPI_TOKEN_URL</span>(self) <span style="color:#f92672">-&gt;</span> str:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;https://login.microsoftonline.com/</span><span style="color:#e6db74">{</span>self<span style="color:#f92672">.</span>TENANT_ID<span style="color:#e6db74">}</span><span style="color:#e6db74">/oauth2/v2.0/token&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>settings <span style="color:#f92672">=</span> Settings()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>azure_scheme <span style="color:#f92672">=</span> SingleTenantAzureAuthorizationCodeBearer(
</span></span><span style="display:flex;"><span>    app_client_id<span style="color:#f92672">=</span>settings<span style="color:#f92672">.</span>APP_CLIENT_ID,
</span></span><span style="display:flex;"><span>    tenant_id<span style="color:#f92672">=</span>settings<span style="color:#f92672">.</span>TENANT_ID,
</span></span><span style="display:flex;"><span>    scopes<span style="color:#f92672">=</span>settings<span style="color:#f92672">.</span>SCOPES,
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div>
    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>This example doesn&rsquo;t go into how you supply the module with the required <code>OPENAPI_CLIENT_ID</code>, <code>APP_CLIENT_ID</code> and <code>TENANT_ID</code> variables.
It is good practice to use a secret vault to supply these variables when calling <code>Settings()</code> to override the default empty strings.
For example:</p>
<blockquote>
<pre tabindex="0"><code>settings = Settings(
   APP_CLIENT_ID=app_client_id,
   OPENAPI_CLIENT_ID=openapi_client_id,
   TENANT_ID=tenant_id,
)
</code></pre></blockquote>
<p>In this example, the three variables supplied to <code>Settings()</code> have been called from a secret vault.</p>
      </div>
    </div><h2 id="the-error-handler">The Error Handler</h2>
<p>Define a custom error handler within your FastAPI code. For example, create a folder called <code>modules</code> in your project root, and create a file called <code>errors.py</code> within <code>modules</code>. Put the following in that file.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> fastapi <span style="color:#f92672">import</span> Request, HTTPException
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> fastapi.responses <span style="color:#f92672">import</span> JSONResponse
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> modules <span style="color:#f92672">import</span> azure
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">custom_http_exception_handler</span>(request: Request, exc: HTTPException):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> exc<span style="color:#f92672">.</span>status_code <span style="color:#f92672">==</span> <span style="color:#ae81ff">401</span>:
</span></span><span style="display:flex;"><span>        auth_url <span style="color:#f92672">=</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;https://login.microsoftonline.com/</span><span style="color:#e6db74">{</span>azure<span style="color:#f92672">.</span>settings<span style="color:#f92672">.</span>TENANT_ID<span style="color:#e6db74">}</span><span style="color:#e6db74">/oauth2/authorize&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> JSONResponse(
</span></span><span style="display:flex;"><span>            status_code<span style="color:#f92672">=</span><span style="color:#ae81ff">401</span>,
</span></span><span style="display:flex;"><span>            content<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;detail&#34;</span>: <span style="color:#e6db74">&#34;Not authenticated&#34;</span>},
</span></span><span style="display:flex;"><span>            headers<span style="color:#f92672">=</span>{
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;WWW-Authenticate&#34;</span>: <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;Bearer authorization_uri=&#34;</span><span style="color:#e6db74">{</span>auth_url<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;&#39;</span>
</span></span><span style="display:flex;"><span>            },
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> JSONResponse(
</span></span><span style="display:flex;"><span>            status_code<span style="color:#f92672">=</span>exc<span style="color:#f92672">.</span>status_code, 
</span></span><span style="display:flex;"><span>            content<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;detail&#34;</span>: exc<span style="color:#f92672">.</span>detail})
</span></span></code></pre></div><p>In this code snippet, we are doing the following:</p>
<ul>
<li>When a HTTP Exception occurs, the functions checks to see if a 401 error is returned.</li>
<li>We define the URL for OAuth2 authorization flow by using the tenant ID taken from the Azure config.</li>
<li>We construct and return the <code>WWW-Authenticate</code> header to be compliant with the format expected by Power Query.</li>
<li>For HTTP Errors that are not 401, the default response is passed back.</li>
</ul>
<p>We can then import this error handler in <code>main.py</code> as follows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> Annotated, AsyncGenerator
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> contextlib <span style="color:#f92672">import</span> asynccontextmanager
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> starlette.middleware.cors <span style="color:#f92672">import</span> CORSMiddleware
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> fastapi <span style="color:#f92672">import</span> FastAPI, HTTPException, Security
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> fastapi_azure_auth.user <span style="color:#f92672">import</span> User
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> modules <span style="color:#f92672">import</span> azure
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> modules.errors <span style="color:#f92672">import</span> custom_http_exception_handler
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>settings <span style="color:#f92672">=</span> azure<span style="color:#f92672">.</span>settings
</span></span><span style="display:flex;"><span>azure_scheme <span style="color:#f92672">=</span> azure<span style="color:#f92672">.</span>azure_scheme
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@asynccontextmanager</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">lifespan</span>(app: FastAPI) <span style="color:#f92672">-&gt;</span> AsyncGenerator[<span style="color:#66d9ef">None</span>, <span style="color:#66d9ef">None</span>]:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    Load OpenID config on startup.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">await</span> azure_scheme<span style="color:#f92672">.</span>openid_config<span style="color:#f92672">.</span>load_config()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">yield</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>app <span style="color:#f92672">=</span> FastAPI(
</span></span><span style="display:flex;"><span>    lifespan<span style="color:#f92672">=</span>lifespan,
</span></span><span style="display:flex;"><span>    swagger_ui_oauth2_redirect_url<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/api/oauth2-redirect&#34;</span>,
</span></span><span style="display:flex;"><span>    swagger_ui_init_oauth<span style="color:#f92672">=</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;usePkceWithAuthorizationCodeGrant&#34;</span>: <span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;clientId&#34;</span>: settings<span style="color:#f92672">.</span>OPENAPI_CLIENT_ID,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;scopes&#34;</span>: settings<span style="color:#f92672">.</span>SCOPE_NAME,
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> settings<span style="color:#f92672">.</span>BACKEND_CORS_ORIGINS:
</span></span><span style="display:flex;"><span>    app<span style="color:#f92672">.</span>add_middleware(
</span></span><span style="display:flex;"><span>        CORSMiddleware,
</span></span><span style="display:flex;"><span>        allow_origins<span style="color:#f92672">=</span>[str(origin) <span style="color:#66d9ef">for</span> origin <span style="color:#f92672">in</span> settings<span style="color:#f92672">.</span>BACKEND_CORS_ORIGINS],
</span></span><span style="display:flex;"><span>        allow_credentials<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>        allow_methods<span style="color:#f92672">=</span>[<span style="color:#e6db74">&#34;*&#34;</span>],
</span></span><span style="display:flex;"><span>        allow_headers<span style="color:#f92672">=</span>[<span style="color:#e6db74">&#34;*&#34;</span>],
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>app<span style="color:#f92672">.</span>add_exception_handler(HTTPException, custom_http_exception_handler)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@app.get</span>(<span style="color:#e6db74">&#34;/&#34;</span>, dependencies<span style="color:#f92672">=</span>[Security(azure_scheme)])
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">root</span>():
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> {<span style="color:#e6db74">&#34;message&#34;</span>: <span style="color:#e6db74">&#34;Hello World&#34;</span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">&#39;__main__&#39;</span>:
</span></span><span style="display:flex;"><span>    uvicorn<span style="color:#f92672">.</span>run(<span style="color:#e6db74">&#39;main:app&#39;</span>, reload<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span></code></pre></div><p>With this error handler imported, PowerBI should be able to use the Power Query Connector to enable SSO via your Microsoft login when it is attempting to connect to your API!</p>
<h1 id="references">References</h1>
<ul>
<li><a href="https://vibber-ai.github.io/fastapi-azure-auth/">FastAPI-Azure-Auth documentation</a></li>
<li><a href="https://learn.microsoft.com/en-us/power-query/connector-authentication">Microsoft Learn documentation on the Power Query connector and authentication flows</a></li>
<li><a href="https://www.rfc-editor.org/info/rfc7235/#section-4.1">Documenation on the <code>WWW-Authenticate</code> header standards</a></li>
</ul>
]]></content:encoded></item></channel></rss>