Microsoft 365 Authentiication Error

Hi everyone,

Since many weeks, I encountered a problem on Microsoft 365 authentication for Emails Accounts.

I had already see the topic and try all proposals solutions, but didn’t work.

I followed the instructions scrupulously on

and

https://frappeframework.com/docs/user/en/microsoft-email-oauth

To connect my accounts, but didn’t work.

I’m using ERPNext 15.29.3 and frappe 15.34.1.

the problem seems to be at the level of the token refresh. When I connect an account, he work between 1 hour and 1h30, but when it expires, I’m forced to delete token in Token Cache List, and re-authorize connection on Email Account.

I have tried to execute the comportement of TokenCache.update_data in a console. So, in database, the token appears to be updated correctly, but the error persists.

I’m so puzzled. I have the same configuration in Azure in production, the same versions of frappe and ERPNext, and it work’s. The only one difference, in development, ERPNext is build with custom image of frappe, to customize EmailAccount.pull, but no modification on token management.

So, the errors :


Open one of your token cache and check the field refresh_token, it should be populated with ***********

If not make sure the offline_access scope is enabled on azure side and used during the flow on frappe connected app side.

1 Like

The refresh token is here, and I checked many time the presence of offline_access in Azure :frowning:

image

I have also tried this workflow :

get token with refresh token in token cache :
try to update token cache with new data and try to connect => don’t work

drop token cache
create new token cache with token get in first step : work’s fine

  1. note down the access_token and refresh_token of working token cache
  2. note down the access_token and refresh_token before update token cache
  3. note down the access_token and refresh_token after update token cache

use token_cache.get_password("access_token") and token_cache.get_password("refresh_token") for string output.

If you have openid in the scope, try to make get request with bearer token to “userinfo” endpoint and check if the token is valid and returns the profile. For each above noted token. OpenID Connect (OIDC) on the Microsoft identity platform - Microsoft identity platform | Microsoft Learn

If you are doing everything in bench console, make sure you have set the frappe.session.user as you are Administrator when using console.

I have openind in my scopes :

For all tests, I reach an error 401.

I don’t use Administrator for frappe.session.user, because, when users setup an email account, they use their account for authentication :

I don’t know if you can access the userinfo endpoint like that or it should have azure tenent id in the url.

Basically we need to check: if the token is valid or not before and after the update. And the data is stored correctly on frappe side. If something is missing (incorrect data, invalid token) it will cause 403.

So, I have tried to request graph.microsoft.com API, which should allow me to test the token, but without result and again 401 Unauthorized.

In the meantime, I analyzed token informations.
my Issuer is : "https://sts.windows.net/ (api use in old time for auth v1)
and token version is 1.0.

But, my URIs token and authorization are in v2.0 :
image

I don’t know about combination of versions with their endpoints. Someone who is actively using them can guide you.

So, since recovering a token from the refresh token of the non-functional token, deleting the old token, then creating a new one with the information from the generated token works, I am quite confident about the information carried by the token and suspicious about token_cache.update_data or internal usage of token after update.

Full script API test :

app = frappe.get_doc("Connected App", "h5v40g02t4")
token_cache = app.get_token_cache("aurelien.martinet@absys.fr")
email_account = frappe.get_doc("Email Account", "Amartinet")

oauth_session = app.get_oauth2_session("aurelien.martinet@absys.fr")

new_token = oauth_session.refresh_token(
	body=f"redirect_uri={app.redirect_uri}",
	token_url=app.token_uri,
)

token_cache.update_data(new_token)


try:
    email_server = email_account.get_incoming_server(in_receive=True)
    frappe.log_error("It work's !")
except:
    frappe.log_error("first try not work.")
    

frappe.delete_doc("Token Cache", "h5v40g02t4-aurelien.martinet@absys.fr", ignore_permissions=True, force=True, delete_permanently=True)

frappe.get_doc({
        "doctype": "Token Cache",
        "user": "aurelien.martinet@absys.fr",
        "connected_app": "h5v40g02t4",
        "access_token": new_token['access_token'],
        "refresh_token": new_token['refresh_token'],
        "expires_in": new_token['expires_in'],
        "provider_name": "Microsoft",
        "token_type": "Bearer"
    }
).insert()


try:
    email_server = email_account.get_incoming_server(in_receive=True)
    frappe.log_error("It work's after deletion !")
except:
    frappe.log_error("Again Authenticate Failed.")

printed result :

Following is expected usage. get_active_token does refresh if required.

app = frappe.get_doc("Connected App", "h5v40g02t4")
token_cache = app.get_active_token("aurelien.martinet@absys.fr")

The result is the same :frowning:

I found this thread Access Token Issuer from Azure AD is sts.windows.net Instead Of login.microsoftonline.com - Stack Overflow

Very interesting resource, thank’s. I will try this with my Azure Administrator today.

So,

I have good value for accessTokenAcceptedVersion in the Manifest of my application.
I have found other similar thread, who talk about scopes, and I have tried to delete all scope, and token is already in v1.

I’m very confused. I have verifying many many time configuration in Prod where he work, also with token v1, vs dev where is down. Only ID’s are differents …