I have an MSAL app that creates authentication tokens for accessing various Microsoft APIs.
I provide the app specific scopes, and it creates a corresponding authentication token bearing those scopes. This app works perfectly fine for all types of endpoint I tried up
def _create_or_get_msal_app_client(
self, client_id: str, tenant_id: str | None = None, is_confidential_client: bool = False
) -> msal.ClientApplication:
"""
Create public or confidential msal app client to generate tokens
:param client_id: the client id (also known as application id)
:param tenant_id: the tenant id to use as authority, if not provided will use common authority
:return: the msal app
"""
if self._msal_app:
return self._msal_app
try:
authority = tenant_id if tenant_id else "common"
authority_url = f"https://login.microsoftonline.com/{authority}"
if is_confidential_client:
self._msal_app = msal.ConfidentialClientApplication(
client_id=[client_id], client_credential=[client_credential], authority=authority_url
)
else:
self._msal_app = msal.PublicClientApplication(client_id=client_id, authority=authority_url)
return self._msal_app
msal_app = self._create_or_get_msal_app_client(
client_id=[client_id], tenant_id=[tenant_id]
)
return msal_app.acquire_token_by_username_password(
username=[username], password=[password], scopes=[some scopes]
)
The tokens produced if inputted into jwt.io, will be marked as invalid, which is not a bad thing in itself, as noted by this qustion.
My problem is, when I try to call APIs with endpoints of type:
https://admin.powerplatform.microsoft.com/api/*
It almost seems like those kinds of endpoints has a different authorization system than the rest of the endpoints; For once, the token this EP uses in the UI I tool it from have a perfectly valid signature when trying to decode it in JTW.io, as opposed to the token issues by MSAL. But, this means that now I get in the response a 401 response when I try to use the MSAL-issues tokens, and the reason for the failed request, is, according to the response header resp.headers._store['www-authenticate'][1] is:
Bearer error="invalid_token", error_description="The signature is invalid"
This doesn't happen in any other Microsoft API I tried to call; for example in EPs of type https://graph.microsoft.com/v1.0/* the token produced by MSAL works perfectly fine.
The prime suspect in these types of authentication errors is the scopes asked. But no matter what scopes I ask, whether I ask for insufficient or sufficient or no scopes at all, I still get the same error.
Except what was suggested here to try to ask for the scope [client_id]/.defualt (where client id is the client id) but when I try to do that I get the error:
Bearer error="invalid_token", error_description="The audience \'[client_id]\' is invalid"
in the response headers.
I have another clue about what might be the problem in this forum, where the one asking the question mentioned that the EP is using OAuth. could it be that this is different from MS Graph in any way?
So my question is, how do I configure my MSAL app to work with https://admin.powerplatform.microsoft.com/api/*? Or alternatively, what EP could I use instead that does work with MSAL, and contains the same functionality as this one?
Note: looking at the headers in the request to get the tokens in the UI, I see they are using msal.js.browser, so this should be possible in theory. (by the way, the requested scope in the UI is [client_id]/.defualt openid profile offline_access) to the EP https://login.microsoftonline.com/common/oauth2/v2.0/token). When trying to decode the UI token in jwt.ms it says that the token is issued by AAD.
Example of a concrete EP I am trying to access: https://admin.powerplatform.microsoft.com/api/Environments/{env_name}/roleassignments/environmentadmin. The API is taken from the Power Platform Admin Center. More info about it here.
Related
I have built a python application to access read only Power BI Rest API’s. I am automating the collection of tenant activity. However despite configuring my Azure App and using the service principal to generate an access token, the response I receive from the API request is one of an unauthorised response:
{"error": {"code": "PowerBINotAuthorizedException", "pbi.error": {"code":
"PowerBINotAuthorizedException", "parameters": {}, "details": [], "exceptionCulprit": 1}}}
I have found a number of similar issues posted online, however feel that I have done everything that is suggested but am still not able to get it working. I would appreciate any guidance.
The steps that I have taken are:
Configured an Azure App, adding the Application Permission for Power Bi Service-Tenant.Read.All
Screenshot of App Settings in Azure Portal
Requested my access token based upon the Client Credentials Flow using my app's client_ID and client_Secret as documented in the below link:
https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow
I successfully receive a token using the script below:
import requests
azureTenantID = "xxxxxxxxxxxxxxxxx"
azureClientId = "xxxxxxxxxxxxxxxxx"
azureClientSecret = "xxxxxxxxxxxxxxxxxx"
url = f"https://login.microsoftonline.com/{azureTenantID}/oauth2/v2.0/token"
payload = {
"grant_type": "client_credentials",
"client_id": azureClientId,
"client_secret": azureClientSecret,
"scope": "https://analysis.windows.net/powerbi/api/.default"
}
# Header HAS to be x-www-form-urlencoded for MS to accept it.
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
# Return POST content as JSON.
r = requests.post(url, data=payload, headers=headers).json()
# Grab the access token.
response = r.get("access_token")
# Concatenate with Bearer string
access_token = "Bearer {r['access_token']}"
Configured my Power BI Tenant Settings to enable Service Principals to use API's.
Screenshot of Admin API Setting
Screenshot of Developer API Setting
Note that I added the Service Principal as a member of the Security Group for which both of these settings are enabled
Execute my Get request to the API
The followings script returns a good response when I take an access token from the Power BI REST API Documentation's 'Try it out' feature, but not when I generate the token as above.
import requests
# Initialise parameters.
url = "https://api.powerbi.com/v1.0/myorg/admin/groups?$top=1000&$expand=datasets,dataflows,reports,users,dashboards"
headers = {'Authorization': get_access_token2()}
# Get response.
response = requests.get(url, headers=headers)
response = response.json()
Any assistance would be appreciated !
I just went through this exact scenario that you described, and in the end we had to engage Microsoft support to solve it.
Although extremely counter intuitive, if the app that you create for your service principal authentication has any Power BI permissions assigned to it then the access token that is generated (when passed to Power BI REST Admin API) will return an error response that reports PowerBINotAuthorizedException.
To be even more specific, if the access token that you pass to the Power BI API has a roles key/value pair, then you will get a PowerBINotAuthorizedException.
In your case, the issue is easier because you have listed out what permissions you granted. You mentioned that you Configured an Azure App, adding the Application Permission for Power Bi Service-Tenant.Read.All. In order to resolve this issue, you will need to remove that permission.
For future readers, you can troubleshoot this by decoding your access token using a JWT token decoder like one found at jstoolset.com. If your app has permissions allocated to the scope that you have requested (https://analysis.windows.net/powerbi/api/.default is the typical Power BI scope that you request in your authorization) and you decode your JWT token then you will see a roles key/value pair. The presence of this roles is essentially the issue. It does not matter that the values there might match up to the Required Scope in the Power BI REST Admin API documentation. It was described to us as if there is a roles value in your access token then when the token is presented to the Power BI API the roles that are granted are attempted to be used, which ultimately results in a PowerBINotAuthorizedException because service principals are not allowed to use a certain role.
If you have an app that you have removed all permissions from, but still has a value coming through in your access token for the roles key/value pair, then I would suggest starting with a new app with no permissions allocated to it, and simply add the new app to the existing security group that you originally created. This is how we realized that this truly was the issue, and were then able to reconcile from there.
EDIT: Microsoft has now updated their API documentation on the relevant endpoints to reflect this information. For example, in Admin - Groups GetGroupUsersAsAdmin the Required Scope now reads:
Tenant.Read.All or Tenant.ReadWrite.All
Relevant only when authenticating via a standard delegated admin access token. Must not be present when authentication via a service principal is used.
This is the code I use to retrieve idToken in my flutter app
Future<void> _handleSignIn() async {
try {
final result = await _googleSignIn.signIn();
final ggAuth = await result!.authentication;
print(ggAuth.idToken); // this is the one that I use as token value
print(ggAuth.accessToken);
} catch (error) {
print(error);
}
}
And this is the code I use to access user info in my backend.
from google.oauth2 import id_token
from google.auth.transport import requests
try:
idinfo = id_token.verify_oauth2_token(token, requests.Request(), CLIENT_ID)
userid = idinfo['sub']
print(userid)
except ValueError:
print('Invalid token)
But after replacing the token variable with the token received from flutter app and replacing the CLIENT_ID with the id generated for my app (available in the console) , it still throws the error of invalid token. What am I doing wrong?
EDIT- When I use https://jwt.io/ to decode the token it works as expected and I get all the details.
Authentication Think of the id token as your birth certificate. It just identifies you as a person.
Authorization Think of the access token as your drivers license its what contains the permissions that you have to drive a car.
An Id token does not out write give you permission to access any data on google servers. With an access token you have been authorized to access some data in this case some profile information.
The issue you are having is that you are using sign-in in. Sign in is Open id connect think of it as a birth certificate (Id token). There is a user their who is signing in to your app. They are in fount of their computer. By default with Google signin gives you get basic profile information. What you are doing with Jwt.io is checking the claims that are returned in the Id token. Id tokens are just the token that allows your application to identify that the user is logged in. There should also be a sub claim there you should use this id to map the user to your integral user system. There may be a user name and email address in the claims but you can not guarantee that they will be there every time google has stated they do not always return these claims.
To get user profile information after the user has logged in you should be using the people api the people.get method when passed the access token will return to you the profile information of the currently authenticated user.
beyond that you should IMO not be passing tokens to your backend if you want the token in the backend you should log the user in using your backend language.
Okay so the idToken that I had was correct but flutter terminal was not able to print its full length and because of which I was copying only half the token and was getting a signature error when checking manually.
I created 2 applications in my Azure directory, 1 for my API Server and one for my API client. I am using the Python ADAL Library and can successfully obtain a token using the following code:
tenant_id = "abc123-abc123-abc123"
context = adal.AuthenticationContext('https://login.microsoftonline.com/' + tenant_id)
token = context.acquire_token_with_username_password(
'https://myapiserver.azurewebsites.net/',
'myuser',
'mypassword',
'my_apiclient_client_id'
)
I then try to send a request to my API app using the following method but keep getting 'unauthorized':
at = token['accessToken']
id_token = "Bearer {0}".format(at)
response = requests.get('https://myapiserver.azurewebsites.net/', headers={"Authorization": id_token})
I am able to successfully login using myuser/mypass from the loginurl. I have also given the client app access to the server app in Azure AD.
Although the question was posted a long time ago, I'll try to provide an answer. I stumbled across the question because we had the exact same problem here. We could successfully obtain a token with the adal library but then we were not able to access the resource I obtained the token for.
To make things worse, we sat up a simple console app in .Net, used the exact same parameters, and it was working. We could also copy the token obtained through the .Net app and use it in our Python request and it worked (this one is kind of obvious, but made us confident that the problem was not related to how I assemble the request).
The source of the problem was in the end in the oauth2_client of the adal python package. When I compared the actual HTTP requests sent by the .Net and the python app, a subtle difference was that the python app sent a POST request explicitly asking for api-version=1.0.
POST https://login.microsoftonline.com/common//oauth2/token?api-version=1.0
Once I changed the following line in oauth2_client.py in the adal library, I could access my resource.
Changed
return urlparse('{}?{}'.format(self._token_endpoint, urlencode(parameters)))
in the method _create_token_url, to
return urlparse(self._token_endpoint)
We are working on a pull request to patch the library in github.
For the current release of Azure Python SDK, it support authentication with a service principal. It does not support authentication using an ADAL library yet. Maybe it will in future releases.
See https://azure-sdk-for-python.readthedocs.io/en/latest/resourcemanagement.html#authentication for details.
See also Azure Active Directory Authentication Libraries for the platforms ADAL is available on.
#Derek,
Could you set your Issue URL on Azure Portal? If I set the wrong Issue URL, I could get the same error with you. It seems that your code is right.
Base on my experience, you need add your application into Azure AD and get a client ID.(I am sure you have done this.) And then you can get the tenant ID and input into Issue URL textbox on Azure portal.
NOTE:
On old portal(manage.windowsazure.com),in the bottom command bar, click View Endpoints, and then copy the Federation Metadata Document URL and download that document or navigate to it in a browser.
Within the root EntityDescriptor element, there should be an entityID attribute of the form https://sts.windows.net/ followed by a GUID specific to your tenant (called a "tenant ID"). Copy this value - it will serve as your Issuer URL. You will configure your application to use this later.
My demo is as following:
import adal
import requests
TenantURL='https://login.microsoftonline.com/*******'
context = adal.AuthenticationContext(TenantURL)
RESOURCE = 'http://wi****.azurewebsites.net'
ClientID='****'
ClientSect='7****'
token_response = context.acquire_token_with_client_credentials(
RESOURCE,
ClientID,
ClientSect
)
access_token = token_response.get('accessToken')
print(access_token)
id_token = "Bearer {0}".format(access_token)
response = requests.get(RESOURCE, headers={"Authorization": id_token})
print(response)
Please try to modified it. Any updates, please let me know.
I am trying to implement token based authentication for my Flask REST API. I am using Stormpath as my third-party authentication service.
I looked into flask-stormpath built on top of flask-login. Looks like it uses password based authentication as they are trying to maintain session on the server. Also, the documentation doesn't provide me enough information.
Do we have a flask integration for stormpath token based authentication ?
If yes, can someone point me to a sample code.
I have already gone through the stormpath/flask-stormpath-sample on github, which again maintains sessions in server.
References:
https://stormpath.com,
https://github.com/stormpath/stormpath-flask
So here is the way I am currently using until rdegges shall build this feature into flask-stormpath.
You will need stormpath python sdk latest version and wraps from func tools.
from stormpath.api_auth import (PasswordGrantAuthenticator, RefreshGrantAuthenticator, JwtAuthenticator)
from functools import wraps
You can create your application as such.
stormpathClient = Client(id=KEYS['STORMPATH_ID'], secret=KEYS['STORMPATH_SECRET'])
stormpathApp = stormpathClient.applications.search('your-application')[0]
This decorator shall help you with securing endpoints.
def tokenRequired(func):
"""
Decorator to apply on all routes which require tokens.
"""
#wraps(func)
def wrappingFunc():
#check the auth header of the request for a bearer token.
authHeader = request.headers.get('Authentication')
#make sure that the string is a bearer type.
if len(authHeader)<8 or (not authHeader[:7] == 'Bearer ') or (
not authHeader):
return Response("401 Unauthorized",401)
authToken = authHeader[7:]
try:
authenticator = JwtAuthenticator(stormpathApp)
authResult = authenticator.authenticate(authToken)
request.vUser = authResult.account
except:
return Response("403 Forbidden",403)
return func()
return wrappingFunc
#Use this decorator like below.
#flaskApp.route('/secure-route',methods=['GET','POST'])
#tokenRequired
def secureEndpoint():
# return JSON based response
return Response("This is secure Mr." + request.vUser.given_name ,200)
Let me know in the comments if someone wishes to know the token issuing and refreshing end points as well.
I'm the author of the Flask-Stormpath library. The answer is no. I'm actually working on a new release of the library (coming out in a month or so) that will provide this functionality by default, but right now it only supports session based authentication.
I'm trying to use the Google Docs API with Python+Django and OAuth 2. I've got the OAuth access token, etc. via google-api-python-client, with the code essentially copied from http://code.google.com/p/google-api-python-client/source/browse/samples/django_sample/plus/views.py
Now, I assume I should be using the google gdata API, v 2.0.17. If so, I'm unable to find exactly how to authorize queries made using the gdata client. The docs at http://packages.python.org/gdata/docs/auth.html#upgrading-to-an-access-token (which appear outdated anyway), say to set the auth_token attribute on the client to an instance of gdata.oauth.OAuthToken. If that's the case, what parameters should I pass to OAuthToken?
In short, I'm looking for a brief example on how to authorize queries made using the gdata API, given an OAuth access token.
The OAuth 2.0 sequence is something like the following (given suitably defined application constants for your registered app).
Generate the request token.
token = gdata.gauth.OAuth2Token(client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
scope=" ".join(SCOPES),
user_agent=USER_AGENT)
Authorise the request token. For a simple command-line app, you can do something like:
print 'Visit the following URL in your browser to authorise this app:'
print str(token.generate_authorize_url(redirect_url=REDIRECT_URI))
print 'After agreeing to authorise the app, copy the verification code from the browser.'
access_code = raw_input('Please enter the verification code: ')
Get the access token.
token.get_access_token(access_code)
Create a gdata client.
client = gdata.docs.client.DocsClient(source=APP_NAME)
Authorize the client.
client = token.authorize(client)
You can save the access token for later use (and so avoid having to do the manual auth step until the token expires again) by doing:
f = open(tokenfile, 'w')
blob = gdata.gauth.token_to_blob(token)
f.write(blob)
f.close()
The next time you start, you can reuse the saved token by doing:
f = open(tokenfile, 'r')
blob = f.read()
f.close()
if blob:
token = gdata.gauth.token_from_blob(blob)
Then, the only change to the authentication sequence is that you pass this token to OAuth2Token by specifying a refresh_token argument:
token = gdata.gauth.OAuth2Token(client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
scope=" ".join(SCOPES),
user_agent=USER_AGENT,
refresh_token=token.refresh_token)
Hope this helps. It took a while to work it out :-).
This is from https://developers.google.com/gdata/docs/auth/overview:
Warning: Most newer Google APIs are not Google Data APIs. The Google Data APIs documentation applies only to the older APIs that are listed in the Google Data APIs directory. For information about a specific new API, see that API's documentation. For information about authorizing requests with a newer API, see Google Accounts Authentication and Authorization.
You should either use OAuth for both authorization and access or OAuth 2.0 for both.
For OAuth 2.0 API are now at https://developers.google.com/gdata/docs/directory.