Search code examples
python-3.xazureazure-active-directorymicrosoft-graph-apirefresh-token

Offline_access to get refresh token: "error_description":"AADSTS65001: The user or administrator has not consented to use the application with ID


I'm designing a Python script that can extract and query information and data via Microsoft Dynamics CRM API.

I hope to get a refresh token so that I can get the access token for a long time without logging in frequently.

def login(): 
    authorization_url = (
        f"https://login.microsoftonline.com/{tenant}/oauth2/authorize"
        "?response_type=code"
        f"&client_id={client_id}"
        f"&redirect_uri={redirect_uri}"
        "&response_mode=query&prompt=consent"
        f"&scope={scope}%2f.default openid profile offline_access"
        # f"&scope={scope}"
        # f"&scope={scope}"
        "&state=12345&sso_reload=true"
        f"&code_challenge={code_challenge}"
        "&code_challenge_method=S256"
        f"&nonce={nonce}"
        "&client_info=1"
        )
    
   return render_template("login.html", version=__version__, auth_uri = authorization_url)

After the login is successful, url jumps to the redirect path that has already been added in the Azure Ad. Get the authorization_code from requests and bring it in posting for request refresh code.

@app.route(app_config.REDIRECT_PATH)
def auth_response():
    authorization_code = request.args.get('code')
    refresh_token = _get_refresh_token(tenant,
                                       client_id,
                                       client_secret,
                                       authorization_code,
                                       redirect_uri
                                       )

However, when requesting the posting for a refresh token, the scope carried offline_access and received a Bad Request response.

scope = 'https://<myid>.api.crm3.dynamics.com/user_impersonation'
data = {
    'client_id': client_id,
    'scope': f'{scope}%2f.default openid profile offline_access',
    'code': authorization_code,
    'redirect_uri': redirect_uri,
    'grant_type': 'authorization_code',
    'client_secret': client_secret,
    'code_verifier': code_verifier,
}
response = requests.post(token_endpoint, data=data)

Unable to get authorization for offline_access to get refresh token: "error_description":"AADSTS65001: The user or administrator has not consented to use the application with ID.

Got the error text from response:

'{"error":"invalid_grant","error_description":"AADSTS65001: The user or administrator has not consented to use the application with ID '320ae26a-b04b-49c6-aa2e-dfc77e734f01' named 'PowerAppAPI'. Send an interactive authorization request for this user and resource. Trace ID: ecfc933e-e5ae-4675-927e-bbfc16980700 Correlation ID: 017df973-0078-4c54-8258-163a0fd3705d Timestamp: 2023-12-14 21:18:57Z","error_codes":[65001],"timestamp":"2023-12-14 21:18:57Z","trace_id":"ecfc933e-e5ae-4675-927e-bbfc16980700","correlation_id":"017df973-0078-4c54-8258-163a0fd3705d","suberror":"consent_required"}'

I have added permissions for Dynamic CRM and Microsoft Graph in Azure AD's Application Management and Granted myself.

enter image description here

In fact, access tokens can be obtained and used when the scope did not declare offline_access.

Did I miss any settings in the application management in Azure AD, and how do I get permissions with the access token? Thank you for your answer

Expect the request can respond and return a refresh token for the current Dynamic CRM or Microsoft Graph Scope.


Solution

  • In both your authorization request and token request, you're building the scope parameter with:

    f'{scope}%2f.default openid profile offline_access'
    

    With the scope variable being https://<myid>.api.crm3.dynamics.com/user_impersonation, this results in:

    https://<myid>.api.crm3.dynamics.com/user_impersonation%2f.default openid profile offline_access
    

    That gets form-encoded by requests.post() (which ends up doing a second encoding on %2f), and is sent to Microsoft Entra ID as:

    scope=https%3a%2f%2f<myid>.api.crm3.dynamics.com%2fuser_impersonation%252f.default+openid+profile+offline_access
    

    In your token request, the first scope value there is interpreted by Microsoft Entra ID as "the delegated permission user_impersonation%2f.default at the resource https://<myid>.api.crm3.dynamics.com". Since no delegated permission named user_impersonation%2f.default has been granted, this fails.

    The reason the authorization request (which came first) didn't fail is because you sent that request to the v1.0 endpoint for Microsoft Entra ID, which is the older (not even documented anymore) endpoint. On that endpoint:

    • The only value in the scope parameter that matters is openid, and it only serves to identify that the app wants an ID token. Any other values are ignored entirely (including email, offline_access, profile, and your malformed CRM scope value).
    • The requested resource (API) is identified in the resource parameter. If the parameter is not provided, it is assumed to be Azure AD Graph (https://graph.windows.net, which is deprecated).

    So, recapping the issues, and how to address them:

    1. Problem: You're building your authorization request with parameters for the v2.0 endpoint, but you're actually sending it to the v1.0 endpoint:

      Solution: Send your authorization request to the v2.0 endpoint, as documented:

      https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize
      
    2. Problem: In your token request, you're requesting a Dynamics CRM delegated permission user_impersonation%2f.default, which does not exist and is not granted.

      Solution: Request the Dynamics CRM delegated permission user_impersonation, which does exist. In both your authorization request and token request:

      f"&scope={scope} openid profile offline_access"
      

    Note: You also included prompt=consent in the authorization request. Don't, it's just going to cause problems for you.