Skip to content

[BUG/Question] Use Organizational Account authentication from Excel #232

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
romaindup opened this issue Feb 14, 2025 · 12 comments
Closed

[BUG/Question] Use Organizational Account authentication from Excel #232

romaindup opened this issue Feb 14, 2025 · 12 comments
Labels
question Further information is requested

Comments

@romaindup
Copy link
Contributor

Describe the bug
I would like to connect to FastAPI using Excel's PowerQuery and the Organizational Account authentication for the Web connector. When Excel tries to sign in, it sends a request with an empty Bearer token, and it expects a 401 response with the Entra ID authorize URI (see docs). But, at the moment, FastAPI is returning a 401 error {"detail":"Invalid token format"} with a header 'www-authenticate': 'Bearer'.

How can I return a custom 401 error so that Excel PowerQuery can request its OAuth2 token and then interact with FastAPI?

To Reproduce

  1. Setup FastAPI as in the example provided in the documentation
  2. Confirm authenticated access from the docs works
  3. Confirm authenticated access using client secret (python access) works
  4. Open Excel and go to Data > From Web. Enter the url http://localhost:5000 and try to connect
  5. Once the authentication error is returned, select the Organizational account tab and try to sign in
  6. See error We were unable to connect because this credential type isn't supported for this resource. Please choose another credential type.

Stack trace
FastAPI debug logs:

INFO:     Application startup complete.
WARNING 2025-02-14 11:26:52,010 fastapi_azure_auth Malformed token received. . Error: Not enough segments
Traceback (most recent call last):
  File "C:\code\.venv\Lib\site-packages\jwt\api_jws.py", line 269, in _load
    signing_input, crypto_segment = jwt.rsplit(b".", 1)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: not enough values to unpack (expected 2, got 1)

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "C:\code\.venv\Lib\site-packages\fastapi_azure_auth\auth.py", line 153, in __call__
    header: dict[str, Any] = get_unverified_header(access_token)
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\code\.venv\Lib\site-packages\fastapi_azure_auth\utils.py", line 23, in get_unverified_header
    return dict(jwt.get_unverified_header(access_token))
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\code\.venv\Lib\site-packages\jwt\api_jws.py", line 256, in get_unverified_header
    headers = self._load(jwt)[2]
              ^^^^^^^^^^^^^^^
  File "C:\code\.venv\Lib\site-packages\jwt\api_jws.py", line 272, in _load
    raise DecodeError("Not enough segments") from err
jwt.exceptions.DecodeError: Not enough segments
INFO:     127.0.0.1:60214 - "GET / HTTP/1.1" 401 Unauthorized
INFO:     127.0.0.1:60214 - "GET /web?SDKClientVersion=7.0.0.2067 HTTP/1.1" 404 Not Found
INFO:     127.0.0.1:60214 - "GET /web?SDKClientVersion=7.0.0.2067 HTTP/1.1" 404 Not Found
INFO:     127.0.0.1:60214 - "OPTIONS / HTTP/1.1" 405 Method Not Allowed

Your configuration
Backend Entra ID's app registration:

{
	"id": "uuuuuuuuuuuuuuuuuuuuuuuuuuuuu",
	"deletedDateTime": null,
	"appId": "ttttttttttttttttttttttttttttttttttttttttttttttttt",
	"applicationTemplateId": null,
	"disabledByMicrosoftStatus": null,
	"createdDateTime": "2025-02-10T05:37:28Z",
	"displayName": "LNG Asset Optimisation - Dev",
	"description": null,
	"groupMembershipClaims": null,
	"identifierUris": [
		"api://vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv"
	],
	"isDeviceOnlyAuthSupported": null,
	"isFallbackPublicClient": null,
	"nativeAuthenticationApisEnabled": null,
	"notes": null,
	"publisherDomain": "example.com",
	"serviceManagementReference": null,
	"signInAudience": "AzureADMyOrg",
	"tags": [],
	"tokenEncryptionKeyId": null,
	"samlMetadataUrl": null,
	"defaultRedirectUri": null,
	"certification": null,
	"optionalClaims": null,
	"requestSignatureVerification": null,
	"addIns": [],
	"api": {
		"acceptMappedClaims": null,
		"knownClientApplications": [],
		"requestedAccessTokenVersion": 2,
		"oauth2PermissionScopes": [
			{
				"adminConsentDescription": "Allows the app to access the API as the user.",
				"adminConsentDisplayName": "Access API as user",
				"id": "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww",
				"isEnabled": true,
				"type": "User",
				"userConsentDescription": "Allows the app to access the API as you.",
				"userConsentDisplayName": "Access API as you",
				"value": "user_impersonation"
			}
		],
		"preAuthorizedApplications": [
			{
				"appId": "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
				"delegatedPermissionIds": [
					"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
				]
			},
			{
				"appId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
				"delegatedPermissionIds": [
					"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
				]
			}
		]
	},
	"appRoles": [],
	"info": {
		"logoUrl": null,
		"marketingUrl": null,
		"privacyStatementUrl": null,
		"supportUrl": null,
		"termsOfServiceUrl": null
	},
	"keyCredentials": [],
	"parentalControlSettings": {
		"countriesBlockedForMinors": [],
		"legalAgeGroupRule": "Allow"
	},
	"passwordCredentials": [],
	"publicClient": {
		"redirectUris": []
	},
	"requiredResourceAccess": [
		{
			"resourceAppId": "00000003-0000-0000-c000-000000000000",
			"resourceAccess": [
				{
					"id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbd",
					"type": "Scope"
				}
			]
		}
	],
	"verifiedPublisher": {
		"displayName": null,
		"verifiedPublisherId": null,
		"addedDateTime": null
	},
	"web": {
		"homePageUrl": null,
		"logoutUrl": null,
		"redirectUris": [
			"http://localhost:5000"
		],
		"implicitGrantSettings": {
			"enableAccessTokenIssuance": false,
			"enableIdTokenIssuance": false
		},
		"redirectUriSettings": [
			{
				"uri": "http://localhost:5000",
				"index": null
			}
		]
	},
	"servicePrincipalLockConfiguration": {
		"isEnabled": true,
		"allProperties": true,
		"credentialsWithUsageVerify": true,
		"credentialsWithUsageSign": true,
		"identifierUris": false,
		"tokenEncryptionKeyId": true
	},
	"spa": {
		"redirectUris": []
	}
}
@romaindup romaindup added the question Further information is requested label Feb 14, 2025
@romaindup
Copy link
Contributor Author

romaindup commented Feb 14, 2025

I've confirmed that the authentication works if I raise this 401 exception:

                if access_token is None:
                    raise InvalidAuth('No access token provided', request=request)
                if access_token == "":
                    raise HTTPException(
                        status_code=401,
                        detail="Not authenticated",
                        headers={"WWW-Authenticate": f"Bearer authorization_uri={self.authorization_url}"},
                    )

Plus a bit of hacking to make sure the exception is not caught and raised as an InvalidAuth exception. And changing the application id uri to http://localhost:5000 for local testing.

Now, the question is whether that could be supported natively in fastapi-azure-auth?

@JonasKs
Copy link
Member

JonasKs commented Feb 24, 2025

Could you try the latest release?

@romaindup
Copy link
Contributor Author

I've just tried, and I'm still getting the same issue. Excel start the authentication process by sending a header Authorization: Bearer and fastapi-azure-auth fails to process it with ValueError: not enough values to unpack (expected 2, got 1).
Excel is expecting an 401 error with header "WWW-Authenticate": f"Bearer authorization_uri={self.authorization_url}"

@JonasKs
Copy link
Member

JonasKs commented Feb 24, 2025

Hmm.. These are documented, I guess. But they are also optional to implement. I find it strange that Excel don't allow for customization of these, without relying on the API.

PR for a fix welcome, of course.

I suspect something like this would be easiest:

class UnauthorizedHttp(HTTPException):
    """HTTP exception for authentication failures"""

    def __init__(self, detail: str, authorization_url: str | None = None, client_id: str | None = None ) -> None:
        header_value = 'Bearer'
        if authorization_url:
            header_value += f', authorization_uri="{authorization_url}"'
        if client_id:
            header_value += f', client_id="{client_id}"'
        super().__init__(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail={"error": "invalid_token", "message": detail},
            headers={"WWW-Authenticate": header_value},
        )

and then fix Unauthorized to accept the same parameters. And then something like this, here:

            except Exception as error:
                log.warning('Malformed token received. %s. Error: %s', access_token, error, exc_info=True)
                raise Unauthorized(
                    detail='Invalid token format', 
                    client_id=self.app_client_id,
                    authorization_url=self.authorization_url,
                    request=request
                ) from error

CC @davidhuser

@romaindup
Copy link
Contributor Author

As suggested, raised a Pull Request to add support.
I've tested it locally with Excel and it's working. It still logs the exception ValueError: not enough values to unpack (expected 2, got 1) on the initial request with empty Bearer token, but I guess that's useful.

@romaindup
Copy link
Contributor Author

The checks on the PR are successful except for the upload of the code coverage: error - 2025-03-04 02:38:47,655 -- Upload failed: {"message":"Token required - not valid tokenless upload"}. I don't think that's related to my change, but not sure if it's blocking the review/merging process

@JonasKs
Copy link
Member

JonasKs commented Mar 6, 2025

It is not, thanks for the PR! ☺️
I'm out with a concussion, so I can't really review now, sorry.

Maybe @davidhuser has some thoughts.

@romaindup
Copy link
Contributor Author

Sorry to hear that. Hopefully you'll recover quickly and fully.

Let's see what @davidhuser thinks in the meantime.

@davidhuser
Copy link
Contributor

I left a comment in the PR. Implementation looks good. I cannot comment on the validity of the requirement itself as I'm not an Azure admin anymore but I trust it's ok 🙂

I'm not sure whether to document the behavior in a new docs page or section or docstring, though.

@romaindup
Copy link
Contributor Author

In terms of documentation, I'm thinking an optional step in the azure setup doc.

Something along the line of?
To enable Excel to query the API using PowerQuery, add the Excel client a672d62c-fc7b-4e81-a576-e60dc46e951d to the backend application's authorized clients.

@JonasKs
Copy link
Member

JonasKs commented Mar 11, 2025

I don’t think we need to explicitly document it, honestly. It’ll just “work” for those who needs it.

@JonasKs
Copy link
Member

JonasKs commented Mar 18, 2025

Released in 5.1.1 - thanks so much. 😊

@JonasKs JonasKs closed this as completed Mar 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants