Search code examples
oauth-2.0dynamics-crmazure-ad-msal

What auth flow to use with spa and service account msal


There's so many different flows in the Microsoft docs that I have no clue what one is needed for me. I am using React and Python. (I understand node, so if someone explains using node/express its fine)

What user should see: A page with a button to login, nav is there but wont work till logged in. The login creates a popup to sign in with Microsoft account. Once signed in, the user will be able to use nav to see dynamics information.

What I am trying to do: This app needs to sign in a user and obtain the users email through 'https://graph.microsoft.com/v1.0/me'.(no client secrets needed) Then I need to send that email in this request;

(The tenant == {company}.crm.dynamics.com.)

 allInfo = requests.get(
            f'https://{TENANT}api/data/v9.0/company_partneruserses?$filter=company_email eq \'{email}\'', headers=headers).json()

This backend request needs to have a client secret to obtain the information. So I believe my backend also needs to be logged on to a service account. I believe I need to get a token for my backend to make requests on behalf of the service account.

What I have: I have a React frontend that is signing a user in and calling 'https://graph.microsoft.com/v1.0/me' correctly and getting that email. Once I get the email, I am sending it to my backend.

Now I have no clue how to proceed and have tried many things.

What I have tried for backend: Attempt 1: I get a token but error: {'error': {'code': '0x80072560', 'message': 'The user is not a member of the organization.'}}. Problem is, this id is the Azure AD ID. It should def work

@app.route('/dynToken', methods=['POST'])
def get_dyn_token():
    req = request.get_json()
    partnerEmail = req['partnerEmail']
    token = req['accessToken']
    body = {
        "client_id": microsoft_client_id,
        "client_secret": client_secret,
        "grant_type": "client_credentials",
        "scope": SCOPE_DYN,
    }
    TENANTID = '{hash here}'
    res = requests.post(
        f'https://login.microsoftonline.com/{TENANTID}/oauth2/v2.0/token', data=body).json()

    dyn_token = res['access_token']

    headers = {
        "Prefer": "odata.include-annotations=\"*\"",
        "content-type": "application/json; odata.metadata=full",
        "Authorization": f"Bearer {dyn_token}"
    }
    try:
        allInfo = requests.get(
            f'https://{TENANT}api/data/v9.0/company_partneruserses?$filter=company_email eq \'{email}\'', headers=headers).json()
        print(allInfo)

Attempt 2: Same code but instead of f'https://login.microsoftonline.com/{TENANTID}/oauth2/v2.0/token' its f'https://login.microsoftonline.com/common/oauth2/v2.0/token'. Error: An exception occurred: [Errno Expecting value] : 0. Because it returns an empty string.

Now I don't know if I am even on the right path or where to go. I know the routes work themselves if the token is correct. I used only SSR with no react and these routes work. But I need the React to be there too. I just don't know what flow to use here to get what I need. The docs make it easy for /me route to work. But the {company}crm.dynamics.com docs don't really provide what I am trying to do.

Additional info after comment:

What 'f'https://{TENANT}api/data/v9.0/company_partneruserses?$filter=company_email eq '{email}'', headers=headers" is trying to get are API keys. Full code :

 try:
        allInfo = requests.get(
            f'https://{TENANT}api/data/v9.0/company_partneruserses?$filter=company_email eq \'{email}\'', headers=headers).json()

        partner_value = allInfo['value'][0]['_company_partner_value']

        response = requests.get(
            f'https://{TENANT}api/data/v9.0/company_partnerses({partner_value})', headers=headers).json()

        return {'key': response['company_apikey'], 'secret': response['company_apisecret']}

Then once it has the keys:

def api_authentication(apikey, apisecret):
    headers = get_headers() #<-- same headers as above with using dyn_token 
    response = requests.get(
        f'https://{TENANT}api/data/v9.0/company_partnerses?$filter=company_apikey eq \'{apikey}\' and company_apisecret eq \'{apisecret}\'&$select=company_apikey,company_apisecret,_company_account_value,_company_primarycontact_value,blahblah_unassignedhours,company_reporturl', headers=headers).json()
    return response

Afterwards I am able to get all the information I am looking for to send back to my frontend for the client to see. (By making multiple request to crm with these keys)


Solution

  • The client_credentials grant that you are using should work, provided the CRM trusts the token issued to the client (your python backend). Please use MSAL library instead of hand crafting the token request. It will save you time and eliminate errors.