44# This code is licensed under the MIT License.
55import json
66import logging
7+ import os
8+ import time
79try : # Python 2
810 from urlparse import urlparse
911except : # Python 3
@@ -17,8 +19,18 @@ def _scope_to_resource(scope): # This is an experimental reasonable-effort appr
1719 return "{}://{}" .format (u .scheme , u .netloc )
1820 return scope # There is no much else we can do here
1921
22+
2023def _obtain_token (http_client , resource , client_id = None ):
24+ if "IDENTITY_ENDPOINT" in os .environ and "IDENTITY_HEADER" in os .environ :
25+ return _obtain_token_on_app_service (
26+ http_client , os .environ ["IDENTITY_ENDPOINT" ], os .environ ["IDENTITY_HEADER" ],
27+ resource , client_id = client_id )
28+ return _obtain_token_on_azure_vm (http_client , resource , client_id = client_id )
29+
30+
31+ def _obtain_token_on_azure_vm (http_client , resource , client_id = None ):
2132 # Based on https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http
33+ logger .debug ("Obtaining token via managed identity on Azure VM" )
2234 params = {
2335 "api-version" : "2018-02-01" ,
2436 "resource" : resource ,
@@ -44,3 +56,42 @@ def _obtain_token(http_client, resource, client_id=None):
4456 logger .debug ("IMDS emits unexpected payload: %s" , resp .text )
4557 raise
4658
59+ def _obtain_token_on_app_service (http_client , endpoint , identity_header , resource , client_id = None ):
60+ # Prerequisite: Create your app service https://docs.microsoft.com/en-us/azure/app-service/quickstart-python
61+ # Assign it a managed identity https://docs.microsoft.com/en-us/azure/app-service/overview-managed-identity?tabs=portal%2Chttp
62+ # SSH into your container for testing https://docs.microsoft.com/en-us/azure/app-service/configure-linux-open-ssh-session
63+ logger .debug ("Obtaining token via managed identity on Azure App Service" )
64+ params = {
65+ "api-version" : "2019-08-01" ,
66+ "resource" : resource ,
67+ }
68+ if client_id :
69+ params ["client_id" ] = client_id
70+ resp = http_client .get (
71+ endpoint ,
72+ params = params ,
73+ headers = {
74+ "X-IDENTITY-HEADER" : identity_header ,
75+ "Metadata" : "true" , # Unnecessary yet harmless for App Service,
76+ # It will be needed by Azure Automation
77+ # https://docs.microsoft.com/en-us/azure/automation/enable-managed-identity-for-automation#get-access-token-for-system-assigned-managed-identity-using-http-get
78+ },
79+ )
80+ try :
81+ payload = json .loads (resp .text )
82+ if payload .get ("access_token" ) and payload .get ("expires_on" ):
83+ return { # Normalizing the payload into OAuth2 format
84+ "access_token" : payload ["access_token" ],
85+ "expires_in" : int (payload ["expires_on" ]) - int (time .time ()),
86+ "resource" : payload .get ("resource" ),
87+ "token_type" : payload .get ("token_type" , "Bearer" ),
88+ }
89+ return {
90+ "error" : "invalid_scope" , # Empirically, wrong resource ends up with a vague statusCode=500
91+ "error_description" : "{}, {}" .format (
92+ payload .get ("statusCode" ), payload .get ("message" )),
93+ }
94+ except ValueError :
95+ logger .debug ("IMDS emits unexpected payload: %s" , resp .text )
96+ raise
97+
0 commit comments