Intility / fastapi-azure-auth

Easy and secure implementation of Azure Entra ID (previously AD) for your FastAPI APIs 🔒 B2C, single- and multi-tenant support.
https://intility.github.io/fastapi-azure-auth
MIT License
462 stars 67 forks source link

Auth with React #73

Closed ocni-dtu closed 2 years ago

ocni-dtu commented 2 years ago

Describe the bug

Hey all, Thanks for a great library! I have with success implemented the auth workflow with FastAPI and OpenAPI following your documentation I have a React frontend that needs to talk to the FastAPI backend and I have trouble getting it to work. I use the "@azure/msal-browser" package to get the access token in React, but when I send it in the header to FastAPI I get the following error:

Traceback (most recent call last):
   File "/app/.local/lib/python3.9/site-packages/fastapi_azure_auth/auth.py", line 183, in __call__
     token = jwt.decode(
   File "/app/.local/lib/python3.9/site-packages/jose/jwt.py", line 144, in decode
     raise JWTError(e)
 jose.exceptions.JWTError: Signature verification failed.
 INFO:     127.0.0.1:36092 - "GET /api/project/a833b3ff30 HTTP/1.1" 401 Unauthorized

I have followed the same steps as for OpenAPI to setup my React SPA in Azure.

To Reproduce

  1. Go to React UI
  2. Login to Azure
  3. Catch the token from the callback
  4. Set the token in the header
  5. Call FastAPI

Stack trace

[backend] INFO:     127.0.0.1:36088 - "OPTIONS /api/project/a833b3ff30 HTTP/1.1" 200 OK
[backend] 2022-04-21 12:40:18,023 WARNING fastapi_azure_auth __call__() Malformed token received. null. Error: Error decoding token headers.
[backend] Traceback (most recent call last):
[backend]   File "/app/.local/lib/python3.9/site-packages/jose/jws.py", line 176, in _load
[backend]     signing_input, crypto_segment = jwt.rsplit(b".", 1)
[backend] ValueError: not enough values to unpack (expected 2, got 1)
[backend] 
[backend] During handling of the above exception, another exception occurred:
[backend] 
[backend] Traceback (most recent call last):
[backend]   File "/app/.local/lib/python3.9/site-packages/jose/jwt.py", line 183, in get_unverified_header
[backend]     headers = jws.get_unverified_headers(token)
[backend]   File "/app/.local/lib/python3.9/site-packages/jose/jws.py", line 109, in get_unverified_headers
[backend]     return get_unverified_header(token)
[backend]   File "/app/.local/lib/python3.9/site-packages/jose/jws.py", line 90, in get_unverified_header
[backend]     header, claims, signing_input, signature = _load(token)
[backend]   File "/app/.local/lib/python3.9/site-packages/jose/jws.py", line 180, in _load
[backend]     raise JWSError("Not enough segments")
[backend] jose.exceptions.JWSError: Not enough segments
[backend] 
[backend] During handling of the above exception, another exception occurred:
[backend] 
[backend] Traceback (most recent call last):
[backend]   File "/app/.local/lib/python3.9/site-packages/fastapi_azure_auth/auth.py", line 136, in __call__
[backend]     header: dict[str, str] = jwt.get_unverified_header(token=access_token) or {}
[backend]   File "/app/.local/lib/python3.9/site-packages/jose/jwt.py", line 185, in get_unverified_header
[backend]     raise JWTError("Error decoding token headers.")
[backend] jose.exceptions.JWTError: Error decoding token headers.
[backend] INFO:     127.0.0.1:36088 - "GET /api/project/a833b3ff30 HTTP/1.1" 401 Unauthorized
[backend] 2022-04-21 12:40:18,150 WARNING fastapi_azure_auth __call__() Invalid token. Error: Signature verification failed.
[backend] Traceback (most recent call last):
[backend]   File "/app/.local/lib/python3.9/site-packages/jose/jws.py", line 262, in _verify_signature
[backend]     raise JWSSignatureError()
[backend] jose.exceptions.JWSSignatureError
[backend] 
[backend] During handling of the above exception, another exception occurred:
[backend] 
[backend] Traceback (most recent call last):
[backend]   File "/app/.local/lib/python3.9/site-packages/jose/jwt.py", line 142, in decode
[backend]     payload = jws.verify(token, key, algorithms, verify=verify_signature)
[backend]   File "/app/.local/lib/python3.9/site-packages/jose/jws.py", line 73, in verify
[backend]     _verify_signature(signing_input, header, signature, key, algorithms)
[backend]   File "/app/.local/lib/python3.9/site-packages/jose/jws.py", line 264, in _verify_signature
[backend]     raise JWSError("Signature verification failed.")
[backend] jose.exceptions.JWSError: Signature verification failed.
[backend] 
[backend] During handling of the above exception, another exception occurred:
[backend] 
[backend] Traceback (most recent call last):
[backend]   File "/app/.local/lib/python3.9/site-packages/fastapi_azure_auth/auth.py", line 183, in __call__
[backend]     token = jwt.decode(
[backend]   File "/app/.local/lib/python3.9/site-packages/jose/jwt.py", line 144, in decode
[backend]     raise JWTError(e)
[backend] jose.exceptions.JWTError: Signature verification failed.
[backend] INFO:     127.0.0.1:36088 - "GET /api/project/a833b3ff30 HTTP/1.1" 401 Unauthorized
[backend] 2022-04-21 12:43:06,084 WARNING fastapi_azure_auth __call__() Malformed token received. null. Error: Error decoding token headers.
[backend] Traceback (most recent call last):
[backend]   File "/app/.local/lib/python3.9/site-packages/jose/jws.py", line 176, in _load
[backend]     signing_input, crypto_segment = jwt.rsplit(b".", 1)
[backend] ValueError: not enough values to unpack (expected 2, got 1)
[backend] 
[backend] During handling of the above exception, another exception occurred:
[backend] 
[backend] Traceback (most recent call last):
[backend]   File "/app/.local/lib/python3.9/site-packages/jose/jwt.py", line 183, in get_unverified_header
[backend]     headers = jws.get_unverified_headers(token)
[backend]   File "/app/.local/lib/python3.9/site-packages/jose/jws.py", line 109, in get_unverified_headers
[backend]     return get_unverified_header(token)
[backend]   File "/app/.local/lib/python3.9/site-packages/jose/jws.py", line 90, in get_unverified_header
[backend]     header, claims, signing_input, signature = _load(token)
[backend]   File "/app/.local/lib/python3.9/site-packages/jose/jws.py", line 180, in _load
[backend]     raise JWSError("Not enough segments")
[backend] jose.exceptions.JWSError: Not enough segments
[backend] 
[backend] During handling of the above exception, another exception occurred:
[backend] 
[backend] Traceback (most recent call last):
[backend]   File "/app/.local/lib/python3.9/site-packages/fastapi_azure_auth/auth.py", line 136, in __call__
[backend]     header: dict[str, str] = jwt.get_unverified_header(token=access_token) or {}
[backend]   File "/app/.local/lib/python3.9/site-packages/jose/jwt.py", line 185, in get_unverified_header
[backend]     raise JWTError("Error decoding token headers.")
[backend] jose.exceptions.JWTError: Error decoding token headers.
[backend] INFO:     127.0.0.1:36092 - "GET /api/project/a833b3ff30 HTTP/1.1" 401 Unauthorized
[backend] 2022-04-21 12:43:06,334 WARNING fastapi_azure_auth __call__() Invalid token. Error: Signature verification failed.
[backend] Traceback (most recent call last):
[backend]   File "/app/.local/lib/python3.9/site-packages/jose/jws.py", line 262, in _verify_signature
[backend]     raise JWSSignatureError()
[backend] jose.exceptions.JWSSignatureError
[backend] 
[backend] During handling of the above exception, another exception occurred:
[backend] 
[backend] Traceback (most recent call last):
[backend]   File "/app/.local/lib/python3.9/site-packages/jose/jwt.py", line 142, in decode
[backend]     payload = jws.verify(token, key, algorithms, verify=verify_signature)
[backend]   File "/app/.local/lib/python3.9/site-packages/jose/jws.py", line 73, in verify
[backend]     _verify_signature(signing_input, header, signature, key, algorithms)
[backend]   File "/app/.local/lib/python3.9/site-packages/jose/jws.py", line 264, in _verify_signature
[backend]     raise JWSError("Signature verification failed.")
[backend] jose.exceptions.JWSError: Signature verification failed.
[backend] 
[backend] During handling of the above exception, another exception occurred:
[backend] 
[backend] Traceback (most recent call last):
[backend]   File "/app/.local/lib/python3.9/site-packages/fastapi_azure_auth/auth.py", line 183, in __call__
[backend]     token = jwt.decode(
[backend]   File "/app/.local/lib/python3.9/site-packages/jose/jwt.py", line 144, in decode
[backend]     raise JWTError(e)
[backend] jose.exceptions.JWTError: Signature verification failed.
[backend] INFO:     127.0.0.1:36092 - "GET /api/project/a833b3ff30 HTTP/1.1" 401 Unauthorized
JonasKs commented 2 years ago

Can you de-code your token at https://jwt.io and show me how it looks? If you're not comfortable posting that information online, you can send it to me at jonas.svensson@intility.no.

Also, your react frontend is set up exactly as the OpenAPI app registration?

ocni-dtu commented 2 years ago

Sure here is a ~link to the token~(Removed by @JonasKs)

It says it cannot verify the signature.

Yes, the Azure setup should be the same: Screenshot from 2022-04-25 10-59-52 Screenshot from 2022-04-25 10-57-51

JonasKs commented 2 years ago

Your tokens aud(audience) is for Microsoft Graph and not your backend application. So audience verification fails.

Can you provide some react code so I can see how it has been configured? Could it be that it fetches multiple tokens(maybe silently) and you’re using the wrong one?

If not, could you click login and copy the Azure URL you’re signing in from/redirected to?

JonasKs commented 2 years ago

On a side note: I'm not a frontend developer, but could it be an idea to switch to @azure/msal-react instead of using @azure/msal-browser?

ocni-dtu commented 2 years ago

Thanks for the help @JonasKs! So it turns out that I fucked up 🤦 The mistake was in the React code. Anyways I'm just going to explain below for anyone in the future.

// main.tsx

import { MsalProvider } from "@azure/msal-react";
import { msalInstance } from "./features/utils/aad";

ReactDOM.render(
  <React.StrictMode>
    <MsalProvider instance={msalInstance}>
      <App />
    </MsalProvider>
  </React.StrictMode>,
  document.getElementById("root")
);
// features/utils/aad.tsx

import {
  Configuration,
  RedirectRequest,
  PublicClientApplication,
  EventMessage,
  EventType,
  AuthenticationResult,
} from "@azure/msal-browser";

export const msalConfig: Configuration = {
  auth: {
    clientId: import.meta.env.VITE_AAD_CLIENT_ID as string,
    authority: `https://login.microsoftonline.com/${import.meta.env.VITE_AAD_TENANT_ID}`,
    redirectUri: "http://localhost:3000/login",
  },
  cache: {
    cacheLocation: "sessionStorage", 
    storeAuthStateInCookie: false,
  },
};

export const msalInstance = new PublicClientApplication(msalConfig);

msalInstance.addEventCallback((event: EventMessage) => {
  if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) {
    const payload = event.payload as AuthenticationResult;
    const account = payload.account;
    msalInstance.setActiveAccount(account);
  }
});

// MSAL.js v2 exposes several account APIs, logic to determine which account to use is the responsibility of the developer
const account = msalInstance.getAllAccounts()[0];

export const accessTokenRequest = {
  scopes: [`api://${import.meta.env.VITE_AAD_APP_CLIENT_ID}/user_impersonation`],
  account: account,
};

The issue was that I had my accessTokenRequest without the id of the backend app registration

export const accessTokenRequest = {
  scopes: [`user_impersonation`],
  account: account,
};
JonasKs commented 2 years ago

Glad you figured it out, and thanks for providing an example for future reference.

Have a good week 😊