Giter Site home page Giter Site logo

intility / fastapi-azure-auth Goto Github PK

View Code? Open in Web Editor NEW
390.0 12.0 58.0 5.38 MB

Easy and secure implementation of Azure Entra ID (previously AD) for your FastAPI APIs ๐Ÿ”’ B2C, single- and multi-tenant support.

Home Page: https://intility.github.io/fastapi-azure-auth

License: MIT License

Python 100.00%
fastapi azure azuread authentication python oauth2 asgi openid asyncio azure-active-directory

fastapi-azure-auth's People

Contributors

bkmetzler avatar bmoore avatar bmooreatliberty avatar bulga-xd avatar copdips avatar daniwk avatar davidhuser avatar dependabot[bot] avatar enadeau avatar h3rmanj avatar infomiho avatar ingvaldlorentzen avatar jonasks avatar manupatel007 avatar mehtatejas avatar nikstuckenbrock avatar ombratteng avatar orcharddweller avatar piotrgredowski avatar ravaszf avatar robteeuwen avatar roman-van-der-krogt avatar sondrelg avatar tsw025 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  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  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  avatar

Watchers

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

fastapi-azure-auth's Issues

[Feature request] support for B2C tokens

Describe the feature you'd like

I noticed it's not possible to validate B2C tokens with this library, because it depends on the x5c field being present in the public key, and my organization's B2C public keys don't have this field. There is a simpler way to create the cert_obj that doesn't depend on this field https://python-jose.readthedocs.io/en/latest/jwk/index.html, which would allow for B2C tokens to be validated as well. I had to make some additional modifications to allow a user to override the openid_config url, but that may be specific to my organization. I'd be happy to contribute a pull request, if that is welcome?

Additional context

[BUG/Question] Cross-origin token redemption is permitted only for the 'Single-Page Application' client-type.

Describe the bug

Auth error
Error: Bad Request,
error: invalid_request,
description: AADSTS9002326: Cross-origin token redemption is permitted only for the 'Single-Page Application' client-type.

To Reproduce

This is the minimal FastAPI app:

from pydantic import AnyHttpUrl, BaseSettings, Field
from fastapi.middleware.cors import CORSMiddleware
from typing import Union

class Settings(BaseSettings):
    SECRET_KEY: str = Field('my super secret key', env='SECRET_KEY')
    BACKEND_CORS_ORIGINS: list[Union[str, AnyHttpUrl]] = ['http://localhost:8000']
    OPENAPI_CLIENT_ID: str = Field(default='', env='OPENAPI_CLIENT_ID')
    APP_CLIENT_ID: str = Field(default='', env='APP_CLIENT_ID')
    TENANT_ID: str = Field(default='', env='TENANT_ID')

    class Config:
        env_file = '.env'
        env_file_encoding = 'utf-8'
        case_sensitive = True

from fastapi import FastAPI

settings = Settings()
app = FastAPI()

settings = Settings()
if settings.BACKEND_CORS_ORIGINS:
    app.add_middleware(
        CORSMiddleware,
        allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS],
        allow_credentials=True,
        allow_methods=['*'],
        allow_headers=['*'],
    )

app = FastAPI(
    swagger_ui_oauth2_redirect_url='/oauth2-redirect',
    swagger_ui_init_oauth={
        'usePkceWithAuthorizationCodeGrant': True,
        'clientId': settings.OPENAPI_CLIENT_ID,
    })

from fastapi_azure_auth import SingleTenantAzureAuthorizationCodeBearer

azure_scheme = SingleTenantAzureAuthorizationCodeBearer(
    app_client_id=settings.APP_CLIENT_ID,
    tenant_id=settings.TENANT_ID,
    scopes={
        #"User.ReadBasic.All": 'read'
        'https://graph.microsoft.com/.default': 'default'
        #AADSTS70011
        #f'api://{settings.APP_CLIENT_ID}/user_impersonation': 'user_impersonation',
    })

@app.on_event('startup')
async def load_config() -> None:
    """    Load OpenID config on startup.    """
    await azure_scheme.openid_config.load_config()

from fastapi import Security, responses

@app.get("/", dependencies=[Security(azure_scheme, scopes=["default"])])
def read_root():
    """
    Redirects to /docs
    """
    return "It works."

Please, set the following envars:

export TENANT_ID=<your-tenant_id>
export OPENAPI_CLIENT_ID=<your-client_id>
export APP_CLIENT_ID="https://login.microsoftonline.com/$TENANT_ID"
export SECRET_KEY=<your-secret>

Steps to reproduce the behavior:

  1. Go to http://localhost:8000/docs
  2. Click in 'Autorize'
  3. Leave client_secret blank, and select scopes
  4. Click in 'Autorize', the page will return the error

Configuration

I believe this bug is related to my Azure AD set up, so may provide the Manifest from AD.
Sensitive information is hidden and the <CENSORED> is put in place.

{
	"id": "<CENSORED>",
	"acceptMappedClaims": null,
	"accessTokenAcceptedVersion": 2,
	"addIns": [],
	"allowPublicClient": false,
	"appId": "<CENSORED>",
	"appRoles": [],
	"oauth2AllowUrlPathMatching": false,
	"createdDateTime": "2022-01-11T19:43:15Z",
	"description": null,
	"certification": null,
	"disabledByMicrosoftStatus": null,
	"groupMembershipClaims": null,
	"identifierUris": [],
	"informationalUrls": {
		"termsOfService": null,
		"support": null,
		"privacy": null,
		"marketing": null
	},
	"keyCredentials": [],
	"knownClientApplications": [],
	"logoUrl": null,
	"logoutUrl": "https://localhost:8000/oauth2-redirect",
	"name": "backoffice",
	"notes": null,
	"oauth2AllowIdTokenImplicitFlow": true,
	"oauth2AllowImplicitFlow": true,
	"oauth2Permissions": [],
	"oauth2RequirePostResponse": false,
	"optionalClaims": null,
	"orgRestrictions": [],
	"parentalControlSettings": {
		"countriesBlockedForMinors": [],
		"legalAgeGroupRule": "Allow"
	},
	"passwordCredentials": [
		{
			"customKeyIdentifier": null,
			"endDate": "2022-04-21T17:02:20.006Z",
			"keyId": "<CENSORED>",
			"startDate": "2022-01-21T17:02:20.006Z",
			"value": null,
			"createdOn": "2022-01-21T17:02:31.8956842Z",
			"hint": ".F7",
			"displayName": "API-Test"
		}
	],
	"preAuthorizedApplications": [],
	"publisherDomain": "<CENSORED>",
	"replyUrlsWithType": [
		{
			"url": "http://localhost:8000/",
			"type": "Web"
		},
		{
			"url": "http://localhost:8000/oauth2-redirect",
			"type": "Web"
		},
	],
	"requiredResourceAccess": [
		{
			"resourceAppId": "00000003-0000-0000-c000-000000000000",
			"resourceAccess": [
				{
					"id": "e1fe6dd8-ba31-4d61-89e7-88639da4683d",
					"type": "Scope"
				},
				{
					"id": "14dad69e-099b-42c9-810b-d002981feec1",
					"type": "Scope"
				}
			]
		}
	],
	"samlMetadataUrl": null,
	"serviceManagementReference": null,
	"signInUrl": null,
	"signInAudience": "AzureADMyOrg",
	"tags": [],
	"tokenEncryptionKeyId": null
}

[HELP] fastapi-azure-auth is not published to pypi?

Describe the bug
I am unable to install fastapi-azure-auth, everytime I try I get

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

To Reproduce
You can run this command on a plain ubuntu docker container and you will get the same error.
docker run -it ubuntu /bin/bash -c "apt-get update && apt-get install -y python3 python3-pip && pip install fastapi-azure-auth"

Missing code_challenge

Using authentication from swagger and after receiving the redirect URL I notice that the code_challenge is missing. Any idea? Also looking for code_verifier...

Validating an user with AAD

I'm trying to use the library for implementing authentication on my API on a larger project.

The way things work is basically my API is serving as backend to an Angular app which is in charge of fetching the token from Azure AD. That's ok. What should happen in my end is I have to validate both the token and the user that is making the request. I can get the user details directly from the token but I was wondering if there's a way to check if this user is valid in my Azure application?

Maybe I'm a bit confused but I see the validate_is_admin_user method which definitely looks like something I would need to make this check but instead of verifying if the user is an admin I would just like to check if it's a valid user on my app.

Hope this is clear and that I can get some help :)
Thanks!

B2C token does not contain `tid` in the token

I've managed to set up B2C, but it doesn't seem to return tid in the token. Everything else is working.

I'm not sure how to confirm if everything is working well (I might have to contact microsoft), but if B2C doesn't return tid, would it make sense to make it optional in the User model?

[Question] How to set up AAD B2C for main API and AAD for docs?

Hi! I'm new to Azure and AAD, so please correct me if I'm wrong.

I see that this package supports AAD B2C, but I haven't found any related documentation other than the sample project.

Basically, I would like to limit the main API to registered users in a B2C directory and the Swagger docs to users in the main directory (i.e. admins). Is that possible? Thanks.

[Documentation] How to access user attributes

This crucial part is missing in the documentation.
based on: #8 (comment)

Here a stub that could help:

You can access the authorized user's attributes with request.state.user
A sample view that outputs the current user name:

from fastapi import Request, Security

@app.get("/hello", dependencies=[Security(azure_scheme)])
async def hello(request: Request):
    return "Hello {}".format(request.state.user.name)

Scopes missing in /docs at each endpoint padlock

Describe the bug
Scopes are missing if we select padlock symbol next to each api endpoint while trying to authorize.
they do appear when we select authorize button at the top.

To Reproduce
use below azure scheme

azure_scheme = SingleTenantAzureAuthorizationCodeBearer(
    app_client_id=settings.APP_CLIENT_ID,
    tenant_id=settings.TENANT_ID,
    scopes={
        f'api://{settings.APP_CLIENT_ID}/user_impersonation': 'user_impersonation',
    }
)

use this scheme as a dependency to your endpoint

app.include_router(api_router, prefix=settings.API_V1_STR, dependencies=[Security(azure_scheme, scopes=['user_impersonation'])])

go to /docs
image

you can see scopes when you click authorize button

image

if you click on padlock below scopes are missing

image
image

and unable to authorize as I am getting below error..

image

Add user object to the request

Right now we only validate the token, but having the user object would allow us to easily check for groups, username etc.

Awesome!

Took me 3 days to get a valid token from AAD for my VUEJS SPA, but only 5 Minutes to integrate the SPA with FastAPI utilizing fastapi-azure-auth!

Thank you so much

Volker

[Feature request] Expose auto_error to support multiple other Security auths/bearers along side this package

Describe the feature you'd like
Exposing 'auto_error' from OAuth2AuthorizationCodeBearer and implementing auto_error in 'AzureAuthorizationCodeBearerBase.call'.
This will allow having other SecurityBases along side this package.

Additional context
This allows having both AzureAD authentication(Bearer) and X-Api-Key authorization for the backend API.
If a system is broken up into 2 separate systems (UI and API), the UI would connect to the API via Bearer, where other applications would connect to the API via X-Api-Key.

The current problem is that fastapi-azure-auth throws exceptions if the Bearer token isn't available in the request. 'auto_error' standard allows the developer to decide if it should throw the Exception or handle it with another check/Depends on the API Endpoint.

azure_scheme = SingleTenantAzureAuthorizationCodeBearer( app_client_id=settings.APP_CLIENT_ID, tenant_id=settings.TENANT_ID, scopes={ f'api://{settings.APP_CLIENT_ID}/user_impersonation': 'user_impersonation', }, auto_error=False )

identity_auth = IdentityToken(auto_error=False)

`def check(os_auth: Optional[IdentityUser] = Depends(identity_auth),
azure_auth: Optional[SingleTenantAzureAuthorizationCodeBearer] = Depends(azure_scheme)) -> bool:
if os_auth is not None:
return os_auth

if azure_auth is not None:
    return azure_auth

raise HTTPException(status_code=401, detail="No Authorization method provided")

@app.get("/both", dependencies=[Depends(check)])
async def both():
return {
"status": "OK"
}`

This both handles both authentication methods, along with properly updating swagger to allow one or both authentication methods. 'check' would allow others to define which order they deem fit.

(PR incoming)

-Brian Metzler

[Feature request] Direct Access To Config_url

Hello guys! I'm a part of the Python-dev team in a big company. We decided to use this library for our B2C auth. At the start, it looked fun to be used but after that, we needed to start debugging the library because we didn't find any way to configure config_url easily so the only way we found was:

@app.on_event("startup")
async def load_config() -> None:
    """
    Load OpenID config on startup.
    """
    azure_scheme.openid_config.config_url = get_settings().CONFIG_URL
    await azure_scheme.openid_config.load_config()

So my suggestion is to export the config_url as additional property which is accessible directly from: MultiTenantAzureAuthorizationCodeBearer and SingleTenantAzureAuthorizationCodeBearer

Code suggestion:

class SingleTenantAzureAuthorizationCodeBearer(AzureAuthorizationCodeBearerBase):
    def __init__(
        self,
        app_client_id: str,
        tenant_id: str,
        auto_error: bool = True,
        scopes: Optional[Dict[str, str]] = None,
        token_version: Literal[1, 2] = 2,
        openid_config_use_app_id: bool = False,
        openapi_authorization_url: Optional[str] = None,
        openapi_token_url: Optional[str] = None,
        openapi_description: Optional[str] = None,
        openid_config_url: Optional[str] = None,
        
    ) -> None:
        """
        Initialize settings for a single-tenant application.

        :param app_client_id: str
            Your application client ID. This will be the `Web app` in Azure AD
        :param tenant_id: str
            Your Azure tenant ID, only needed for single tenant apps
        :param auto_error: bool
            Whether to throw exceptions or return None on __call__.
        :param scopes: Optional[dict[str, str]
            Scopes, these are the ones you've configured in Azure AD. Key is scope, value is a description.
            Example:
                {
                    f'api://{settings.APP_CLIENT_ID}/user_impersonation': 'user impersonation'
                }

        :param token_version: int
            Version of the token expected from the token endpoint. Defaults to `2`, but can be set to `1` for single
            tenant applications.
        :param openid_config_use_app_id: bool
            Set this to True if you're using claims-mapping. If you're unsure, leave at False.
            https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc#sample-response

        :param openapi_authorization_url: str
            Override OpenAPI authorization URL
        :param openapi_token_url: str
            Override OpenAPI token URL
        :param openapi_description: str
            Override OpenAPI description
       :param openid_config_url: str
            Override OpenID config URL (used for B2C tenants)
        """
        super().__init__(
            app_client_id=app_client_id,
            auto_error=auto_error,
            tenant_id=tenant_id,
            scopes=scopes,
            token_version=token_version,
            openid_config_use_app_id=openid_config_use_app_id,
            openapi_authorization_url=openapi_authorization_url,
            openapi_token_url=openapi_token_url,
            openapi_description=openapi_description,
            openid_config_url=openid_config_url,
        )
        self.scheme_name: str = 'Azure AD - PKCE, Single-tenant'

And

class MultiTenantAzureAuthorizationCodeBearer(AzureAuthorizationCodeBearerBase):
    def __init__(
        self,
        app_client_id: str,
        auto_error: bool = True,
        scopes: Optional[Dict[str, str]] = None,
        validate_iss: bool = True,
        iss_callable: Optional[Callable[[str], Awaitable[str]]] = None,
        openid_config_use_app_id: bool = False,
        openapi_authorization_url: Optional[str] = None,
        openapi_token_url: Optional[str] = None,
        openapi_description: Optional[str] = None,
        openid_config_url: Optional[str] = None,
    ) -> None:
        """
        Initialize settings for a multi-tenant application.

        :param app_client_id: str
            Your application client ID. This will be the `Web app` in Azure AD
        :param auto_error: bool
            Whether to throw exceptions or return None on __call__.
        :param scopes: Optional[dict[str, str]
            Scopes, these are the ones you've configured in Azure AD. Key is scope, value is a description.
            Example:
                {
                    f'api://{settings.APP_CLIENT_ID}/user_impersonation': 'user impersonation'
                }

        :param validate_iss: bool
            Whether to validate the token `iss` (issuer) or not. This can be skipped to allow anyone to log in.
        :param iss_callable: Async Callable
            Async function that has to accept a `tid` (tenant ID) and return a `iss` (issuer) or
             raise an InvalidIssuer exception
            This is required when validate_iss is set to `True`.

        :param openid_config_use_app_id: bool
            Set this to True if you're using claims-mapping. If you're unsure, leave at False.
            https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc#sample-response

        :param openapi_authorization_url: str
            Override OpenAPI authorization URL
        :param openapi_token_url: str
            Override OpenAPI token URL
        :param openapi_description: str
            Override OpenAPI description
       :param openid_config_url: str
            Override OpenID config URL (used for B2C tenants)
        """
        super().__init__(
            app_client_id=app_client_id,
            auto_error=auto_error,
            scopes=scopes,
            validate_iss=validate_iss,
            iss_callable=iss_callable,
            multi_tenant=True,
            openid_config_use_app_id=openid_config_use_app_id,
            openapi_authorization_url=openapi_authorization_url,
            openapi_token_url=openapi_token_url,
            openapi_description=openapi_description,
            openid_config_url=openid_config_url,
        )
        self.scheme_name: str = 'Azure AD - PKCE, Multi-tenant'

If you think this is acceptable we can contribute with PR.

Definition of fastapi ^0.68.0 only works for patch versions[BUG/Question]

Describe the bug
A new minor release came out yesterday 0.70.0 and causes poetry conflicts when trying to utilize FAA.

The FastAPI-azure-auth package defines ^0.68.0 which effectively means >=0.68.0,<0.69.0

To Reproduce
poetry init a new project

Add fastapi latest version
Add fastapi_azure_auth latest version

Run poetry install and get a dependency conflict error.

Stack trace
$ poetry install
Updating dependencies
Resolving dependencies... (20.1s)

SolverProblemError

Because no versions of fastapi-azure-auth match >3.0.1,<4.0.0
and fastapi-azure-auth (3.0.1) depends on fastapi (>=0.68.0,<0.69.0), fastapi-azure-auth (>=3.0.1,<4.0.0) requires fastapi (>=0.68.0,<0.69.0).
And because fastapi (0.70.0) depends on fastapi (0.70.0)
and no versions of fastapi match >0.70.0,<0.71.0, fastapi-azure-auth (>=3.0.1,<4.0.0) is incompatible with fastapi (>=0.70.0,<0.71.0).
So, because ' depends on both fastapi (^0.70.0) and fastapi-azure-auth (^3.0.1), version solving failed.

at ~/.asdf/installs/python/3.9.6/lib/python3.9/site-packages/poetry/puzzle/solver.py:241 in _solve
237โ”‚ packages = result.packages
238โ”‚ except OverrideNeeded as e:
239โ”‚ return self.solve_in_compatibility_mode(e.overrides, use_latest=use_latest)
240โ”‚ except SolveFailure as e:
โ†’ 241โ”‚ raise SolverProblemError(e)
242โ”‚
243โ”‚ results = dict(
244โ”‚ depth_first_search(
245โ”‚ PackageNode(self._package, packages), aggregate_package_nodes

No module named 'cryptography.hazmat.primitives.asymmetric.types'

Hi,

Describe the error
After installing cryptography 3.3.1 when I'm trying to run the application, I'm getting error for this line:

from fastapi_azure_auth.user import User

The error is:
ModuleNotFoundError: No module named 'cryptography.hazmat.primitives.asymmetric.types'

To Reproduce
Install these packages:

pyjwt==2.3.0
cryptography==3.3.1
bcrypt==3.2.0

Stack trace

File "dc_dexter_api/src/api/endpoints/registration/checks/checks.py", line 12, in <module>
    from fastapi_azure_auth.user import User
  File "dc_dexter_api/venv/lib/python3.9/site-packages/fastapi_azure_auth/__init__.py", line 1, in <module>
    from fastapi_azure_auth.auth import (  # noqa: F401
  File "dc_dexter_api/venv/lib/python3.9/site-packages/fastapi_azure_auth/auth.py", line 14, in <module>
    from fastapi_azure_auth.openid_config import OpenIdConfig
  File "dc_dexter_api/venv/lib/python3.9/site-packages/fastapi_azure_auth/openid_config.py", line 8, in <module>
    from cryptography.hazmat.primitives.asymmetric.types import PUBLIC_KEY_TYPES as KeyTypes
ModuleNotFoundError: No module named 'cryptography.hazmat.primitives.asymmetric.types'

Your configuration

The Config class(If that's what you mean):

class Settings(AzureActiveDirectory):
    API_V1_STR: str = '/v1'

    """ BACKEND_CORS_ORIGINS is a JSON-formatted list of origins
     e.g: '["http://localhost", "http://localhost:4200", "http://localhost:3000", 
          "http://localhost:8080", "http://local.dockertoolbox.tiangolo.com"]'
    """
    BACKEND_CORS_ORIGINS: List[Union[str, AnyHttpUrl]] = ['http://localhost:8007']

    @validator('BACKEND_CORS_ORIGINS', pre=True)
    def assemble_cors_origins(cls, value: Union[str, List[str]]) -> Union[List[str], str]:
        """
        Validate cors list
        """
        if isinstance(value, str) and not value.startswith('['):
            return [i.strip() for i in value.split(',')]
        elif isinstance(value, (list, str)):
            return value
        raise ValueError(value)

[Question] Login integration

First, thank you for the library it's great!

Would be nice to see a very basic integration between the library and a logging endpoint in order to understand the integration with the library in that process.

I can logging with the Fastapi docs but not from my app. I feel I'm reinventing the wheel trying to do the authentication steps. I'm ending creating my own functions try to follow the oath2 flow.

Thanks!

[BUG] AttributeError: partially initialized module 'anyio._backends._asyncio' has no attribute 'Event'

Describe the bug

We are seeing this error reported in our sentry for a FastApi Azure function based on https://github.com/ecerami/fastapi_azure and fastapi-azure-auth

partially initialized module 'anyio._backends._asyncio' has no attribute 'Event' (most likely due to a circular import)

see https://github.com/Intility/fastapi-azure-auth/blob/main/fastapi_azure_auth/openid_config.py#L55

To Reproduce

Not sure how to reproduce, it only happens occasionally. Our app is still in early phase and does not see real traffic.

Stack trace

AttributeError: partially initialized module 'anyio._backends._asyncio' has no attribute 'Event' (most likely due to a circular import)
  File "fastapi_azure_auth/openid_config.py", line 79, in _load_openid_config
    openid_response = await client.get(config_url)
  File "httpx/_client.py", line 1729, in get
    return await self.request(
  File "httpx/_client.py", line 1506, in request
    return await self.send(request, auth=auth, follow_redirects=follow_redirects)
  File "httpx/_client.py", line 1593, in send
    response = await self._send_handling_auth(
  File "httpx/_client.py", line 1621, in _send_handling_auth
    response = await self._send_handling_redirects(
  File "httpx/_client.py", line 1658, in _send_handling_redirects
    response = await self._send_single_request(request)
  File "httpx/_client.py", line 1695, in _send_single_request
    response = await transport.handle_async_request(request)
  File "httpx/_transports/default.py", line 353, in handle_async_request
    resp = await self._pool.handle_async_request(req)
  File "httpcore/_async/connection_pool.py", line 216, in handle_async_request
    status = RequestStatus(request)
  File "httpcore/_async/connection_pool.py", line 19, in __init__
    self._connection_acquired = AsyncEvent()
  File "httpcore/_synchronization.py", line 29, in __init__
    self._event = anyio.Event()
  File "anyio/_core/_synchronization.py", line 77, in __new__
    return get_asynclib().Event()

AttributeError: partially initialized module 'anyio._backends._asyncio' has no attribute 'checkpoint_if_cancelled' (most likely due to a circular import)
  File "fastapi_azure_auth/openid_config.py", line 42, in load_config
    await self._load_openid_config()
  File "fastapi_azure_auth/openid_config.py", line 91, in _load_openid_config
    self._load_keys(jwks_response.json()['keys'])
  File "httpx/_client.py", line 1975, in __aexit__
    await self._transport.__aexit__(exc_type, exc_value, traceback)
  File "httpx/_transports/default.py", line 332, in __aexit__
    await self._pool.__aexit__(exc_type, exc_value, traceback)
  File "httpcore/_async/connection_pool.py", line 326, in __aexit__
    await self.aclose()
  File "httpcore/_async/connection_pool.py", line 303, in aclose
    async with self._pool_lock:
  File "httpcore/_synchronization.py", line 15, in __aenter__
    await self._lock.acquire()
  File "anyio/_core/_synchronization.py", line 117, in acquire
    await checkpoint_if_cancelled()
  File "anyio/lowlevel.py", line 42, in checkpoint_if_cancelled
    await get_asynclib().checkpoint_if_cancelled()

RuntimeError: Unable to fetch provider information. partially initialized module 'anyio._backends._asyncio' has no attribute 'checkpoint_if_cancelled' (most likely due to a circular import)
  File "starlette/exceptions.py", line 93, in __call__
    raise exc
  File "starlette/exceptions.py", line 82, in __call__
    await self.app(scope, receive, sender)
  File "fastapi/middleware/asyncexitstack.py", line 21, in __call__
    raise e
  File "fastapi/middleware/asyncexitstack.py", line 18, in __call__
    await self.app(scope, receive, send)
  File "starlette/routing.py", line 670, in __call__
    await route.handle(scope, receive, send)
  File "starlette/routing.py", line 418, in handle
    await self.app(scope, receive, send)
  File "fastapi/applications.py", line 269, in __call__
    await super().__call__(scope, receive, send)
  File "starlette/applications.py", line 124, in __call__
    await self.middleware_stack(scope, receive, send)
  File "starlette/middleware/errors.py", line 184, in __call__
    raise exc
  File "starlette/middleware/errors.py", line 162, in __call__
    await self.app(scope, receive, _send)
  File "starlette/exceptions.py", line 93, in __call__
    raise exc
  File "starlette/exceptions.py", line 82, in __call__
    await self.app(scope, receive, sender)
  File "fastapi/middleware/asyncexitstack.py", line 21, in __call__
    raise e
  File "fastapi/middleware/asyncexitstack.py", line 18, in __call__
    await self.app(scope, receive, send)
  File "starlette/routing.py", line 670, in __call__
    await route.handle(scope, receive, send)
  File "starlette/routing.py", line 266, in handle
    await self.app(scope, receive, send)
  File "starlette/routing.py", line 65, in app
    response = await func(request)
  File "fastapi/routing.py", line 217, in app
    solved_result = await solve_dependencies(
  File "fastapi/dependencies/utils.py", line 524, in solve_dependencies
    solved = await call(**sub_values)
  File "fastapi_azure_auth/auth.py", line 151, in __call__
    await self.openid_config.load_config()
  File "fastapi_azure_auth/openid_config.py", line 55, in load_config
    raise RuntimeError(f'Unable to fetch provider information. {error}') from error

Your configuration

class AuthSettings(BaseSettings):
    SECRET_KEY: str = Field("secret key", env="SECRET_KEY")
    BACKEND_CORS_ORIGINS: list[Union[str, AnyHttpUrl]] = [
        "http://localhost:7071",
        "http://localhost:8000",
        "https://foo.azurewebsites.net",
    ]
    OPENAPI_CLIENT_ID: str = Field(default="", env="OPENAPI_CLIENT_ID")
    APP_CLIENT_ID: str = Field(default="", env="APP_CLIENT_ID")
    TENANT_ID: str = Field(default="", env="TENANT_ID")

    class Config:
        env_file = "settings.env"
        env_file_encoding = "utf-8"
        case_sensitive = True


auth_settings = AuthSettings()


class FastAPISettings(BaseSettings):
    debug: bool = Field(default=False, env="DEBUG")
    title: str = "Foo"
    description: str = (
        "..."
    )
    version: str = "0.0.1"
    contact: dict = {
        "name": "Foo",
    }
    swagger_ui_oauth2_redirect_url: str = "/oauth2-redirect"
    swagger_ui_init_oauth: dict = {
        "usePkceWithAuthorizationCodeGrant": True,
        "clientId": auth_settings.OPENAPI_CLIENT_ID,
    }

    class Config:
        env_file = "settings.env"


azure_scheme = SingleTenantAzureAuthorizationCodeBearer(
    app_client_id=auth_settings.APP_CLIENT_ID,
    tenant_id=auth_settings.TENANT_ID,
    scopes={
        f"api://{auth_settings.APP_CLIENT_ID}/user_impersonation": "user_impersonation",
    },
)

certificate verify failed: unable to get local issuer certificate (_ssl.c:1129)

Hi,
I'm using this library. everything works fine when I dockerize my app and run the docker image. But if I try to run my project locally, I get this error:

RuntimeError: Unable to fetch provider information. Cannot connect to host login.microsoftonline.com:443 ssl:True [SSLCertVerificationError: (1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1129)')]

How come it works when running via docker image locally, but not without it? The only difference I can see is that image is running on Linux while locally I'm using MacOS. Can you please guide me how to solve this?
Also sorry if I'm asking this here, I couldn't find a solution out there!

Add OBO middleware [Feature request]

Describe the feature you'd like
It would be nice to have the framework allow more than one API scope. The case where this is needed, is if you have multiple business applications that need access to the same API. (For example like how both Teams and Outlook has access to your calendar). Features like this is supported in .NET frameworks by making a list of valid isuers and audiences, instead of enforcing just one (see this StackOverflow example for how the .NET AddJwtBearer middleware works)

The "correct way" of dealing with these cases is to add middleware implementing the OBO ("On-Behalf-Of") flow. Usually this is handled by the client, but for third party applications and/or plugins, we cannot initiate OBO client-side.

Additional context
If I supply an access token with an audience that I've added to the my application's knownClientApplications list, the token validation should pass.

TypeError: 'Request' object does not support item assignment

I'm creating an API gateway using fastAPI for some microservices, which in I authenticate user via Azure AD via this library. So I'm trying to take out the user info from the request object (request.state.user) and inject my own token in it to later pass to other microservices. I can't do this in middleware, cause user info is getting added after it.
But I tried creating an decorator:

from functools import wraps

def token_injector(function):
    @wraps(function)
    def wrap_function(*args, **kwargs):
        user: User = kwargs['request'].state.user
        new_token = generate_token(user)
        kwargs["new_token"] = new_token # or kwargs['request']["new_token"] = new_token
        return function(*args, **kwargs)
    return wrap_function

and for this approach, I'm getting these errors:

ValueError: [TypeError("'coroutine' object is not iterable"), TypeError('vars() argument must have dict attribute')]
and

TypeError: 'Request' object does not support item assignment

Can you please guide me how did you inject user in request.state, so I can do the same?

Ability to store the token in a caching tool

Ability to store the token in a caching tool (e.g: Redis with TTL keys) to prevent checking with azure on every request

It would be great to have an ability in which we can store the user token in a caching tool like Redis and set a TTL for it so that it will expire after a while (Probably set the exact time azure would keep the session for the token), so it prevents sending request to azure AD to check the token on each request. I hope It's clear what I'm asking.

Scopes in the README setup should be a list

In the setup example in README, here https://github.com/Intility/fastapi-azure-auth#5-configure-dependencies

The scopes params is 'user_impersonation' instead of ['user_impersonation']:

# file: main.py
from demoproj.api.dependencies import azure_scheme

app.include_router(api_router, prefix=settings.API_V1_STR, dependencies=[Security(azure_scheme, scopes='user_impersonation')])

... which causes the error:

if scope not in token_scopes:
    raise InvalidAuth('Required scope missing')

This looks good in the docs ๐Ÿ‘

Dynamic model instantiation based on user role

I am using models within FastAPI to validate incoming body data. Like so:

class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: float = 10.5

@app.post("/test")
async def test(item: Item):
    return item

The problem is that the model definition should be different based on the Azure user role that's being returned after authentication.

Is there any way to run the azure dependency globally so I can do something like this?

# ... start by authenticating right away

if "admin" in user.roles:
  class Item(BaseModel):
      name: str
      description: Union[str, None] = None
      admin_prop: float
else:
  class Item(BaseModel):
      name: str
      description: Union[str, None] = None
      price: float
      non_admin_prop: float = 10.5

# ... routes

The correct model needs to be properly loaded on startup so that its schema becomes available in the api docs.

Thank you!

[Question] React Native auth flow

Describe the question
Question about integration with the framework.

I posted an issue some weeks ago. The client on React Native has an auth webview, and returns response from azure with some params. However, I'm not sure how to proceed, since I want to send the params to the FastAPI backend.

Is it possible to use fastapi-azure-auth as a bridge and be able to execute the following steps?:

  • Send params to FastAPI, Process params from azure response (the structure I'm handling is described later in this issue)
  • Save user data from azure on DB, save the user credentials, such as email and department, on the database.
  • Return an auth token, return an auth token for authorization tokens on requests

To Reproduce
I'm coding a sign in service for a mobile app. I want my flow to work as follows, but I'm stuck in the steps inside the circle:
image

The params of the step Send params to FastAPI have the following JSON structure (fake values):

{
   "authentication":null,
   "error":null,
   "errorCode":null,
   "params":{
      "code":"0.AQMAE...",
      "session_state":"123123123-as123-4f1231-1we12",
      "state":"123bkbk21j3b"
   },
   "type":"success",
   "url":"exp://192.168.1.66:19000/?code=0.AQMAE..."
}

[Feature request] Support for python 3.8+ versions.

I had gone through the packages associated with fastapi-azure-auth and found that none of them needs a python version higher then 3.8. And if we can import some of the type hints from typing instead of using inbuilt type hints(as per the new feature in python 3.9 and a few other changes, we can serve a bit wider audience with this nice package. Are you open for a PR in this context?

Cryptography package changed hazmat primitives [BUG/Question]

Describe the bug
If using this package with the most recent version of cryptography, apps crash due to a change in the hazmat primitives.

To Reproduce
Have the following requirements

fastapi[all]
fastapi-azure-auth
uvicorn
pytest

  1. pip install --no-cache-dir -r requirements.txt
  2. Run the sample "getting started" app uvicorn app:app --reload

Stack trace
...
from fastapi_azure_auth import SingleTenantAzureAuthorizationCodeBearer
File "/home/vcap/deps/0/python/lib/python3.9/site-packages/fastapi_azure_auth/init.py", line 1, in
from fastapi_azure_auth.auth import ( # noqa: F401
File "/home/vcap/deps/0/python/lib/python3.9/site-packages/fastapi_azure_auth/auth.py", line 14, in
from fastapi_azure_auth.openid_config import OpenIdConfig
File "/home/vcap/deps/0/python/lib/python3.9/site-packages/fastapi_azure_auth/openid_config.py", line 7, in
from cryptography.hazmat._types import _PUBLIC_KEY_TYPES as KeyTypes
ModuleNotFoundError: No module named 'cryptography.hazmat._types'

Your configuration
Python 3.9.6
using venv

Auth with React

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

Release v1

  • Merge #3
  • Create release
  • Update PyPI token, unfortunately not possible to create a scoped token until you have the repo on PYPI

[Feature request] oauth2-redirect outside local development

The documentation is sufficient for building an API that runs in a local development environment - but I suppose that setting oauth2-redirect to a real domain instead of localhost is a common use case outside local development. Suppose the API is hosted somewhere (kubernetes, Azure Container Instances, Virtual Machine) - how should the oauth2-redirect URI now be changed - and what are some options on how to treat TLS in such a setting (as Azure App Registration will only allow https links as redirects). Maybe a section on production maturing would be helpful or maybe simply just clarifying that oauth2-redirect should be changed in a realistic production/TLS setting.

[BUG] TypeError at library import (Python 3.9.1)

Describe the bug
I'm new to Python/FastAPI, so it is very well possible that it's just me doing something wrong, but my code fails to compile as soon as I add the line to import either SingleTenantAzureAuthorizationCodeBearer or MultiTenantAzureAuthorizationCodeBearer and I'm getting this error: TypeError: unhashable type: 'list'

First I thought it's related to my already written code, so I've started a clean project and followed the tutorial in the documentation, but that also fails as soon as I add the import line. So I'm completely lost here.

I'm using Python 3.9.1 and FastAPI 0.74.1

To Reproduce
Add code line:
from fastapi_azure_auth import SingleTenantAzureAuthorizationCodeBearer

Stack trace

Traceback (most recent call last):
  File "c:\program files\python39\lib\runpy.py", line 197, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "c:\program files\python39\lib\runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "C:\Users\...\AppData\Roaming\Python\Python39\Scripts\uvicorn.exe\__main__.py", line 7, in <module>
  File "C:\Users\...\AppData\Roaming\Python\Python39\site-packages\click\core.py", line 1137, in __call__
    return self.main(*args, **kwargs)
  File "C:\Users\...\AppData\Roaming\Python\Python39\site-packages\click\core.py", line 1062, in main
    rv = self.invoke(ctx)
  File "C:\Users\...\AppData\Roaming\Python\Python39\site-packages\click\core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "C:\Users\...\AppData\Roaming\Python\Python39\site-packages\click\core.py", line 763, in invoke
    return __callback(*args, **kwargs)
  File "C:\Users\...\AppData\Roaming\Python\Python39\site-packages\uvicorn\main.py", line 435, in main
    run(app, **kwargs)
  File "C:\Users\...\AppData\Roaming\Python\Python39\site-packages\uvicorn\main.py", line 461, in run
    server.run()
  File "C:\Users\...\AppData\Roaming\Python\Python39\site-packages\uvicorn\server.py", line 60, in run
    return asyncio.run(self.serve(sockets=sockets))
  File "c:\program files\python39\lib\asyncio\runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "c:\program files\python39\lib\asyncio\base_events.py", line 642, in run_until_complete
    return future.result()
  File "C:\Users\...\AppData\Roaming\Python\Python39\site-packages\uvicorn\server.py", line 67, in serve
    config.load()
  File "C:\Users\...\AppData\Roaming\Python\Python39\site-packages\uvicorn\config.py", line 458, in load
    self.loaded_app = import_from_string(self.app)
  File "C:\Users\...\AppData\Roaming\Python\Python39\site-packages\uvicorn\importer.py", line 21, in import_from_string
    module = importlib.import_module(module_str)
  File "c:\program files\python39\lib\importlib\__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1030, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1007, in _find_and_load
  File "<frozen importlib._bootstrap>", line 986, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 680, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 790, in exec_module
  File "<frozen importlib._bootstrap>", line 228, in _call_with_frames_removed
  File ".\main.py", line 7, in <module>
    from fastapi_azure_auth import SingleTenantAzureAuthorizationCodeBearer
  File "C:\Users\...\AppData\Roaming\Python\Python39\site-packages\fastapi_azure_auth\__init__.py", line 1, in <module>
    from fastapi_azure_auth.auth import (  # noqa: F401
  File "C:\Users\...\AppData\Roaming\Python\Python39\site-packages\fastapi_azure_auth\auth.py", line 20, in <module>
    class AzureAuthorizationCodeBearerBase(SecurityBase):
  File "C:\Users\...\AppData\Roaming\Python\Python39\site-packages\fastapi_azure_auth\auth.py", line 29, in AzureAuthorizationCodeBearerBase
    iss_callable: Optional[Callable[[str], Awaitable[str]]] = None,
  File "c:\program files\python39\lib\typing.py", line 262, in inner
    return func(*args, **kwds)
  File "c:\program files\python39\lib\typing.py", line 339, in __getitem__
    return self._getitem(self, parameters)
  File "c:\program files\python39\lib\typing.py", line 463, in Optional
    return Union[arg, type(None)]
  File "c:\program files\python39\lib\typing.py", line 262, in inner
    return func(*args, **kwds)
  File "c:\program files\python39\lib\typing.py", line 339, in __getitem__
    return self._getitem(self, parameters)
  File "c:\program files\python39\lib\typing.py", line 451, in Union
    parameters = _remove_dups_flatten(parameters)
  File "c:\program files\python39\lib\typing.py", line 231, in _remove_dups_flatten
    return tuple(_deduplicate(params))
  File "c:\program files\python39\lib\typing.py", line 205, in _deduplicate
    all_params = set(params)
TypeError: unhashable type: 'list'

Your configuration
Exactly the same as the tutorial here: https://intility.github.io/fastapi-azure-auth/single-tenant/fastapi_configuration

[Question] Initialize `SingleTenantAzureAuthorizationCodeBearer` at non-top level

Related SO question: https://stackoverflow.com/questions/75262398/fastapi-read-configuration-before-specifying-dependencies.

Hi. Let me just say that I'm very happy to use this library because it made the Azure authentication for me a piece of cake!

I have a problem with initializing SingleTenantAzureAuthorizationCodeBearer with values from a config file.

After I've succesfully added authentication to my API following the guide on the docs page, I started to prepare a version that would be production ready. And that would mean: values read from a config file. That's because we use Kubernetes so we have an easy way to provide a ConfigMap for a container.

Let's say this is my API code (I can split this into multiple files, but in this case it doesn't matter):

from fastapi import FastAPI
from fastapi_azure_auth import SingleTenantAzureAuthorizationCodeBearer
from pydantic import BaseSettings
import json

class Settings(BaseSettings):
    client_id: str
    tenant_id: str

with open(os.getenv("CONFIG_FILE", "config.json")) as f:
    config_data = json.load(f)

settings = Settings(client_id=config_data["client_id"], tenant_id=config_data["tenant_id"])

azure_scheme = SingleTenantAzureAuthorizationCodeBearer(
    app_client_id=settings.APP_CLIENT_ID,
    tenant_id=settings.TENANT_ID,
    scopes={
        f'api://{settings.APP_CLIENT_ID}/user_impersonation': 'user_impersonation',
    }
)

api = FastAPI(
   title="foo"
)

def init_api() -> FastAPI:
   # I want to read configuration here
   api.swagger_ui.init_oauth = {"clientID": config.CLIENT_ID}
   return api

@api.on_event('startup')
async def load_config() -> None:
    """
    Load OpenID config on startup.
    """
    await azure_scheme.openid_config.load_config()

@api.get("/", dependencies=[Depends(azure_scheme)])
def test():
   return {"hello": "world"}

This obviously works because both azure_scheme and settings get evaluated before load_config and test route definition.

However, this makes testing ugly, because now there's a config file to be read. Even pytest fixtures cannot be evaluated before imports in tests, so I end up with errors - "no config.json" etc. Plus, even if I'm able to "hack" this, every time I do an import from api.py, this code gets run - it's just not good.

I provided an init_api function. This is a perfect example - in my code I do other stuff there, initialize logging, do some other stuff. That way, in tests, I don't have to call init_api, I just import from <app_name>.api import api and do test on it - therefore I dodge all the stuff that I'd otherwise have to mock.

My only hack was to do something like this:

# [...]
settings = None
azure_scheme = None

api = FastAPI(
   title="foo"
)

def init_api():
    with open(os.getenv("CONFIG_FILE", "config.json")) as f:
        config_data = json.load(f)
    global settings
    settings = Settings(client_id=config_data["client_id"], tenant_id=config_data["tenant_id"])

    global azure_scheme
    azure_scheme = SingleTenantAzureAuthorizationCodeBearer(
        app_client_id=settings.APP_CLIENT_ID,
        tenant_id=settings.TENANT_ID,
        scopes={
            f'api://{settings.APP_CLIENT_ID}/user_impersonation': 'user_impersonation',
        }
    )

    api.add_api_route("/", endpoint=test, dependencies=[Depends(azure_scheme)])
  
@api.on_event('startup')
async def load_config() -> None:
    """
    Load OpenID config on startup.
    """
    if azure_scheme:
        await azure_scheme.openid_config.load_config()

def test():
   return {"hello": "world"}

So I took out the @api decorator and used .add_api_route to init_api. It works, but it's not ideal. In some ways it creates more issues than it solves (typing, more complicated code, access to globals).

Sorry for this long monologue, my question is brief: How can I initialize SingleTenantAzureAuthorizationCodeBearer as a dependency without having to read the config globally? And maybe without so much hacking as I've shown above ๐Ÿ˜„

Update docs

Some references in the docs are not reflecting the latest changes in the package (I suspect).

  • Code example for locking down routes to certain routes. Scopes should be a list: @app.get("/", dependencies=[Security(azure_scheme, scopes='wrong_scope')])
  • Same for multitenant config.
  • Function argument for specifying callable for verifying Azure tenant id should be changed to:
azure_scheme = MultiTenantAzureAuthorizationCodeBearer(
    app_client_id=settings.APP_CLIENT_ID,
    scopes={
        f'api://{settings.APP_CLIENT_ID}/user_impersonation': 'user_impersonation',
    },
    validate_iss=True,
    iss_callable=check_if_valid_tenant
)

[BUG/Question] Minor error in documentation

Thank you for this excellent module - and the fantastic documentation. I found a small typo in the section Calling your APIs from Python
. It should be
'client_id': settings.APP_CLIENT_ID, and not 'client_id': settings.OPENAPI_CLIENT_ID,.

I hope you will continue to maintain this project - it has been a time-saver for me.

[Question] Integration with React Native

Hello, I followed the docs. Everything is well written, and this is probably the best auth library I've seen for FastAPI.

However, I got lost in the woods of FARM stack. How can this library be integrated with a React Native front-end? What flow should one follow? Been lurking around but it seems like a quite uncommon use case.

[Docs] Recipe to mock a logged-in user/Active Directory roles for testing

We are using this to keep the unit tests small and simple and I thought it may be helpful for others. Based on the username and the assigned ActiveDirectory role users have different permissions in the system. For a convenient way of testing this we can now mock any logged in user per-request:

Example test:

def test_readonly_user_may_not_write(client):
    with mockuser(user_name="[email protected]", roles=["foo.read"]):
        response = client.post("/somemodel", json={})
    assert response.status_code == 403

The setup:

in the routes:

def prepare_user(request: Request, db: Session = Depends(get_db)):
    """
    On each request the azure authenticated user is used for local user lookup.
    When not found it will be created. The db user object is then stored in `request.state` for easy access.
    """
    if not hasattr(request.state, "user"):
        raise HTTPException(403, detail="No user information found in request object.")
    email: EmailStr = request.state.user.claims["preferred_username"]  # logged in azure user
    request.state.user_obj = UserRepo.get_or_create(db, email)

router = APIRouter(dependencies=[Security(azure_scheme), Depends(prepare_user)])

conftest.py

class mockuser(ContextDecorator):
    """
    Mock any username and role(s) for api requests as if they would come directly from the real
    Azure Auth integration.
    """

    def __init__(self, user_name="[email protected]", roles=["Bar.readonly"]):
        self.user_name = user_name
        self.roles = roles

    def __enter__(self):
        def mock_prepare_user(request: fastapi.Request, db: Session = fastapi.Depends(get_db)):
            request.state.user = User(
                claims={"preferred_username": self.user_name}, roles=self.roles,
                aud="aud", tid="tid", access_token="123"
            )
            return prepare_user(request, db)

        fastapi_app.dependency_overrides[prepare_user] = mock_prepare_user
        fastapi_app_internal.dependency_overrides[prepare_user] = mock_prepare_user

    def __exit__(self, type, value, traceback):
        del fastapi_app.dependency_overrides[prepare_user]
        del fastapi_app_internal.dependency_overrides[prepare_user]


class staffuser(mockuser):
    """Mock a staff user sending an api request."""
    def __init__(self):
        super().__init__(user_name="[email protected] roles=["yourcompany.superuser"])

[Question] Azure's Authentication feature (Easy Auth)

Hi,

this work looks really great! Is this library targeted to those who deploy their fastAPI application somewhere other than on Azure services where Azure's Easy auth cannot be (or is deliberately not) used but MS is the identity provider?

If that's the case, may I suggest to add a reference for people like me that are not security experts and want to deploy their application to a service managed by Azure (azure function, azure web app etc.). I think this would help non-experts to show them their options and help them to decide if the authentication part should be handled by Azure or if it needs to be in the application itself. Are there any concerns one should consider?

best,
Johannes

Multi tenant support

Current list of new settings (Checked boxes implemented in branch/locally):

  • Allow overriding authorization_url, token_url and description for OpenID documentation
    • Add tests
  • Specify if multi tenant app or not
    • Add tests
  • If multi tenant, force v2 endpoint, also when fetching openid-configuration
    • Add tests
  • If single tenant, provide an option to decide whether it should be using v1 or v2 tokens (and fetch the according openid config)
    • Add tests
  • Support for app_id= parameter for the openid config, to support those (few) who use the claims-mapping feature.
    • Add tests
  • Implement all settings as a pydantic class, to keep it clean skipped, no point
  • Setting to skip validation of iss
    • Add tests
  • Multi tenant requests should go to the common endpoint, not tenant endpoint
    • Add tests
  • Support for passing a callable that will accept tid and return an iss (or raise an invalid auth exception)
    • Add tests
  • Support for using access token to request more information through the graph API. A few paths to take:
    • Make AzureAuthorizationCodeBearer return access token instead of the User object
    • Accept a callable that will be executed and awaited after token validation
    • Append access_token to User object
    • Pre-implement graph features? ๐Ÿค”
    • Add tests
  • Remove allow_guest_users parameter, document how to do that in Azure instead
  • Documentation of new settings
  • Demoproject for multi tenant
  • Document everything about the Azure setup. (I'll do this in Intility/templates, under create-fastapi-app
  • Rename provider_config to openid_config

This will most likely include breaking changes. Might be a v3 bump.

[Question] How to actually authenticate user

This library was easy to set up and works great when I access the /docs link and manually click authenticate. However, I'm using FastAPI as a full web app framework with jinja2 templating pages - not just api calls. Can I use this library for authentication? The security is working because after implementation, my pages are inaccessible and I get "not authenticated" message. However, how do I actually route a user to the microsoft login page to authenticate and then bring them back to the page I want? Have any examples of this?

Sign in user via '/login'-endpoint [BUG/Question]

Hi, I am new to AzureAD and the Authentication-flow (and GitHub).

I am building an API with SingleTenantAzureAuthorization.
I have implemented user-login via swagger 'authorize'-Button and secured my data-endpoints successfully, as shown in the documentation.

But I would like to provide a '/login' endpoint that redirects the user/client to Azures SSO-page, then have them redirected to another endpoint, say 'localhost:port/webinterface'.

I have no clue how to build the POST Request to "https://login.microsoftonline.com/myTenantId/oauth2/v2.0/authorize" including my Scope, redirect_uri and code_challenge. Is this even possible with using the fastapi_azure_auth-package and the azure-scheme and settings? Or do I have to use msal for that?

Make appid/azp claims truely optional

Can you please make appid/azp really optional, we handle autherisation/access through the "assignment required" option and so don't want to have to configure the list of app ids inside fastapi too. It would be really good to make it optionally accept any appid in that claim.

[Question] - How can I get the refresh token after successful authorization?

Hi, I'm trying to gain access to the refresh token after the client clicks authorize.
Is there an option to add an explanation whats going on on the fastapi side once the client clicks authorize?
Also I was wondering if the refresh_token can be added to the User class so I can also implement a token refresh functionality to my app.
Thanks in advance!

[Documentation]: Suggested update in walkthrough

Describe the issue
(https://intility.github.io/fastapi-azure-auth/single-tenant/azure_setup#step-4---allow-openapi-to-talk-to-the-backend)

There should be one final step on this walk through. Azure has added another section under "Exposing an API" called "Authorized client applications". If you do not link the OpenAPI back to the original application, the end-user will be prompted to request consent from an administrator.

To Reproduce
Manually go through the Single Tenant walkthrough (https://intility.github.io/fastapi-azure-auth/single-tenant/). Once fully complete, any attempt to log into Azure via OpenAPI will send the end-user to a page, as described below:

Selection_030

Fix
This is so the end-user doesn't see this prompt.
1.) Go to "Expose an API" under your Sample application (not the OpenAPI).
2.) Under "Authorized client applications" click "Add a client application"

Selection_029

3.) Copy the Application Id from your 'Sample - OpenAPI' into the 'Client ID' field.
4.) Select the exposed 'api://*' check box
5.) Click "Add Application"

Now when you click "Authorize"/"Login" via SwaggerUI, the end user will no longer require Admin consent.

I hope this helps,
-Brian

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.