Failed to use auth code flow with python MSAL - python

I simply can't get acquire_token_by_auth_code_flow() from the MSAL package to work outside a flask app using the basic example giving in the MSAL documentation.
I think the problem comes from using the wrong authentication response which must be a "dict of the query string received from auth server" according to the documentation. In a flask app, I can simply use request.args which I'm not quite sure how to use outside of flask.
I've already tried using requests.request as well as urlsplit. The device flow is working fine as well as using the MSAL package in Java and connecting via R. So the app seems to be set up correctly.
The basic example code from the MSAL app below produces the error:
state mismatch: XXXXXXXXXXXX vs None
(so auth_response is wrong).
Any thoughts?
import requests
import msal
CLIENT_ID = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" # Application (client) ID of app registration
CLIENT_SECRET = "XX-XXXXXXXX-XXXXXXXX.XX~XXXXX~XXXX" # Placeholder - for use ONLY during testing.
AUTHORITY = "https://login.microsoftonline.com/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX"
REDIRECT_PATH = "/getAToken" # Used for forming an absolute URL to your redirect URI.
# The absolute URL must match the redirect URI you set
# in the app's registration in the Azure portal.
ENDPOINT = 'https://graph.microsoft.com/v1.0/me'
SCOPE = ["https://graph.microsoft.com/.default"]
# Cache
cache = msal.SerializableTokenCache()
# Build msal app
app = msal.ConfidentialClientApplication(
CLIENT_ID, authority=AUTHORITY,
client_credential=CLIENT_SECRET, token_cache=cache)
# Initiate auth code flow
session = requests.Session()
session.flow = app.initiate_auth_code_flow(scopes=SCOPE, redirect_uri=REDIRECT_PATH)
# Aquire token
result = app.acquire_token_by_auth_code_flow(auth_code_flow=session.flow, auth_response = dict(parse.parse_qsl(parse.urlsplit(REDIRECT_PATH).query)))
The equivalent code for the last bit from the flask app looks like this with REDIRECT_PATH = "/getAToken":
#app.route(app_config.REDIRECT_PATH) # Its absolute URL must match your app's redirect_uri set in AAD
def authorized():
result = _build_msal_app(cache=cache).acquire_token_by_auth_code_flow(
session.get("flow", {}), request.args)
return redirect(url_for("index"))

Getting a token requires few requests according to documentation. To make it possible you need to create flow and store it inside session before navigating to microsoft login page.
session["flow"] = _build_auth_code_flow(authority=app_config.AUTHORITY, scopes=app_config.SCOPE)
After navigation back to your application you should use this flow object as you did in your example
result = _build_msal_app(cache=cache).acquire_token_by_auth_code_flow(
session.get("flow", {}), request.args)
Make sure that you didn't create it twice. In this case error will be similar, but state mismatch: XXXXXXXXXXXX vs XXXXXXXXXXXX. It may happened if you route called twice.

auth_response must be a dictionary built from the current HTTP request query params.
If this is a desktop application you must switch to PublicClientApplication. You can find a sample here.

Related

Bigquery - google auth does not direct to url

I'm trying to run a query on bigquery in a Django project and get results. While working successfully in localhost, it does not redirect to the verification link at all when I take it to the live server.
I think I need to change the redirect_uri value as I read it. I added this in Da appflow variable but the url doesn't change. I am using the same query below with the example query in google's document, I am submitting my own query because it contains private information, but it is exactly the same query.
I have added to Authorized redirect URIs, and I put the api in production mode.;
The resulting redirect url is output as localhost in this way;
https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=123-nml31ekr2n0didomei5.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2F&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fbigquery&state=XF1WdtCoR4HaICwzSKk9A1giBrSzBv&access_type=offline
def query_stackoverflow():
launch_browser = True
project = 'xx-prod'
appflow = flow.InstalledAppFlow.from_client_secrets_file("static/client_secret_518684-nmpoqtgo5flvcgnl31ekr2ni5.apps.googleusercontent.com.json", scopes=["https://www.googleapis.com/auth/bigquery"], redirect_uri=["https://xx.com/"])
if launch_browser:
appflow.run_local_server()
else:
appflow.run_console()
credentials = appflow.credentials
client = bigquery.Client(project=project, credentials=credentials)
client = bigquery.Client()
query_job = client.query(
"""
SELECT
CONCAT(
'https://stackoverflow.com/questions/',
CAST(id as STRING)) as url,
view_count
FROM `bigquery-public-data.stackoverflow.posts_questions`
WHERE tags like '%google-bigquery%'
ORDER BY view_count DESC
LIMIT 10"""
)
results = query_job.result() # Waits for job to complete.
for row in results:
print("{} : {} views".format(row.url, row.view_count))
On live server google return auth url like this;
Please visit this URL to authorize this application: https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=51864584-nmpoqtgo5flvcgnln0didomei5.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2F&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fbigquery&state=W2uMZwzaYMEpFzExodRCf2wA4&access_type=offline
The first problem is that it does not automatically redirect to the link as in localhost, the second problem is that when I open this link manually, the link cannot be reached after mail verification.
From what i can see your code is using installed app flow. This means that the consent screen is going to open up on the machine its running on. If you have this running on a server, are you logging into the server and running it or are you in fact creating a web application?
flow.InstalledAppFlow
web app
If you are making a web application then you should be following this sample.
API access on behalf of your clients (web flow)
You will need to convert it to work with big query.
import google.oauth2.credentials
import google_auth_oauthlib.flow
# Initialize the flow using the client ID and secret downloaded earlier.
# Note: You can use the GetAPIScope helper function to retrieve the
# appropriate scope for AdWords or Ad Manager.
flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
'client_secret.json',
scope=[oauth2.GetAPIScope('adwords')])
# Indicate where the API server will redirect the user after the user completes
# the authorization flow. The redirect URI is required.
flow.redirect_uri = 'https://www.example.com/oauth2callback'
The code for a web application is slightly different then that of an installed application.

Authenticate to Azure Functions App using Azure Active Directory in Python daemon application with MSAL

I built a REST API by creating an Azure Functions App and a Python console application should be able to authenticate and make requests to the API.
Here are the steps I have taken so far:
I created the REST API as an Azure Functions App (in C#) where I used AuthorizationLevel.Anonymous.
In the Azure Active Directory I created an app registration (Azure Active Directory > App registrations) where I added a client secret under Certificates & secrets > Client secrets.
In the Azure Functions App I added the registration from step 2 under Authentication > Identity provider where I provided the registration's client ID as well as the value of the registration's client secret.
Here is the code of the Python console application (as described here: https://github.com/AzureAD/microsoft-authentication-library-for-python):
import requests
import msal
import json
with open('configuration.json') as json_file:
configuration = json.load(json_file)
app = msal.ConfidentialClientApplication(
configuration["client_id"],
authority=configuration["authority"],
client_credential=configuration["secret"]
)
result = None
result = app.acquire_token_silent(configuration["scope"],account=None)
if not result:
result = app.acquire_token_for_client(scopes=configuration["scope"])
if "access_token" in result:
url = ... # the URL of a specific function of the Azure Functions App
parameters = {...}
response = requests.get(url=url, params=parameters, headers={'Authorization': 'Bearer ' + result['access_token']})
print("Status code: {}".format(response.status_code))
print("Message: {}".format(response.text))
registration["scope"] equals ["https://graph.microsoft.com/.default"] and registration["secret"] equals the value of the client secret created in step 2.
The code returns:
Status code: 401
Message: You do not have permission to view this directory or page.
What am I missing? I know there are similar issues on Stackoverflow but they did not solve my problem.
If you use the newest authentication, please check the issue url:
Check your Issuer URL:
Or to change the accessTokenAcceptedVersion to 2:
Any way, make sure the issue url version is the same.
(I found there are still many problems with Authentication, and many things will not be automatically configured.)

How to obtain the caller's IP address in Flask/Connexion?

I'm using an OpenAPI 3.0 specification (swagger.yml) and use Swagger Codegen to create the corresponding Python Flask application stubs. This is how I run the application to expose my Swagger API:
app = connexion.App(__name__, specification_dir='./swagger/')
app.app.json_encoder = encoder.JSONEncoder
app.add_api('swagger.yaml', arguments={'title': 'My Test API'})
# add CORS support to send Access-Control-Allow-Origin header
CORS(app.app)
So far so good. The application logic is handled within the generated Python stubs which are linked by the x-openapi-router-controller: swagger_server.controllers.user_controller.
I now however need to access HTTP Request specific information within the application itself to for example react differently based on the HTTP_CLIENT_IP address
How can I obtain that information within my controller endpoint?
Use Flask's request context.
For example, to get the HTTP_CLIENT_IP, use:
from flask import request
http_client_ip = request.remote_addr
You can read more about request here.
Attached two related links addressing the same issue on request header parameters and how connexion does not forward them to custom controllers. I ended up manually accessing them via
access_token = connexion.request.headers['access_token']

python3 upload files to ondrive or sharepoint?

Anyone know if this is possible?
I just want to automate dropping some documents into my onedrive for business account.
I tried
import onedrivesdk
from onedrivesdk.helpers import GetAuthCodeServer
from onedrivesdk.helpers.resource_discovery import ResourceDiscoveryRequest
redirect_uri = 'http://localhost:8080'
client_id = 'appid'
client_secret = 'mysecret'
discovery_uri = 'https://api.office.com/discovery/'
auth_server_url='https://login.live.com/oauth20_authorize.srf?scope=wl.skydrive_update'
#auth_server_url='https://login.microsoftonline.com/common/oauth2/authorize',
auth_token_url='https://login.microsoftonline.com/common/oauth2/token'
http = onedrivesdk.HttpProvider()
auth = onedrivesdk.AuthProvider(http,
client_id,
auth_server_url=auth_server_url,
auth_token_url=auth_token_url)
auth_url = auth.get_auth_url(redirect_uri)
code = GetAuthCodeServer.get_auth_code(auth_url, redirect_uri)
auth.authenticate(code, redirect_uri, client_secret, resource=resource)
# If you have access to more than one service, you'll need to decide
# which ServiceInfo to use instead of just using the first one, as below.
service_info = ResourceDiscoveryRequest().get_service_info(auth.access_token)[0]
auth.redeem_refresh_token(service_info.service_resource_id)
client = onedrivesdk.OneDriveClient(service_info.service_resource_id + '/_api/v2.0/', auth, http)
I registered an APP and got a secret and id. But when I ran this I got scope is invalid errors. Plus it tries to launch a webpage which isn't great for a command line kinda environment. I think this SDK might be outdated as well because originally this script had login.microsoftonline, but that wasn't reachable so I changed it to login.live.com.
I wrote this sample code you posted. You replaced the auth_server_URLwith the authentication URL for Microsoft Account authentication, which can only be used to access OneDrive (the consumer product). You need to continue using the login.microsoftonline.com URL to log into your OneDrive for Business account.
You are correct that this pops up a dialog. However, you can write a little supporting code so that only happens the first time you log into a particular app. Follow these steps (assuming you are using the default implementation of AuthProvider:
Use the sample code above up through the line auth.redeem_refresh_token()
The AuthProvider will now have a Session object, which caches the credentials of the current user and session. Use AuthProvider.save_session() to save the credentials for later.
Next time you start your app, use AuthProvider.load_session() and AuthProvider.refresh_token() to retrieve the previous session and refresh the auth token. This will all be headless.
Take note that the default implementation of SessionBase (found here) uses Pickle and is not safe for product use. Make sure to create a new implementation of Session if you intend to deploy this app to other users.
Onerive's website shows "Not Yet" on "OneDrive SDK for Python" to "OneDrive for Business"
https://dev.onedrive.com/SDKs.htm
The github sample codes did not work for me either, it tried to popup a window of authentication, but IE can not find the address:
http://('https//login.microsoftonline.com/common/oauth2/authorize',)?redirect_uri=http%3A%2F%2Flocalhost%3A8080&client_id=034xxxx9-9xx8-4xxf-bexx-1bc5xxxxbd0c&response_type=code
or removed all the "-" in client id
http://('https//login.microsoftonline.com/common/oauth2/authorize',)?redirect_uri=http%3A%2F%2Flocalhost%3A8080&client_id=034xxxx99xx84xxfbexx1bc5xxxxbd0c&response_type=code
Either way, I got the same result, IE did not show the popup with a line "This page can’t be displayed"

Getting user info with Cloud Endpoints (using other API Endpoints)

I'm trying to setup endpoints api (with google app engine, python), but I'm having some trouble getting user profile info. API is working, I can create entities through API Explorer on my localhost.
My goal is to allow user to register for my app by providing just an email, and authorizing the app to get the reset of the info from their profile. I have this endpoints method:
#User.method(http_method="POST",
auth_level=endpoints.AUTH_LEVEL.REQUIRED,
allowed_client_ids=[
endpoints.API_EXPLORER_CLIENT_ID
],
scopes=[
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/plus.me',
],
user_required=True,
request_fields=('email',),
response_fields=('id',),
name="register",
path="users")
def UserRegister(self, instance):
logging.info(os.getenv( 'HTTP_AUTHORIZATION' ))
# 'Beared __TOKEN__'
logging.info(endpoints.users_id_token._get_token(None))
# '__TOKEN__'
instance.put()
return instance
This works fine, I receive authorization token and user is created in datastore, but I can't figure out how to get the profile info. If I enter the token in OAuth2 API (through API Explorer):
POST https://www.googleapis.com/oauth2/v2/tokeninfo?access_token=__TOKEN__
I get token info with some data I need { "user_id": "__ID__", "verified_email": true, ...}, and if I use user_id in +API:
GET https://www.googleapis.com/plus/v1/people/__ID__
I can get the rest of the data I need (name, image, etc).
What do I need to do to achieve this in my UserRegister() method? I'd prefer to return just entity ID and do the rest of registration asynchronously, but that's another issue, I'll figure it out (; Just need some guidance how to call other endpoints from my code...
EDIT:
I've managed to figure out how to call other APIs (code on Gist), now only have one issue with Plus API:
I did some queries and eventually got anonymous quota error. Then I added key parameter and set it to WEB_CLIENT_ID or SERVICE_ACCOUNT:
WEB_CLIENT_ID is OAuth2 Client ID (type: Web Application) from console.developers.google.com/apis/credentials,
SERVICE_ACCOUNT is default App Engine service account - MY_APP#appspot.gserviceaccount.com...
and now I'm getting following error:
HttpError: <HttpError 400 when requesting https://www.googleapis.com/plus/v1/people/__VALID_USER_ID__?key=__WEB_CLIENT_ID__or__SERVICE_ACCOUNT__&alt=json returned "Bad Request">
When I use +API explorer I get results as expected:
REQUEST:
https://www.googleapis.com/plus/v1/people/__VALID_USER_ID__?key={YOUR_API_KEY}
RESPONSE:
200 OK + json data for user...
Anyone knows why is this happening?
Why am I getting BadRequest response?
Problem with BadRequest was that I didn't send authorization token... I did try to send it as access_token, but seams like +api docs are outdated - it should be oauth_token. When I included this parameter issue was resolved:
build('plus', 'v1').people().get(userId=user_id, key=SERVICE_ACCOUNT, oauth_token=token).execute()
HINT: Use http://localhost:8001/_ah/api/discovery/v1/apis/, and discoveryRestUrl property it has to see real properties of your API - this is where I found the answer.
oauth_token can be obtained like this:
token = os.getenv('HTTP_AUTHORIZATION').split(" ")[1]
# or like in my question:
token = endpoints.users_id_token._get_token(None)
I'd suggest HTTP_AUTHORIZATION variable, because users_id_token docs state that it's a:
Utility library for reading user information from an id_token.
This is an experimental library that can temporarily be used to extract
a user from an id_token. The functionality provided by this library
will be provided elsewhere in the future.
How to call other API Endpoints?
This is also an answer to my first question:
from googleapiclient.discovery import build
service = build('plus', 'v1')
request = service.people().get(userId=user_id, key=SERVICE_ACCOUNT, oauth_token=token)
response = request.execute()
data = dict(self.response.POST)
Code that worked for me is here.
NOTE: WEB_CLIENT_ID obtained from https://console.developers.google.com/apis/credentials (OAuth2 Client ID of type Web Application) will NOT work in this case. I had to use SERVICE_ACCOUNT - I didn't try to generate one through console, default service account I got from App Engine worked fine.
...things are much clearer now that I got this working. Hope it will help someone else (;

Categories