Search code examples
pythonazureazure-functionsusergroupseasy-auth

Azure Function App using python: How to access user groups for authorization


I am very new to Azure Function Apps and OAuth so please bear with me.

My Setup

I have an Azure Function App with a simple python-function doing nothing else but printing out the request headers:

import logging

import azure.functions as func

def main(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function processed a request.')    

    name = req.params.get('name')
    if not name:
        try:
            req_body = req.get_json()
        except ValueError:
            pass
        else:
            name = req_body.get('name')

    if name:
        aadIdToken = req.headers.get('X-MS-TOKEN-AAD-ID-TOKEN')
        aadAccessToken = req.headers.get('X-MS-TOKEN-AAD-ACCESS-TOKEN')
        principalID = req.headers.get('X-MS-CLIENT-PRINCIPAL-ID')
        principalName = req.headers.get('X-MS-CLIENT-PRINCIPAL-NAME')
        idProviderId = req.headers.get('X-MS-CLIENT-PRINCIPAL-IDP')
        aadRefreshToken = req.headers.get('X-MS-TOKEN-AAD-REFRESH-TOKEN')

        clientPrincipal = req.headers.get('X-MS-CLIENT-PRINCIPAL')

        result = "\n"
        myDict = sorted(dict(req.headers))
        for key in myDict:
            result += f"{key} = {dict(req.headers)[key]}\n"

        return func.HttpResponse(
            f"Hello, {name}. How are you ? Doing well ?"\
            f"\n\nHere is some data concerning your Client principal:"\
            f"\nThis is your X-MS-CLIENT-PRINCIPAL-ID: {principalID}"\
            f"\nThis is your X-MS-CLIENT-PRINCIPAL-NAME: {principalName}"\
            f"\nThis is your X-MS-CLIENT-PRINCIPAL-IDP: {idProviderId}"\
            f"\nThis is your X-MS-CLIENT-PRINCIPAL: {clientPrincipal}"\
            f"\n\nHere is some data concerning your AAD-token:"\
            f"\nThis is your X-MS-TOKEN-AAD-ID-TOKEN: {aadIdToken}"\
            f"\nThis is your X-MS-TOKEN-AAD-ACCESS-TOKEN: {aadAccessToken}"\
            f"\nThis is your X-MS-TOKEN-AAD-REFRESH-TOKEN: {aadRefreshToken}"\
            f"\n\n\nresult: {result}"\
        )
    else:
        return func.HttpResponse(
             "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.",
             status_code=200
        )

I followed this guide to let the user authenticate via EasyAuth before calling the function.
This seems to work fine. When accessing the function via browser I am redirected to sign-in. After successful sign-in I am then redirected again and the HTTP response is printed out in the browser. As I am able to access X-MS-CLIENT-PRINCIPAL-ID and X-MS-CLIENT-PRINCIPAL-NAME I suppose the authentication was successful. However when printing out the whole request header I did not find a X-MS-TOKEN-AAD-REFRESH-TOKEN, X-MS-TOKEN-AAD-ACCESS-TOKEN or X-MS-TOKEN-AAD-ID-TOKEN.
This is the output (output too large; below the output shown in the screenshot I can see the header content): First half of my output

My question

What I am trying to do now is to access the groups assigned to the logged-in user via the python code of the function to further authorize his request (e.g. "user can only execute the function when group xyz is assigned, else he will be prompted 'not allowed'").
To achieve this I added the "groups"-claim to the Token Configuration of my App Registration.

From what I understand accessing the user groups via a function coded in .NET is easily possible by using the ClaimsPrinciple object (source).

How would I be able to access the user assigned groups via python code?
Is that possible?
Am I understanding something completely wrong?

Followup:
One thing that I do not understand by now, is that I can see an id_token in the callback-http-request of the browser-debuggger when accessing the function via browser for the first time (to trigger sign in): Browser debugger: id_token in callback-request

When I decrypted that token using jwt.io I was able to see some IDs of assigned user groups which seems to be exactly what I want to access via the python code.
Re-loading the page (I suppose the request then uses the already authenticated browser session) makes the callback disappear.


Solution

  • The header X-MS-CLIENT-PRINCIPAL contains the same claims as the id_token. So if we want to get the group claim, we can base64 decode the header.

    For example

    My code

    import logging
    
    import azure.functions as func
    import base64
    
    def main(req: func.HttpRequest) -> func.HttpResponse:
        logging.info('Python HTTP trigger function processed a request.')
    
        name = req.params.get('name')
        if not name:
            try:
                req_body = req.get_json()
            except ValueError:
                pass
            else:
                name = req_body.get('name')
    
        if name:
            aadAccessToken = req.headers.get('X-MS-TOKEN-AAD-ACCESS-TOKEN')
            principalID = req.headers.get('X-MS-CLIENT-PRINCIPAL-ID')
            principalName = req.headers.get('X-MS-CLIENT-PRINCIPAL-NAME')
            idProviderId = req.headers.get('X-MS-CLIENT-PRINCIPAL-IDP')
            aadRefreshToken = req.headers.get('X-MS-TOKEN-AAD-REFRESH-TOKEN')
    
            clientPrincipal = req.headers.get('X-MS-CLIENT-PRINCIPAL')
            clientPrincipal= base64.b64decode(clientPrincipal)
    
            result = "\n"
            myDict = sorted(dict(req.headers))
            for key in myDict:
                result += f"{key} = {dict(req.headers)[key]}\n"
    
            return func.HttpResponse(
                f"Hello, {name}. How are you ? Doing well ?"\
                f"\n\nHere is some data concerning your Client principal:"\
                f"\nThis is your X-MS-CLIENT-PRINCIPAL-ID: {principalID}"\
                f"\nThis is your X-MS-CLIENT-PRINCIPAL-NAME: {principalName}"\
                f"\nThis is your X-MS-CLIENT-PRINCIPAL-IDP: {idProviderId}"\
                f"\nThis is your X-MS-CLIENT-PRINCIPAL: {clientPrincipal}"\
                f"\n\nHere is some data concerning your AAD-token:"\
                f"\nThis is your X-MS-TOKEN-AAD-ID-TOKEN: {aadIdToken}"\
                f"\nThis is your X-MS-TOKEN-AAD-ACCESS-TOKEN: {aadAccessToken}"\
                f"\nThis is your X-MS-TOKEN-AAD-REFRESH-TOKEN: {aadRefreshToken}"\
                f"\n\n\nresult: {result}"\
            )
        else:
            return func.HttpResponse(
                 "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.",
                 status_code=200
            )
    
    

    enter image description here