Authentication with Azure AD for Azure Resource Manager with Python – The multitenant app approach

This would be the most likely scenario you want to automate Azure infrastructure. Where you are either a multi-cloud infrastructure management provider or Azure only, you will need to incorporate the AD authentication mechanism for a user.

Azure AD implements OAuth2.0 authentication, following are the steps you can follow to set it up.

Please note at the time of writing of this blog, Azure ADL for Python has just been released. We will modify this blog later to add those details.

Meanwhile, you could refer to the complete code at:

https://github.com/shwetams/arm-samples-py 

Pre-requisite

Img1

Step 1: Add application AIM to your subscription

You can add the application through the portal or through command line. We will show the portal route, but you could refer to the link below to do that through command line.

https://azure.microsoft.com/en-us/documentation/articles/active-directory-integrating-applications/

Step 2: Add Azure Service Management & AD app permissions to your application

Under the configure tab, click on Add application under permissions to other applications section:

addapplication

The popup window loads with default Microsoft Apps, select Windows Azure Service management API and select ok.

app_listpermissions

Once the application gets added, click on delegated permissions and select the Access Azure Service Management option.

app_permissions

Then save the settings.

Obtain Authorization from user
These set of steps will need to be done each time a user is added or wants to edit their logon settings. Or perhaps add additional subscriptions they want to manage.

All the steps below have been implemented in the aad_integration module.

Step 3: Redirect user to login using OpenID Connect to get a JWT token

If you take a look at the aad_integration module views.py, function authorise_user() creates the Open ID logon url.

The initialize() method called initializes the client_id, secret and other variables of the application generated and stored in steps above.

The method expects a hint on the domain name whether it is a work account or a Microsoft account. As each of them have a separate logon url generated.

def authorise_user(request):

    initialise()

    PROVIDER_ISSUER_URL = "https://login.windows.net/common/OAuth2/Authorize"

    if request.method == "GET":

       isMSA = False

       noonce_val = str(uuid.uuid4())

       if isMSA == True:

           PROVIDER_ISSUER_URL = "https://login.windows.net/common/OAuth2/Authorize?client_id="+client_id+"&response_mode=form_post&response_type=code+id_token&nonce="+\

noonce_val + "&redirect_uri=https://127.0.0.1:8000/armaccess/&resource=https://management.azure.com/\

&scope=openid+profile&\

domain_hint=live.com"

       else:

           PROVIDER_ISSUER_URL ="https://login.windows.net/common/OAuth2/Authorize?client_id="+client_id+"&response_mode=form_post&response_type=code+id_token&nonce="+ \

noonce_val + "&redirect_uri=https://127.0.0.1:8000/armaccess/&resource=https://management.azure.com/& \

scope=openid+profile"

    return redirect(PROVIDER_ISSUER_URL)

This redirect URI will take the user to the Azure AD login page and request the user to login as shown in the diagram below:

OAuth2Login

Figure 1: Azure AD Login page redirect

Once the user logs in, the user will be automatically redirected to a “Allow access” page where the user will be requested explicit access for the application to access Azure Service Management (Preview).

Note: This will work seamlessly with organizational accounts. However, for Microsoft accounts, the account needs to be explicitly added to the AD of your account.

OAuth2Access

Figure 2: Allow access to application:

Once the user clicks on “Accept”. Azure redirects the response to the Redirect URI provided in the post request. In our case it is https://localhost/armaccess.

The application is added as a Service Principal to the user’s subscription automatically by Azure AD.

Azure returns a JWT token which has user details including tenant ID and an access token.

Step 4: Use JWT to fetch subscriptions the logged in user has authority to manage

The returned Json Web Token has all the details of the user, following is a sample JWT token.

The code sample uses Python library jwt to decode the JSON web token. Please take a look at the armaccess function in views.py of aad_integration module.

We first retrieve the token which returns in the POST request.

id_token = request.POST["id_token"]

       

token_payload = jwt.decode(id_token,verify=False)

Then we call the access_token method to get the access token for next steps.

access_token = get_access_token(tenant_id=str(token_payload["tid"]),client_id=client_id,code=code,client_secret=client_secret, \

redirect_uri=redirect_uri)

Please do keep in mind that the tenant_id we are passing in this case is not the tenant_id of the application but it is of the customer who has logged in and returned with the JWT token as tid.

With this token that is returned we retrieve the list of subscriptions the user has linked to his/her account:

Let’s take a look at the get_subscription_list function. The url to get the subscription list is:

https://management.azure.com/subscriptions?api-version=2014-04-01-preview

We use the GET method and pass the token obtained in the earlier step in the header.

def get_subscription_list(token_det):

    initialise()

    subs_url = "https://management.azure.com/subscriptions?api-version=2014-04-01-preview"

    headers = {"Authorization":"Bearer " + token_det }

    req = Request(method="GET",url=subs_url,headers=headers)

    req_prepped = req.prepare()

    s = Session()

    res = Response()

    res = s.send(req_prepped)

    subs_list = []

    if (res.status_code == 200):

        resJSON = json.loads(res.content)

        subs_list = resJSON["value"]

    else:

        subs_err = {}

        subs_err["state"]="error"

        subs_err["subscriptionId"]="Error"

        subs_err["displayName"] = res.content + "token " + token_det

        subs_list.append(subs_err)

    return subs_list

The API returns an array of JSON objects containing the SubscriptionID (subscriptionID), Display name (displayName) and if it is enabled.

But before we move to the next step, we should ideally filter out subscriptions that the particular user doesn’t have access rights to manage. This is because we will be adding the application as a service principal on each of these subscriptions using the token retrieved by user details.

Let’s take a look at the following function:

def get_subscription_permissions(token_det, subscription_id):

    initialise()

    url = "https://management.azure.com/subscriptions/"+subscription_id+"/providers/ \

microsoft.authorization/permissions?api-version=2014-07-01-preview"

    headers = {"Authorization":"Bearer " + token_det}

    req = Request(method="GET",url=url,headers=headers)

    req_prepped = req.prepare()

    s = Session()

    res = Response()

    hasPermission = False

    #hasPermission = {}

    res = s.send(req_prepped)

    per_sp = "microsoft.authorization/roleassignments/write"

    per_gen = "microsoft.authorization/*/write"

    hasPermission = res.content

    if (res.status_code == 200):

        resJSON = json.loads(res.content)

        actions_result = resJSON["value"]

        for actions_r in actions_result:

            actions = actions_r["actions"]

            notactions = actions_r["notActions"]

            if per_sp in actions:

                if per_gen not in notactions:

                    hasPermission = True

            else:

                if per_gen in actions:

                    if per_gen not in notactions:

                        hasPermission = True

    return hasPermission

Basically, the function is called for every subscription ID that is returned and then checks for the permissions the user has. If either the permission of role assignment (microsoft.authorization/roleassignments/write) or a generic permission that has all writes (microsoft.authorization/*/write) in Actions and not present in notActions then the function returns True. Which is then included in the subscription list below and sent back as a response as shown below.

ListSubscription

Figure 3: List subscriptions

I had created a free trial account with my Office 365 ID. Hence it lists only one subscription in the screenshot.

Step 5: Let the user select from the list of subscriptions they want to manage

As the screenshot above shows the application will return a list of subscriptions that the user can select from. Once the user selects the subscription she/he wants the application to deploy infrastructure on and clicks on Connect. Following is the assignrole(request) function which is invoked on the server side when a user clicks the button.

The html page sends a list of subscription IDs, User ID & Tenant ID in the POST request

def assignrole(request):

    initialise()

   subs_list_assigned = []

    if request.method == "POST":

        subs_list = json.loads(request.POST["subs_list"])

        user_id = request.POST["user_id"]

        t_id = request.POST["tenant_id"]

        token_user = request.POST["token_user"]

We need to again use the tenant_id of the user to generate the token that will be used to get the service principal ID that was created when the user had clicked on Accept.

Please do make a note of the resource URI that we are passing it is different from what we generally call:

get_acacess_token_app(tenant_id=t_id,client_id=client_id,client_secret=client_secret, \

resource= "https://graph.windows.net/" )

The function get_principal_id(token=access_token["details"],tenant_id=t_id,client_id=client_id)

Is called to get the Principal ID. Following is its body.

def get_principal_id(token,tenant_id,client_id):

    initialise()

   

    get_url = "https://graph.windows.net/" + tenant_id + "/servicePrincipals?api-version=1.5&$filter=appid%20eq%20'"+ client_id +"'"

    headers = {"Authorization": "Bearer " + token}

    req = Request(method="GET",url=get_url,headers=headers)

    req_prepped = req.prepare()

    principalId = None

    s = Session()

    res = Response()

    res = s.send(req_prepped)

    if (res.status_code==200):

        responseJSON = json.loads(res.content)

        value = responseJSON["value"]

        for val in value:

            principalId = val["objectId"]

           

    return principalId

It returns the ObjectId value which is the Principal ID of the application for the user’s account.

Step 6: Add AIM as a contributor to all the subscriptions selected by user

Once we have the Principal ID and the list of subscriptions that the user can manager, we need to Assign the application as a contributor role to all the subscriptions:

Please take a look at the code snippet we create a PUT request which sends the body that contains the principal ID, the access rights the application needs and the Contributor’s role ID which is:

### Predefined role definition ID

role_def_id_reader = "acdd72a7-3385-48ef-bd42-f606fba81ae7"

role_def_id_contributor = "b24988ac-6180-42a0-ab88-20f7382dd24c"

put_url = "https://management.azure.com//subscriptions/" + subs +"/providers/Microsoft.Authorization/roleAssignments/" + role_assgn_id + "?api-version=2014-07-01-preview"

                body = {"properties":{"roleDefinitionId":"/subscriptions/"+ subs +"/providers/Microsoft.Authorization/roleDefinitions/" + role_def_id_contributor ,"principalId":principal_id}}

             

                headers_val = {"Authorization":"Bearer " + token_user, "Content-Type":"application/json"}

                req = Request(method="PUT",headers=headers_val,json=body,url=put_url)

                req_prepped = req.prepare()

                s = Session()

                res = Response()

                res = s.send(req_prepped)

                if res.status_code == 201:

                    subs_list_assigned.append({"subscriptionid":subs,"assigned_status":True})

     

Step 7: Save the subscription IDs and application’s configuration against the user

In this sample application, we are storing the user’s configuration in the default SQLLite storage.

The application stores a JSON array of subscription IDs, the client_id, client_secret (same as the application’s registered one) and tenant_id.

When a user deploys resources through the application, the application will use these details to generate an Auth token and submit the ARM requests.

auth_settings = {}

    auth_settings['auth_type']="A"

    auth_settings['client_id']=client_id

    auth_settings['tenant_id']=t_id

    auth_settings['redirect_uri']=redirect_uri

    auth_settings['client_secret']=client_secret

    auth_settings['subscription_id']=json.dumps(subscription_list)

    auth_settings['user'] = user_id

    user = User.objects.get(pk=auth_settings['user'])

    if AuthSettings.objects.filter(user_id_id=auth_settings['user']).exists():

         u = AuthSettings.objects.get(user_id_id=auth_settings['user'])

         u.delete()

    settings = AuthSettings(user_id=user,auth_type=auth_settings['auth_type'],client_id=auth_settings['client_id'],tenant_id=auth_settings['tenant_id'],client_secret=auth_settings['client_secret'],redirect_uri=auth_settings['redirect_uri'],subscription_id=auth_settings['subscription_id'])

    settings.save()

Once this is done, the application returns the user a confirmation as shown above (it’s a basic message, you could modify it to make it more pretty! The user is not ready to deploy infrastructure on his/her set of subscriptions.