Google backend oAuth with Python3? - python-3.x

I query Google Calendar a program written in Python 2.7, in server to server mode (using a certificate). Typically like this:
with open('mycertfromgoogleconsole.p12', 'rb') as f:
private_key = f.read()
credentials = oauth2client.client.SignedJwtAssertionCredentials(
'something#developer.gserviceaccount.com,
private_key,
'https://www.googleapis.com/auth/calendar.readonly',
sub='theaccounttoimpersonate#example.com'
)
http_auth = credentials.authorize(httplib2.Http())
service = apiclient.discovery.build('calendar', 'v3', http=http_auth)
I now need to port the script to Python 3.4 but cannot find a library which would support the backend version of oAuth (there are several which seem to support the web version one). Would you know is there is one available? (ideally a drop-in replacement but that would be close to miraculous)

I finally managed to make it work, the code below follows the Google API documentation and the last request yields the token data mentioned in "Handling the response" in the docs above
import requests
import jwt
import arrow
import OpenSSL.crypto
class GetToken(object):
"""
Get Google API token as documented at https://developers.google.com/identity/protocols/OAuth2ServiceAccount
"""
def __init__(self):
self.expires = 0
self.token = None
def gettoken(self):
now = arrow.now().timestamp
# is the token still valid and exists ? (within a 10 seconds margin)
if now < (self.expires - 10) and self.token:
return self.token
self.expires = now + 3600
claim = {
"iss": "dev_account_from_google_console#developer.gserviceaccount.com",
"scope": "https://www.googleapis.com/auth/calendar.readonly",
"aud": "https://www.googleapis.com/oauth2/v3/token",
"exp": now,
"iat": now,
"sub": "account_with_delegated_rights#example.com"
}
p12 = OpenSSL.crypto.load_pkcs12(open('certificate_from_google_api_console.p12', 'rb').read(), 'notasecret')
private_key = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, p12.get_privatekey()).decode('utf-8')
myjwt = jwt.encode(claim, private_key, algorithm='RS256', headers={"alg": "RS256", "typ": "JWT"}).decode('utf-8')
data = {
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": myjwt
}
headers = {"Content-Type": "application/x-www-form-urlencoded"}
r = requests.post("https://www.googleapis.com/oauth2/v3/token", data=data, headers=headers)
print(r.json())
if __name__ == "__main__":
t = GetToken()
t.gettoken()

Related

Unable to use accessToken instead of idToken - msal python

I am validating my Bearer token through JWT in Python and it was earlier written in a way to handle idTokens only. We just moved to the new auth code flow pattern on the UI and they advice me to use accessToken instead of idToken for token validation. The app works great E2E if I use idToken, however when I use accessToken in the Bearer auth, the validation fails. I get a 401 unauthorized.
Please advice.
Here is my python code:
import os
import sys
import requests
import time
import calendar
from functools import wraps
from jose import jwk, jwt, JWTError
from flask import abort, current_app, request
from src.database import db
from src.secrets import derive_base64_secret
from src.models.user import User, UserRoleEnum
CLIENT_ID = os.environ.get("AZURE_AD_CLIENT_ID")
TENANT_ID = os.environ.get("AZURE_AD_TENANT_ID")
AUTHORITY_MSAL = f"https://login.microsoftonline.com/{TENANT_ID}/v2.0"
OIDC = requests.get(f"{AUTHORITY_MSAL}/.well-known/openid-configuration").json()
MSAL_JWKS = requests.get(OIDC["jwks_uri"]).json()
S2S_JWK = {"alg": "HS256", "kty": "oct", "k": derive_base64_secret("S2S JWT", 32)}
def validate_msal_token(token):
try:
return jwt.decode(token, MSAL_JWKS, audience=CLIENT_ID, issuer=[AUTHORITY_MSAL])
except JWTError:
abort(401)
def validate_s2s_token(token):
try:
return jwt.decode(token, S2S_JWK, audience=CLIENT_ID, issuer=CLIENT_ID)
except JWTError:
abort(401)
def create_s2s_token(ttl, additional_claims):
now = calendar.timegm(time.gmtime())
claims = {
"iss": CLIENT_ID,
"aud": CLIENT_ID,
"iat": now,
"exp": now + ttl,
**additional_claims,
}
return jwt.encode(claims, S2S_JWK)
def get_access_token():
authorization = request.headers.get("Authorization")
if isinstance(authorization, str) and authorization.startswith("Bearer "):
return authorization[7:]
return request.args.get("access_token")
def load_user():
access_token = get_access_token()
if not access_token:
abort(401)
token = validate_msal_token(access_token)
if not token:
abort(401)
oid = token.get("oid", None)
if not oid:
abort(403)
user = User.query.filter_by(OID=oid).first()
if not user:
# Auto add the first user to access the API as an Admin IN DEVELOPMENT ONLY
if current_app.env == 'development' and User.query.count() == 0:
user = User(OID=oid, Display_Name="Unknown", User_Name="unknown#example.com", Role=0)
db.session.add(user)
else:
abort(403)
display_name = token.get("name", None)
if display_name and user.Display_Name != display_name:
user.Display_Name = display_name
# `preferred_username` is supplied on a v2 id token as provided by the FM
# `unique_name` is supplied on a v1 id token as provided by the MDA
user_name = token.get("preferred_username", token.get("unique_name", None))
if user_name and user.User_Name != user_name:
user.User_Name = user_name
db.session.commit()
request.token = token
request.user = user
return user
def jwt_required(fn):
#wraps(fn)
def wrapper(*args, **kwargs):
access_token = get_access_token()
if not access_token:
abort(401)
token = validate_s2s_token(access_token)
if not token:
abort(401)
request.token = token
return fn(*args, **kwargs)
return wrapper
def user_required(fn):
#wraps(fn)
def wrapper(*args, **kwargs):
load_user()
return fn(*args, **kwargs)
return wrapper
Thank you for sharing your code. You should receive a response from the bearer auth and be granted access. It appears that your credentials are insufficient for successful authentication resulting in a 401 unauthorized. Based on the situation, perform a secondary check on the cause of failure. Token validation is not required for all apps. Apps should only validate a token in the following circumstances listed here. APIs and web apps can only validate tokens with an aud claim that matches their app; other resources may have their own token validation requirements. Learn more about managing access tokens here.

Exception has occurred: JSONDecodeError App Store Connect Financial API using Python

I am trying to get financial data from my APP store connect account and am receiving a JSON error, see below:
import requests, time, json
from authlib.jose import jwt
KEY_ID = "KEY"
ISSUER_ID = "ID"
EXPIRATION_TIME = int(round(time.time() + (20.0 * 60.0))) # 20 minutes timestamp
PATH_TO_KEY = "C:\\Users\\justi\\Desktop\\Script Files\\App Store Connect\\private_key\\AuthKey_KEY.p8"
with open(PATH_TO_KEY, 'r') as f:
PRIVATE_KEY = f.read()
header = {
"alg": "ES256",
"kid": KEY_ID,
"typ": "JWT"
}
payload = {
"iss": ISSUER_ID,
"exp": EXPIRATION_TIME,
"aud": "appstoreconnect-v1"
}
# Create the JWT
token = jwt.encode(header, payload, PRIVATE_KEY)
# URL Query Params
report_type = "filter[reportType]=FINANCE_DETAIL"
region_code = "filter[regionCode]=Z1"
report_date = "filter[reportDate]=2021-03"
vendor_number = "filter[vendorNumber]=VEND_###"
# API Request
JWT = 'Bearer ' + token.decode()
URL = 'https://api.appstoreconnect.apple.com/v1/financeReports?'+ report_type +'&' +region_code+ '&' + report_date + '&' +vendor_number
HEAD = {'Authorization': JWT}
r = requests.get(URL, params={'limit': 200}, headers=HEAD)
# Write the response in a pretty printed JSON file
with open('financial_report.json', 'w') as out:
out.write(json.dumps(r.json(), indent=4))
I am getting a 200 response from my r object but when I go to dump the JSON data, here is my error:
https://www.screencast.com/t/ODMojS13WH
Has any one seen this before?
Is the response not a JSON object (which would be strange)?
Thank you in advance!
check if r.ok is True
check if r.json() returns json string. Use print statement.
Also r.text may be enough to write json to output file.
Example :
r = requests.get(URL, params={'limit': 200}, headers=HEAD)
print(r.json())
print(r.text())
Finance Reports Endpoint response content type is application/a-gzip, not application/json, so as you have commented, you have to decompress the binary content yourself.
However, there is a drawback in your code:
decompressed_data=zlib.decompress(r.content, 16+zlib.MAX_WBITS)
response.content triggers python-requests to load the content at once into memory, it could consume large amount of memory for large gzip responses.
Use response.raw file-like object and decompress data incrementally is more preferable. You can find example code here.
Or you can use applaud directly, it is a Python client library for accessing App Store Connect API. Your code can be shortened by using applaud:
import os
from applaud.endpoints.finance_reports import FinanceReportsEndpoint
from applaud.connection import Connection
KEY_ID = "XXXXXXXXXX"
ISSUER_ID = "XXXXXX-XXXXXXX-XXXXXX-XXXXXXX"
PATH_TO_KEY = os.path.expanduser('path/to/your/key.p8')
VENDOR_NUMBER = '12345678'
with open(PATH_TO_KEY, 'r') as f:
PRIVATE_KEY = f.read()
# Create the Connection
connection = Connection(ISSUER_ID, KEY_ID, PRIVATE_KEY)
r = connection.finance_reports().filter(
report_type=FinanceReportsEndpoint.ReportType.FINANCE_DETAIL, # or 'FINANCE_DETAIL'
region_code='Z1',
report_date='2021-03',
vendor_number=VENDOR_NUMBER
).get()
r.save('finance_reports.txt', decompress=True)
Full disclosure, I'm original author of applaud.

Azure Sql connection using access token from Azure Identity with nodejs

I'm trying to use access tokens from #azure/identity to connect to azure sql using mssql (which uses tedious behind the scenes). The access tokens don't seem to work as is (quite similar to python - more on this later).
I have the following code:
const identity = require("#azure/identity")
function getConfig(accessToken){
var config = {
"authentication": {
"type": "azure-active-directory-access-token",
"options": {
"token": accessToken
}
},
"server": "dbserver.database.windows.net",
"options": {
"encrypt": true,
"database": "dbname",
}
};
return config;
}
const cred = new identity.DefaultAzureCredential();
const token = await cred.getToken("https://database.windows.net/.default")
const conf = getConfig(token.token)
let pool = await sql.connect(conf)
This always fails with "Login failed for user ''".
I have the following python code which does exactly the same:
def get_token():
creds = identity.DefaultAzureCredential()
token = creds.get_token("https://database.windows.net/.default")
tokenb = bytes(token.token, "UTF-8")
exptoken = b''
for i in tokenb:
exptoken += bytes({i})
exptoken += bytes(1)
tokenstruct = struct.pack("=i", len(exptoken)) + exptoken
return tokenstruct
def execute_query():
access_token = get_token()
print(access_token)
sql_server_name = "db-server"
sql_server_db = "database_name"
SQL_COPT_SS_ACCESS_TOKEN = 1256
connString = f"Driver={{ODBC Driver 17 for SQL Server}};SERVER={sql_server_name}.database.windows.net;DATABASE={sql_server_db}"
conn = pyodbc.connect(connString, attrs_before={
SQL_COPT_SS_ACCESS_TOKEN: access_token})
cursor = conn.cursor()
cursor.execute("SELECT * from SYSOBJECTS")
row = cursor.fetchone()
while row:
print(row)
row = cursor.fetchone()
This works perfectly. I've also noticed the following:
If I take the access token from the node version (printed by console.log) and pass it to the python code in please of access_token, I get the same error from python (Login failed for user '').
If I pass the access token from javascript and pass it to the python code for token.token (in get_token), then it works perfectly.
So I'm guessing the binary padding and packing thing that's working for python needs to be done for the node code to work. Is there some way of doing this? Or is there some better way to pass an access token from azure-identity to tedious?
Doh... I was using node-mssql which is the abandoned 0.0.1 library. Switching to mssql (v6.3.1) uses a recent version of tedious, and the access token works directly.

oauth2session dynamics365 : keep receiving Error 401 Unatuthorized

The use case
I am trying to connect to Microsoft Dynamics 365 - Field Service.
I am using Python, Falsk and OAuth2Session to perform a Oauth2 authentication
I have setup the Azure App on Azure.
the error message
I keep receiving the HTTP Error 401
Who could help me?
the code : config.py
"""Configuration settings for running the Python auth samples locally.
In a production deployment, this information should be saved in a database or
other secure storage mechanism.
"""
import os
from dotenv import load_dotenv
load_dotenv()
CLIENT_ID = os.environ["dynamics365_field_service_application_client_id"]
CLIENT_SECRET = os.environ["dynamics365_field_service_client_secrets"]
REDIRECT_URI = os.environ["dynamics365_field_service_redirect_path"]
# AUTHORITY_URL ending determines type of account that can be authenticated:
# /organizations = organizational accounts only
# /consumers = MSAs only (Microsoft Accounts - Live.com, Hotmail.com, etc.)
# /common = allow both types of accounts
AUTHORITY_URL = os.environ["dynamics365_field_service_authority"]
AUTHORIZATION_BASE_URL = os.environ["dynamics365_field_service_authorization_base_url"]
TOKEN_URL = os.environ["dynamics365_field_service_token_url"]
AUTH_ENDPOINT = "/oauth2/v2.0/authorize"
RESOURCE = "https://graph.microsoft.com/"
API_VERSION = os.environ["dynamics365_field_service_version"]
SCOPES = [
"https://admin.services.crm.dynamics.com/user_impersonation"
]
# "https://dynamics.microsoft.com/business-central/overview/user_impersonation",
# "https://graph.microsoft.com/email",
# "https://graph.microsoft.com/offline_access",
# "https://graph.microsoft.com/openid",
# "https://graph.microsoft.com/profile",
# "https://graph.microsoft.com/User.Read",
# "https://graph.microsoft.com/User.ReadBasic.All"
# ] # Add other scopes/permissions as needed.
the code : dynamics365_flask_oauth2.py
# *-* coding:utf-8 *-*
# See https://requests-oauthlib.readthedocs.io/en/latest/index.html
from requests_oauthlib import OAuth2Session
from flask import Flask, request, redirect, session, url_for
from flask.json import jsonify
import os
import flaskr.library.dynamics365.field_service.config as config
app = Flask(__name__)
app.secret_key = os.urandom(24)
# This information is obtained upon registration of a new dynamics
# client_id = "<your client key>"
# client_secret = "<your client secret>"
# authorization_base_url = 'https://dynamics.com/login/oauth/authorize'
# token_url = 'https://dynamics.com/login/oauth/access_token'
#app.route("/")
def index():
"""Step 1: User Authorization.
Redirect the user/resource owner to the OAuth provider (i.e. dynamics)
using an URL with a few key OAuth parameters.
"""
dynamics = OAuth2Session(
config.CLIENT_ID, scope=config.SCOPES, redirect_uri=config.REDIRECT_URI
)
authorization_url, state = dynamics.authorization_url(config.AUTHORIZATION_BASE_URL)
# State is used to prevent CSRF, keep this for later.
session["oauth_state"] = state
print(f"Please go here and authorize : {authorization_url}")
return redirect(authorization_url)
# Step 2: User authorization, this happens on the provider.
#app.route("/login/authorized", methods=["GET"]) # callback
def callback():
""" Step 3: Retrieving an access token.
The user has been redirected back from the provider to your registered
callback URL. With this redirection comes an authorization code included
in the redirect URL. We will use that to obtain an access token.
"""
if session.get("oauth_state") is None:
return redirect(url_for(".index"))
dynamics = OAuth2Session(
config.CLIENT_ID, state=session["oauth_state"], redirect_uri=config.REDIRECT_URI
)
token = dynamics.fetch_token(
token_url=config.TOKEN_URL,
client_secret=config.CLIENT_SECRET,
authorization_response=request.url,
)
print(f"token: {token}")
# At this point you can fetch protected resources but lets save
# the token and show how this is done from a persisted token
# in /profile.
session["oauth_token"] = token
# return redirect(url_for(".dynamics_get_accounts_postman"))
return redirect(url_for(".dynamics_get_accounts"))
#app.route("/profile", methods=["GET"])
def profile():
"""Fetching a protected resource using an OAuth 2 token.
"""
dynamics = OAuth2Session(config.CLIENT_ID, token=session["oauth_token"])
return jsonify(dynamics.get("https://api.dynamics.com/user").json())
#app.route("/get_accounts")
def dynamics_get_accounts():
if session.get("oauth_token") is None:
return redirect(url_for(".index"))
dynamics = OAuth2Session(
client_id=config.CLIENT_ID,
# token="Bearer " + session["oauth_token"]["access_token"]
token=session["oauth_token"],
)
result = dynamics.get("https://{env_name}.{region}.dynamics.com/api/data/v9.0")
if result.status_code != 200:
result = {"status code": result.status_code, "reason": result.reason}
else:
result = result.json()
result = jsonify(result)
return result
import requests
#app.route("/dynamics_get_accounts_postman")
def dynamics_get_accounts_postman():
if session.get("oauth_token") is None:
return redirect(url_for(".index"))
url = "https://{env_name}.{region}.dynamics.com/api/data/v9.0/accounts"
payload = {}
headers = {
"Accept": "application/json",
"OData-MaxVersion": "4.0",
"OData-Version": "4.0",
"If-None-Match": "null",
"Authorization": f'Bearer {session["oauth_token"]["access_token"]}',
}
response = requests.request("GET", url, headers=headers, data=payload)
result = response.text.encode("utf8")
print(f"result : {result}")
return jsonify(result)
if __name__ == "__main__":
# This allows us to use a plain HTTP callback
os.environ["DEBUG"] = "1"
app.secret_key = os.urandom(24)
app.run(debug=True)
The parameter resourcewas missing when generating the authorization_url.
authorization_url, state = dynamics.authorization_url(
config.AUTHORIZATION_BASE_URL + f"?resource={config.DYNAMICS365_CRM_ORG}"
)

How to convert Postman OAuth 2.0 to Python

I have been getting the issue when I'm trying to convert Postman OAuth 2.0 to Python3. I tried to research but seems unfortunate for me, I did not find any example
Here is my code:
from rauth import OAuth2Service
import json
def get_token(client_id, client_secret):
access_token = None
service = OAuth2Service(
name="Viafoura",
client_id=client_id,
client_secret=client_secret,
access_token_url='https://auth.viafoura.io/authorize_client'
)
data = {
'scope': 'xxxxx-xxxx-xxxx-xxxx-xxxxx',
'grant_type': 'client_credentials'
}
session = service.get_auth_session(data=data)
access_token = session
Here is OAuth 2.0 on Postman and it's working:
I want to get the access_token via Python3. Could anyone please help me on it?
Maybe it can help you, an example with the basic algorithm of OAuth2
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from requests import post, auth, exceptions
from json import loads
if __name__ == '__main__':
client_id = ''
client_secret = ''
user = ''
password = ''
access_point = 'https://account.lab.fiware.org/oauth2/token'
grant_type = 'password'
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
auth = auth.HTTPBasicAuth(client_id, client_secret)
data = {'grant_type': grant_type,
'username': user,
'password': password}
resp = None
try:
resp = post(access_point, auth=auth, data=data, headers=headers, timeout=5)
except exceptions.ConnectionError:
exit(1)
if resp.status_code == 200:
resp = loads(resp.text)
if 'access_token' in resp:
print(resp['access_token'])
exit(0)
exit(1)
You need to fix access point, grant type. This source code can be found here
Sorry, I can't help directly with Viafoura and OAuth2Service library.

Resources