fastapi-azure-auth: [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
}

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 19 (11 by maintainers)

Most upvoted comments

Hi,

That’s not a silly question! You would use something called Client Credentials flow. Basically just create a secret for your app reg and do something like this:

from aiohttp import ClientSession

payload = {
    'grant_type': 'client_credentials',
    'client_id': client_id,
    'client_secret': client_secret,
    'scope': scope
}
async with ClientSession() as azure_client:
    async with azure_client.post(f'https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token', data=payload) as azure_response:
        azure_response = await azure_response.json()
        token = azure_response['access_token']
        print(token)

or if you use httpx:

from httpx import AsyncClient

payload = {
    'grant_type': 'client_credentials',
    'client_id': client_id,
    'client_secret': client_secret,
    'scope': scope
}

async with AsyncClient() as azure_client:
    response = await azure_client.post(url=f'https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token', data=body)
        token = response.json()['access_token']
        print(token)

If I don’t remember wrong, you have to use the .default scope. So if your backend app reg client ID is abcd then your scope should be api://abcd/.default.

Hi!

There’s two ways, either using the request object as seen here, or adding a dependency in the input of your function, as seen here.


@app.get("/")
def read_root(user: User = Security(azure_scheme, scopes=["user_impersonation"]):
    return user.dict()

Hi,

The application created for OpenAPI seems to be configured for web and not Spa. See this section:

bilde

The application itself should looke something like this:

"replyUrlsWithType": [
		{
			"url": "http://localhost:8000",
			"type": "Web"
		}
	],

and the OpenAPI application registration should look something like this:

	"replyUrlsWithType": [
		{
			"url": "http://localhost:8000/oauth2-redirect",
			"type": "Spa"
		}
	],