question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

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

See original GitHub issue

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
}

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:17 (10 by maintainers)

github_iconTop GitHub Comments

1reaction
JonasKscommented, Mar 21, 2022

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.

1reaction
JonasKscommented, Mar 10, 2022

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()

Read more comments on GitHub >

github_iconTop Results From Across the Web

Cross-origin token redemption is permitted only for the 'Single ...
Cross-origin token redemption is permitted only for the 'Single-Page Application ' In angular . While genrating token for using graph api in ...
Read more >
AADSTS9002326: Cross-origin token redemption is permitted ...
When I run the code block below, I get 400 bad request errors. Proxy code '/payment': { target: 'https://apitest.domain ...
Read more >
Do I need to enable Cors? Tokens issued for the 'Single-Page ...
Tokens issued for the 'Single-Page Application' client-type may only be redeemed via cross-origin requests.
Read more >
Add `Origin` header to OAuth 2.0 flow - Help - Postman
Error: AADSTS9002327: Tokens issued for the 'Single-Page Application' client-type may only be redeemed via cross-origin requests.
Read more >
Making cross-origin, browser-side API requests
Zendesk only implements CORS for API requests authenticated with OAuth ... If the end user grants your app access, the token is sent...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found