GithubHelp home page GithubHelp logo

dudil / fastapi_msal Goto Github PK

View Code? Open in Web Editor NEW
33.0 3.0 19.0 665 KB

A FastAPI Plug-In to support authentication authorization using the Microsoft Authentication Library (MSAL)

License: MIT License

Python 100.00%
fastapi msal microsoft-identity-platform python starlette oauth2

fastapi_msal's Introduction

FastAPI/MSAL - MSAL (Microsoft Authentication Library) plugin for FastAPI

Checked with mypy Code style: black Lint & Security Download monthly

FastAPI - https://github.com/tiangolo/fastapi FastAPI is a modern, fast (high-performance), web framework for building APIs based on standard Python type hints.

MSAL for Python - https://github.com/AzureAD/microsoft-authentication-library-for-python The Microsoft Authentication Library for Python enables applications to integrate with the Microsoft identity platform. It allows you to sign in users or apps with Microsoft identities and obtain tokens to call Microsoft APIs such as Microsoft Graph or your own APIs registered with the Microsoft identity platform. It is built using industry standard OAuth2 and OpenID Connect protocols

The fastapi_msal package was built to allow quick "out of the box" integration with MSAL. As a result the pacage was built around simplicity and ease of use on the expense of flexability and versatility.

Features

  1. Includes Async implementation of MSAL confidential client class utilizaing Starlette threadpool model.
  2. Use pydantic models to translate the MSAL objects to data objects which are code and easy to work with.
  3. Have a built-in router which includes the required paths for the authentication flow.
  4. Include a dependency class to authenticate and secure your application APIs
  5. Includes a pydantic setting class for easy and secure configuration from your ENV (or .env or secrets directory)
  6. Full support with FastAPI swagger documentations and authentication simulation

Installation

pip install "fastapi_msal"

Or if you wish to have all the required packages straight forward

pip install "fastapi_msal[full]"

Prerequisets

  1. Python 3.9 and above
  2. As part of your fastapi application the following packages should be included:
    (if you use the [full] method it is not required.)
    1. python-multipart, From FastAPI documentation: This is required since OAuth2 (Which MSAL is based upon) uses "form data" to send the credentials.

    2. itsdangerous Used by Starlette session middleware

Usage

  1. Follow the application registration process with the Microsoft Identity Platform. Finishing the processes will allow you to register your app callback path with the platform, as well as to retrieve your application client_id, tenant_id and client_credential (client secrets) - see images below:

Client and tenant ID page

Client secrets page

  1. Create a new main.py file and add the following lines. Make sure to update the lines with the information retrieved in the previous step
import uvicorn
from fastapi import FastAPI, Depends
from starlette.middleware.sessions import SessionMiddleware
from fastapi_msal import MSALAuthorization, UserInfo, MSALClientConfig

client_config: MSALClientConfig = MSALClientConfig()
client_config.client_id = "The client_id retrieved at step #1"
client_config.client_credential = "The client_credential retrieved at step #1"
client_config.tenant = "Your tenant_id retrieved at step #1"

app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key="SOME_SSH_KEY_ONLY_YOU_KNOW")  # replace with your own!!!
msal_auth = MSALAuthorization(client_config=client_config)
app.include_router(msal_auth.router)


@app.get("/users/me", response_model=UserInfo, response_model_exclude_none=True, response_model_by_alias=False)
async def read_users_me(current_user: UserInfo = Depends(msal_auth.scheme)) -> UserInfo:
    return current_user


if __name__ == "__main__":
    uvicorn.run("main:app", host="localhost", port=5000, reload=True)
  1. Run your app
(pipenv shell)$ python main.py
INFO:     Uvicorn running on http://localhost:5000 (Press CTRL+C to quit)
INFO:     Started reloader process [12785] using statreload
INFO:     Started server process [12787]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
  1. Browse to http://localhost:5000/docs - this is the API docs generated by FastAPI (totaly cool!) Document Page Image

  2. Using the "built-in" authenticaiton button (the little lock) you will be able to set the full authentication process Authorize Page Image (Igonre the cline_id and client_secret - they are not relevant for the process as you already set them)

  3. After you complete the process you will get a confirmation popup Token Page Image

  4. Trying out the ME api endpoint Me Page Image

Working Example/Template

If you wish to try out a working example, clone the following project and adjust it to your needs: https://github.com/dudil/ms-identity-python-webapp

NB! Make sure you are using the fastapi_msal branch!!!

fastapi_msal's People

Contributors

dudil avatar raulsanguita avatar reastyn avatar timovp avatar yuenherny avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

fastapi_msal's Issues

Authority URL is now always returns as a b2c url.

Describe the bug
I have a AAD_Single app registration and application, therefore i need the authority URL for this particular MSALpolicy, however with the recent commit, it's now always returned with a b2c policy.

To Reproduce
Steps to reproduce the behavior:
go to core/msal_client_config,py line 72.

Expected behavior
I would have expected an early return when the authority url is set and not being overwritten by other if statements or at all.

Environment Settings

  • OS: WSL / Ubuntu20.04
  • Python Version: 3.11
  • Packages Versions:
    • msal==1.27.0
    • fastapi==0.110.0
    • fastapi_msal==2.1.3

Additional context
Adding the early returns solves the issue for my use case, but perhaps my understanding of the policy and b2cpolicy settings is not extensive enough have the full context for all.

Missing "groups" property on IDTokenClaim

Is your feature request related to a problem? Please describe.
The groups property is missing on the IDTokenClaims model. See current MS documentation: https://learn.microsoft.com/en-us/entra/identity-platform/id-token-claims-reference

Describe the solution you'd like
groups added as an optional field to the IDTokenClaims model.

Describe alternatives you've considered
I have locally made a quick fix by parsing the raw _id_token property. For future cases like this, it would also be nice if the decoded token would be added as a private attribute to the IDTokenClaims model similar to _id_token.

Exception in ASGI Application When Running Sample from README

Describe the bug
I'm get the following error:

ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "/usr/local/lib/python3.9/site-packages/uvicorn/protocols/http/httptools_impl.py", line 375, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "/usr/local/lib/python3.9/site-packages/uvicorn/middleware/proxy_headers.py", line 75, in __call__
    return await self.app(scope, receive, send)
  File "/usr/local/lib/python3.9/site-packages/fastapi/applications.py", line 212, in __call__
    await super().__call__(scope, receive, send)
  File "/usr/local/lib/python3.9/site-packages/starlette/applications.py", line 112, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/usr/local/lib/python3.9/site-packages/starlette/middleware/errors.py", line 181, in __call__
    raise exc
  File "/usr/local/lib/python3.9/site-packages/starlette/middleware/errors.py", line 159, in __call__
    await self.app(scope, receive, _send)
  File "/usr/local/lib/python3.9/site-packages/starlette/middleware/sessions.py", line 77, in __call__
    await self.app(scope, receive, send_wrapper)
  File "/usr/local/lib/python3.9/site-packages/starlette/exceptions.py", line 82, in __call__
    raise exc
  File "/usr/local/lib/python3.9/site-packages/starlette/exceptions.py", line 71, in __call__
    await self.app(scope, receive, sender)
  File "/usr/local/lib/python3.9/site-packages/starlette/routing.py", line 656, in __call__
    await route.handle(scope, receive, send)
  File "/usr/local/lib/python3.9/site-packages/starlette/routing.py", line 259, in handle
    await self.app(scope, receive, send)
  File "/usr/local/lib/python3.9/site-packages/starlette/routing.py", line 61, in app
    response = await func(request)
  File "/usr/local/lib/python3.9/site-packages/fastapi/routing.py", line 226, in app
    raw_response = await run_endpoint_function(
  File "/usr/local/lib/python3.9/site-packages/fastapi/routing.py", line 159, in run_endpoint_function
    return await dependant.call(**values)
  File "/usr/local/lib/python3.9/site-packages/fastapi_msal/auth.py", line 83, in _post_token_route
    token: AuthToken = await self.handler.authorize_access_token(
  File "/usr/local/lib/python3.9/site-packages/fastapi_msal/security/msal_auth_code_handler.py", line 51, in authorize_access_token
    auth_token: AuthToken = await self.msal_app(cache=cache).finalize_auth_flow(
  File "/usr/local/lib/python3.9/site-packages/fastapi_msal/clients/async_conf_client.py", line 106, in finalize_auth_flow
    return AuthToken.parse_obj_debug(to_parse=auth_token)
  File "/usr/local/lib/python3.9/site-packages/fastapi_msal/models/base_auth_model.py", line 13, in parse_obj_debug
    debug_model: AuthModel = cls.parse_obj(obj=to_parse)
  File "pydantic/main.py", line 511, in pydantic.main.BaseModel.parse_obj
  File "pydantic/main.py", line 331, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for AuthToken
id_token
  field required (type=value_error.missing)

To Reproduce
Steps to reproduce the behavior:

  1. Copied the code from the README
  2. Changed the client_id, client_credential, tenant and secret_key
  3. Run the code
  4. Goto http://localhost:5000/docs
  5. Click the lock on the left of GET /users/me
  6. In the popup, click Authorize
  7. auth errorError: Internal Server Error is displayed in the popup and the above stack trace is in the console

Expected behavior
Would expect to get the confirmation popup as mentioned in the README on step 6.

Environment Settings

  • OS: Ubuntu 20.04 (WSL2)
  • Python Version: 3.9.5
  • Packages Versions: [msal 1.17.0 / fastapi 0.73.0 / fastapi_msal 0.1.7]

Additional context
Tried changing my secret_key as mentioned in #1 but didn't change anything.

Migration to pydantic v2 (and pydantic-settings)

Thank you for this package! We are currently migrating to pydantic v2 and are having issues with the current version of the package which has a strict dependency to pydantic=1.*.

I propose we migrate to pydantic=2.* and pydantic-settings for BaseSettings (forMSALClientConfig).

Alternatively only migrate to pydantic-settings for usage of BaseSettings and loosen up the pydantic=1.* dependency,

Expired token return Internal Server Error

Describe the bug
When you put an already expired token, it will return an internal server error.

To Reproduce
Steps to reproduce the behavior:

  1. Go to FastAPI autogenerated documentation /docs
  2. Wait until the token expired
  3. Try out a request

Expected behavior
It should return 401 Unauthorized instead of internal server error

Environment Settings

  • OS: Windows
  • Python Version: 3.9
  • Packages Versions: [masl==1.16.0 / fastapi==0.73.0/ fastapi_msal==0.1.7]

Additional context
Error trace:

Traceback (most recent call last):
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\uvicorn\protocols\http\httptools_impl.py", line 376, in run_asgi  
    result = await app(self.scope, self.receive, self.send)
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\uvicorn\middleware\proxy_headers.py", line 75, in __call__        
    return await self.app(scope, receive, send)
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\fastapi\applications.py", line 212, in __call__
    await super().__call__(scope, receive, send)
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\starlette\applications.py", line 112, in __call__
    await self.middleware_stack(scope, receive, send)
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\starlette\middleware\errors.py", line 181, in __call__
    raise exc
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\starlette\middleware\errors.py", line 159, in __call__
    await self.app(scope, receive, _send)
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\starlette\middleware\cors.py", line 84, in __call__
    await self.app(scope, receive, send)
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\starlette\middleware\sessions.py", line 77, in __call__
    await self.app(scope, receive, send_wrapper)
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\starlette\exceptions.py", line 82, in __call__
    raise exc
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\starlette\exceptions.py", line 71, in __call__
    await self.app(scope, receive, sender)
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\starlette\routing.py", line 656, in __call__
    await route.handle(scope, receive, send)
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\starlette\routing.py", line 259, in handle
    await self.app(scope, receive, send)
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\starlette\routing.py", line 61, in app
    response = await func(request)
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\fastapi\routing.py", line 216, in app
    solved_result = await solve_dependencies(
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\fastapi\dependencies\utils.py", line 498, in solve_dependencies   
    solved_result = await solve_dependencies(
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\fastapi\dependencies\utils.py", line 527, in solve_dependencies   
    solved = await call(**sub_values)
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\fastapi_msal\security\msal_scheme.py", line 47, in __call__       
    token_claims: Optional[IDTokenClaims] = await self.handler.parse_id_token(
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\fastapi_msal\security\msal_auth_code_handler.py", line 79, in pars
e_id_token
    return await self.msal_app().validate_id_token(id_token=id_token)
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\fastapi_msal\clients\async_conf_client.py", line 50, in validate_i
d_token
    token_claims: OptStrsDict = await self.__execute_async__(
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\fastapi_msal\clients\async_conf_client.py", line 37, in __execute_
async__
    result: T = await run_in_threadpool(func, **kwargs)
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\starlette\concurrency.py", line 39, in run_in_threadpool
    return await anyio.to_thread.run_sync(func, *args)
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\anyio\to_thread.py", line 28, in run_sync
    return await get_asynclib().run_sync_in_worker_thread(func, *args, cancellable=cancellable,
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\anyio\_backends\_asyncio.py", line 818, in run_sync_in_worker_thre
ad
    return await future
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\anyio\_backends\_asyncio.py", line 754, in run
    result = context.run(func, *args)
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\msal\oauth2cli\oidc.py", line 107, in decode_id_token
    return decode_id_token(
  File "C:\Users\x\.virtualenvs\y-M0aUaMTg\lib\site-packages\msal\oauth2cli\oidc.py", line 76, in decode_id_token
    raise RuntimeError("%s Current epoch = %s.  The id_token was: %s" % (
RuntimeError: 9. The current time MUST be before the time represented by the exp Claim. Current epoch = 1644427758.  The id_token was: {
  "aud": "7907897b-f451-4056-a529-xxxxxxxxxxxx",
  "iss": "https://login.microsoftonline.com/94118b0c-61a0-42a1-99c9-xxxxxxxxxxxx/v2.0",
  "iat": 1644417071,
  "nbf": 1644417071,
  "exp": 1644420971,
  "idp": "https://sts.windows.net/9188040d-6c67-4c5b-b112-xxxxxxxxxxxx/",
  "name": "111 dbss",
  "nonce": "edfe0744ff8456867b3c7a0bf3c252a7e053b33e97cbf5dd9012fe9b3944376f",
  "oid": "c47f7dbb-ecd3-475c-ab5b-xxxxxxxxxxxx",
  "preferred_username": "[email protected]",
  "rh": "0.AXEADIsRlKBhoUKZyeU0YCG2pnuJB3lR9FZApSly7PrGSQ-HALI.",
  "sub": "j-eDpYyo8Dbr8cNwdiQwXzl_xxxxxxxxxxxxxxxxx",
  "tid": "94118b0c-61a0-42a1-99c9-xxxxxxxxxxxx",
  "uti": "vXXE-Eq6PE-xxxxxxxxxxx",
  "ver": "2.0"
}

Dependency overrides in unit testing

First of all, thank you so much for making this great integration. You make it so easy to use AzureAD with FastAPI!

And there is a stale discussion about this topic here: #4

Since it's already closed, I thought I would create a new issue with more details because I think I'm encountering the same issue.

So, I'd like to write unit testing for the /user/me endpoint. But since it has a dependency on the msal_auth.scheme, I need to bypass the authentication process.

msal_client_config: MSALClientConfig = MSALClientConfig()
...

msal_auth = MSALAuthorization(client_config=msal_client_config, return_to_path=settings.base_url)
msal_auth_user = Depends(msal_auth.scheme)


@router.get('/me', response_model=UserInfo, response_model_exclude_none=True, response_model_by_alias=False)
async def get_user_me(current_user: UserInfo = msal_auth_user) -> UserInfo:
    return current_user

I've tried to follow the tutorial about overriding dependencies from the FastAPI tutorial here. But it seems to be not working as the current_user still returns {'detail': 'Not authenticated'}

This is how I override the dependencies

def override_msal_scheme() -> UserInfo:
    return UserInfo(
        first_name='Test',
        last_name='User',
        display_name='Test User',
        emails=['[email protected]'],
        user_id='test_user_id',
    )


@pytest.fixture(scope="module")
def client() -> Generator:
    with TestClient(app) as c:
        app.dependency_overrides[msal_auth.scheme] = override_msal_scheme
        yield c

If you can provide an example of how to bypass, silently override, or bypass the authentication process, that would be really helpful!

How do I get past this stage to get the access token?

I'm very new to FastAPI and Python. It'd be great if I get some help here. I'm using Python 3.9. and I'm stuck at this this page.
image

I'm not sure how to change the redirect URI in the code.

And also can you please let me know how we can protect APIs using the access token generated using MSAL and also validate the access token? cuz I tried to access the API by changing the access token but still it worked. So I wanted to know how I can validate the access token before executing it. Thanks in advance.

Question: Any way to hide the /docs that is compatible with fastapi_msal?

Hello dudil,

I am trying to hide my documentation (so /docs, /redoc, and /openapi.json not accessible without msal login). Any idea how to achieve this?

I have tried a few things based on reading:

@app.get("/openapi.json", include_in_schema=False)
async def get_open_api_endpoint(current_user: str = Depends(get_current_user)):
    return JSONResponse(get_openapi(title="Kithara", version=0.1, routes=app.routes))

@app.get("/docs", include_in_schema=False)
async def get_documentation(current_user: str = Depends(get_current_user)):
    return get_swagger_ui_html(openapi_url="/openapi.json", title="docs")

and then initialize the fastapi app with

app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)

However, if this is done I do not think fastapi_msal can function. I get weird redirect related errors

In this example "get_current_user" was just using some simple APIKeyCookie stuff.

cookie_sec = APIKeyCookie(name="session")
secret_key = "zzzzzzzzzzzzzzzzzzzzzzz"
users = {"xxxxxxxxxxxxx": {"password": "yyyyyyyyyyyyyyyyyyyyy"}}

# for login to docs
def get_current_user(session: str = Depends(cookie_sec)):
    try:
        payload = jwt.decode(session, secret_key)
        user = users[payload["sub"]]
        return user
    except Exception:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN, detail="Invalid authentication"
        )

Though ideally this would be Microsoft based login.

Any ideas how I can achieve this?

[Auth bug] 500 instead of 401 when invalid auth token (JWT) is provived

Describe the bug
Normally, authentication is done using OAuth2. If you're not properly authenticated and try to use the API, you'll receive a 401 Unauthorized response:

{
    "detail": "Not authenticated"
}

This is expected behavior! Great.

But if you try to authenticate with an invalid Bearer Token, you get a 500 error instead of a 401, leaving the user in the dark as to the source of the problem, unless they check out the logs in detail.

On JWTs
Upon providing valid OAuth2 credentials through a POST to /oauth2/v2.0/token, a valid Bearer Token is generated and sent back with a 200 OK status :
https://www.oauth.com/oauth2-servers/access-tokens/access-token-response/

As described, these are simply 3 sequences of alphanumeric characters separated by certain punctuation characters. Periods "." in our case:
https://www.oauth.com/oauth2-servers/access-tokens/self-encoded-access-tokens/

This token is made up of three components, separated by periods. The first part describes the signature method used. The second part contains the token data. The third part is the signature.

Here is the example provided by https://oauth.com:
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2F1dGhvcml6YXRpb24tc2VydmVyLmNvbS8iLCJleHAiOjE2MzczNDQ1NzIsImF1ZCI6ImFwaTovL2RlZmF1bHQiLCJzdWIiOiIxMDAwIiwiY2xpZW50X2lkIjoiaHR0cHM6Ly9leGFtcGxlLWFwcC5jb20iLCJpYXQiOjE2MzczMzczNzIsImp0aSI6IjE2MzczMzczNzIuMjA1MS42MjBmNWEzZGMwZWJhYTA5NzMxMiIsInNjb3BlIjoicmVhZCB3cml0ZSJ9.SKDO_Gu96WeHkR_Tv0d8gFQN1SEdpN8S_h0IJQyl_5syvpIRA5wno0VDFi34k5jbnaY5WHn6Y912IOmg6tMO91KlYOU1MNdVhHUoPoNUzYtl_nNab7Ywe29kxgrekm-67ZInDI8RHbSkL7Z_N9eZz_J8c3EolcsoIf-Dd5n9y_

Use jwt.io or jwt.ms to decode the JWT online and have a look at its 3 separate parts in detail.

Important data is encoded on the middle part (user identification, scope of authorization...). You can change the first (signature method) and last (signature itself) parts of the JWT while keeping the dots, and you'll still be authenticated by MSAL. Is this expected behavior? The JWT needs to be validated, look into the Validate the signature section of this Microsoft Identity Platform article on Access tokens.

Now to the issues
To Reproduce
The internal errors are caused by raised runtime errors in the running Python code. Most of them are related to MSAL's oauth2cli/oidc.py.

If you play around with the JWT sent in the authorization header as a Bearer Token by changing its data you can raise many different errors. These mostly can be traced back to the json.loads(decode_part(id_token.split('.')[1])) code that converts the JWT to a JSON object in Python.

  • IndexError if you remove the separator/delimiter dots "." and send only alphanumerical characters. Raised by id_token.split('.')[1].
  • UnicodeDecodeError if you provide invalid Base64 data in the middle part of the token. Raised by the native Python string decode() function.
  • decoder.JSONDecodeError if you provide valid Base64 but it translates to invalid JSON. Raised by the json package decoder.
  • RuntimeError if JWT content is in proper JSON format but still invalid (expired or similar).

Etc.

This is a related issue:
#15
This is the specific case where the JWT is expired

We also have this RBAC PR unrelated to the focus point here, but still tied to auth and JWTs.
So not only are we using an id_token instead of an access_token, but also we aren't validating its signature lol.

Expected behavior
These raised errors are expected. They are not the issue. The problem is sending a 500 instead of a 401.
The 401 response only occurs if the JWT is empty/not sent.

I believe the fix here is to wrap these runtime errors in a try/catch block and raise an HTTP error instead, so that the controller, routing or similar receives back the raised error and finally responds to the client's HTTP request with a 401 when Unauthentified instead of a 500.
Like in this closed fastapi_msal PR by yaokaibb (We think alike! Haha)

To be filled later
Environment Settings

  • OS: [Linux/Mac/Windows]
  • Python Version: [NB! Only version 3.9+ is currently supported]
  • Packages Versions: [masl / fastapi / fastapi_msal]

Question: Cannot install fastapi_msal on Windows

I'm getting the following error messages when I try to pip install fastapi_msal:

ERROR: Could not find a version that satisfies the requirement fastapi_msal (from versions: none)
ERROR: No matching distribution found for fastapi_msal

Document how to get a token for accessing the API via curl

Is your feature request related to a problem? Please describe.
The authentication flow from the Swagger Docs UI is clear, but what isn't discussed is how a user could use the API directly from curl, without reusing the token retrieved from the Swagger Docs authentication flow.

Describe the solution you'd like
A session-less/Swagger Doc-less way to use an API I protect with msal_auth.scheme. For example:

curl http://localhost:8000/users/me  # 401 Unauthorized: {"detail":"Not authenticated"}

So we need to login:

curl -v http://localhost:8000/login  # 307 Temporary Redirect
# location: https://login.microsoftonline.com/.../oauth2/v2.0/authorize?...&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Ftoken

So I ensure http://localhost:8000/token is added to my app registrations allowed redirect URIs. Then I open the location URI in my browser and go through the Azure authentication flow in the portal, just as with the Swagger Docs UI auth flow. It should redirect to my /token endpoint which will return a token for me to use later:

curl -H @auth.header.txt http://localhost:8000/users/me

Instead I get {"detail":"Authentication Error"} from my local API.

Describe alternatives you've considered
I've tried to look into service principals, and adding an App Role "Can Invoke my API" but I cannot actually set those up without tenant admin access. I should be able to accomplish my goals without requiring Admin access.

Additional context
Going a completely different direction, ideally, we should be able to protect our API with MSAL, but then internally once authenticated, be able to generate granular tokens for use via Curl, etc... i.e. a user might have admin access to the API via roles returned from MSAL, but then once logged into the local API, they should be able to generate a less-privileged token for use in their scripts. I have no idea how one might set that up though.

Traceback when launching app contains error with itsdangerous

I followed the instructions pretty carefully, but when launching the app and visiting the site I get

kithara_base_1 | ERROR: Exception in ASGI application
kithara_base_1 | Traceback (most recent call last):
kithara_base_1 | File "/usr/local/lib/python3.9/site-packages/uvicorn/protocols/http/httptools_impl.py", line 371, in run_asgi
kithara_base_1 | result = await app(self.scope, self.receive, self.send)
kithara_base_1 | File "/usr/local/lib/python3.9/site-packages/uvicorn/middleware/proxy_headers.py", line 59, in call
kithara_base_1 | return await self.app(scope, receive, send)
kithara_base_1 | File "/usr/local/lib/python3.9/site-packages/fastapi/applications.py", line 199, in call
kithara_base_1 | await super().call(scope, receive, send)
kithara_base_1 | File "/usr/local/lib/python3.9/site-packages/starlette/applications.py", line 111, in call
kithara_base_1 | await self.middleware_stack(scope, receive, send)
kithara_base_1 | File "/usr/local/lib/python3.9/site-packages/starlette/middleware/errors.py", line 181, in call
kithara_base_1 | raise exc from None
kithara_base_1 | File "/usr/local/lib/python3.9/site-packages/starlette/middleware/errors.py", line 159, in call
kithara_base_1 | await self.app(scope, receive, _send)
kithara_base_1 | File "/usr/local/lib/python3.9/site-packages/starlette/middleware/sessions.py", line 42, in call
kithara_base_1 | data = self.signer.unsign(data, max_age=self.max_age)
kithara_base_1 | File "/usr/local/lib/python3.9/site-packages/itsdangerous/timed.py", line 110, in unsign
kithara_base_1 | raise sig_error
kithara_base_1 | File "/usr/local/lib/python3.9/site-packages/itsdangerous/timed.py", line 95, in unsign
kithara_base_1 | result = super().unsign(signed_value)
kithara_base_1 | File "/usr/local/lib/python3.9/site-packages/itsdangerous/signer.py", line 240, in unsign
kithara_base_1 | raise BadSignature(f"No {self.sep!r} found in value")
kithara_base_1 | itsdangerous.exc.BadSignature: No b'.' found in value

Wrong redirect URL when using README usage example

Describe the bug
I'm using the example in the README and having the following issue (The auth around the full ms-identitypython-webapp application is working fine though):
When I try to authorize an endpoint by clicking Authorize, I get the browser login prompt; however, the login fails and I get back a message "AADSTS50011: The redirect URI 'http://localhost:5000/docs/oauth2-redirect' specified in the request does not match the redirect URIs configured for the application."

This is true. I have not specified that URL. I don't know where it comes from. It should be http://localhost:5000/token. That's what I'm shown in steps 4 and 5 (/token), and that's the endpoint I've configured for my Azure app registration.

There is another difference visible. The images on the README shows OAuth2AuthorizationCodeBearer (OAuth2, authorizationCode), and my prompt shows MSALScheme (OAuth2, authorizationCode) for available authorizations. That doesn't look important, but I'm not sure.

To Reproduce
Steps to reproduce the behavior:
Follow the instructions in the README for the sample. Step 5 opens a new tab for Azure login. Instead of the confirmation popup, the error message above is displayed in the login tab.

Expected behavior
The login tab should close and step 6 should show a confirmation popup.

Environment Settings

  • OS: [Linux Centos 7 with Google Chrome]
  • Python Version: [3.9.5]
  • Packages Versions: [masl / fastapi / fastapi_msal] - Latest as of today (msal==1.21.0 / fastapi==0.75.0 / fastapi-msal==0.1.7)

Additional context
As I said, the full application which includes security for loading the routes "/" and "/graphcall" is working. However, that application does not feature an example of a secured OpenAPI endpoint. That would be really useful to compare.

I've tried adding that URL as a redirection URL for the application. The request is logged:

"GET /docs/oauth2-redirect?code=[big long token code]&client_info=[some base64]&state=[some other base64]&session_state=[GUID] HTTP/1.1" 200 OK

But the next log message is "POST /token HTTP/1.1" 500 Internal Server Error

Trace shows:

pydantic.error_wrappers.ValidationError: 1 validation error for AuthToken
id_token
field required (type=value_error.missing)

There is no such endpoint. I'm surprised the previous wasn't a 404, unless /docs/* traps anything. The 500 is not entirely surprising. I think the oauth2-redirect endpoint is not meant to be relative. I think it's meant to be a global Azure endpoint. I'm not sure where the error lies or what to do about it.

Logout with fastapi_msal

Hi Dudil,

Thanks a lot for your previous help on Python 3.9 --> 3.7. Everything works very smoothly!

I am just facing an issue with the logout. I've tried to clear cookies via starlette's Response but it's not working. I was wondering whether fastapi_msal provides such functionality, either already or in the (near) future? Or maybe even how to handle expiration would be helpful :)

redirect_uri str type expected (type=type_error.str)

Thank you for this package!
I tried setting up FastAPI with AzureAD for days, and used many packages.
But only using this package, combined with starlette-authlib (encode/starlette#2019 (comment)) resulted in the expected behavior.

However, as it is, I am encountering a crash with this traceback:

  File "/py/lib/python3.11/site-packages/fastapi_msal/auth.py", line 66, in _login_route
    return await self.handler.authorize_redirect(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/py/lib/python3.11/site-packages/fastapi_msal/security/msal_auth_code_handler.py", line 26, in authorize_redirect
    auth_code: AuthCode = await self.msal_app().initiate_auth_flow(
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/py/lib/python3.11/site-packages/fastapi_msal/clients/async_conf_client.py", line 95, in initiate_auth_flow
    return AuthCode.parse_obj_debug(to_parse=auth_code)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/py/lib/python3.11/site-packages/fastapi_msal/models/base_auth_model.py", line 13, in parse_obj_debug
    debug_model: AuthModel = cls.parse_obj(obj=to_parse)
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "pydantic/main.py", line 526, in pydantic.main.BaseModel.parse_obj
  File "pydantic/main.py", line 341, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for AuthCode
redirect_uri
  str type expected (type=type_error.str)

So as a workaround, I put this line in my Dockerfile and now it works:

RUN sed -i "s/redirec_uri=redirect_uri/redirec_uri=str(redirect_uri)/" /py/lib/python3.11/site-packages/fastapi_msal/auth.py

BTW: In some places over there, it says redirec instead of redirect.
Thanks again.

How to get group claims?

I'm trying to get group information into my FastAPI app from MSAL, but I can't see that it's supported.

Describe the solution you'd like
I want to add group claims in my token configuration and have them show up in the UserInfo model for use in authorizations in my endpoints.

Describe alternatives you've considered
I've configured my token configuration in my AZ AD App to "emit groups as role claims" for ID and Access types.

Additional context
None.

Route to token_path is http when I need httpS per Azure application configuration

Is your feature request related to a problem? Please describe.

I can get everything to work on local where the token_path is http to localhost but my Azure
app needs to route to https. I don't know how to specify https fo the token_path

Describe the solution you'd like

I need to get the token_path to use https somehow. I don't see where that would happen in the code

Describe alternatives you've considered

I'm using FastAPI but I can't find an option in there that'll do this https.

Additional context
Add any other context or screenshots about the feature request here.

Question: How to access API outside of swagger page

Hello,

My exploration into this package is going well, but I have a few more questions as I try to get an optimal experience. Thanks for bearing with me :).

I would like to be able to call the API outside of the swagger page (e.g. from the command line with curl).

I am getting:

2021-06-04T21:50:33.804149794Z [2021-06-04 21:50:33 +0000] [7] [ERROR] Exception in ASGI application
2021-06-04T21:50:33.804248696Z Traceback (most recent call last):
2021-06-04T21:50:33.804263696Z File "/usr/local/lib/python3.9/site-packages/uvicorn/protocols/http/httptools_impl.py", line 371, in run_asgi
2021-06-04T21:50:33.804271997Z result = await app(self.scope, self.receive, self.send)
2021-06-04T21:50:33.804279297Z File "/usr/local/lib/python3.9/site-packages/uvicorn/middleware/proxy_headers.py", line 59, in call
2021-06-04T21:50:33.804286697Z return await self.app(scope, receive, send)
2021-06-04T21:50:33.804293597Z File "/usr/local/lib/python3.9/site-packages/fastapi/applications.py", line 199, in call
2021-06-04T21:50:33.804300897Z await super().call(scope, receive, send)
2021-06-04T21:50:33.804321397Z File "/usr/local/lib/python3.9/site-packages/starlette/applications.py", line 111, in call
2021-06-04T21:50:33.804329098Z await self.middleware_stack(scope, receive, send)
2021-06-04T21:50:33.804336098Z File "/usr/local/lib/python3.9/site-packages/starlette/middleware/errors.py", line 181, in call
2021-06-04T21:50:33.804343398Z raise exc from None
2021-06-04T21:50:33.804350098Z File "/usr/local/lib/python3.9/site-packages/starlette/middleware/errors.py", line 159, in call
2021-06-04T21:50:33.804362998Z await self.app(scope, receive, _send)
2021-06-04T21:50:33.804369798Z File "/usr/local/lib/python3.9/site-packages/starlette/middleware/sessions.py", line 75, in call
2021-06-04T21:50:33.804388099Z await self.app(scope, receive, send_wrapper)
2021-06-04T21:50:33.804395799Z File "/usr/local/lib/python3.9/site-packages/starlette/exceptions.py", line 82, in call
2021-06-04T21:50:33.804403099Z raise exc from None
2021-06-04T21:50:33.804410099Z File "/usr/local/lib/python3.9/site-packages/starlette/exceptions.py", line 71, in call
2021-06-04T21:50:33.804514001Z await self.app(scope, receive, sender)
2021-06-04T21:50:33.804522001Z File "/usr/local/lib/python3.9/site-packages/starlette/routing.py", line 566, in call
2021-06-04T21:50:33.804529501Z await route.handle(scope, receive, send)
2021-06-04T21:50:33.804536701Z File "/usr/local/lib/python3.9/site-packages/starlette/routing.py", line 227, in handle
2021-06-04T21:50:33.804544002Z await self.app(scope, receive, send)
2021-06-04T21:50:33.804551002Z File "/usr/local/lib/python3.9/site-packages/starlette/routing.py", line 41, in app
2021-06-04T21:50:33.804558302Z response = await func(request)
2021-06-04T21:50:33.804578902Z File "/usr/local/lib/python3.9/site-packages/fastapi/routing.py", line 191, in app
2021-06-04T21:50:33.804587002Z solved_result = await solve_dependencies(
2021-06-04T21:50:33.804594103Z File "/usr/local/lib/python3.9/site-packages/fastapi/dependencies/utils.py", line 548, in solve_dependencies
2021-06-04T21:50:33.804603703Z solved = await call(**sub_values)
2021-06-04T21:50:33.804610803Z File "/usr/local/lib/python3.9/site-packages/fastapi_msal/security/msal_scheme.py", line 47, in call
2021-06-04T21:50:33.804618203Z token_claims: Optional[IDTokenClaims] = await self.handler.parse_id_token(
2021-06-04T21:50:33.804625203Z File "/usr/local/lib/python3.9/site-packages/fastapi_msal/security/msal_auth_code_handler.py", line 68, in parse_id_token
2021-06-04T21:50:33.804632603Z auth_token: Optional[AuthToken] = await self.get_token_from_session(
2021-06-04T21:50:33.804639803Z File "/usr/local/lib/python3.9/site-packages/fastapi_msal/security/msal_auth_code_handler.py", line 89, in get_token_from_session
2021-06-04T21:50:33.804647304Z return await AuthToken.load_from_session(
2021-06-04T21:50:33.804654304Z File "/usr/local/lib/python3.9/site-packages/fastapi_msal/models/base_auth_model.py", line 24, in load_from_session
2021-06-04T21:50:33.804661504Z return session.load(model_cls=cls)
2021-06-04T21:50:33.804668404Z File "/usr/local/lib/python3.9/site-packages/fastapi_msal/core/session_manager.py", line 79, in load
2021-06-04T21:50:33.804675604Z session: StrsDict = self._read_session()
2021-06-04T21:50:33.804682604Z File "/usr/local/lib/python3.9/site-packages/fastapi_msal/core/session_manager.py", line 56, in _read_session
2021-06-04T21:50:33.804689804Z raise IOError(
2021-06-04T21:50:33.804696504Z OSError: No session id, (Make sure you initialized the session by calling init_session)

My routes are protected with Depends(msal_auth.scheme).

Cannot use B2C logins

Describe the bug
B2C login policy cannot be used as the authority is an empty string. It seems like core.msal_client_config.py line 59 which is currently elif self.policy not in { should actually be elif self.policy in { ?

I am currently using this effectively by overwriting the authority property with the correct URL

To Reproduce
Set up with B2C login, set policy to MSALPolicies.B2C_LOGIN

Expected behavior
Correct authority should be set, f"https://{self.tenant}.b2clogin.com/{self.tenant}.onmicrosoft.com/{self.policy}"

Environment Settings

  • OS: Linux
  • Python Version: 3.11
  • Packages Versions: [msal: 1.27.0 / fastapi: 0.109.2 / fastapi_msal: 2.1.2]

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.