I have a Python function using the preview option of sending custom metrics to Azure using the REST API https://learn.microsoft.com/en-us/azure/azure-monitor/platform/metrics-store-custom-rest-api, previously this was a C# function where authorisation and getting a bearer token was handled automagically by:
var azureServiceTokenProvider = new AzureServiceTokenProvider();
string bearerToken = await azureServiceTokenProvider.GetAccessTokenAsync("https://monitoring.azure.com/").ConfigureAwait(false);
This worked in VS Code using the logged in user and in Azure when a Managed Identity was assigned to the Function.
I needed to convert this to Python but so far the best (working) I've been able to come up with is:
import logging, requests, os, adal
import azure.functions as func
def main(req: func.HttpRequest) -> func.HttpResponse:
regional_monitoring_url = "https://eastus.monitoring.azure.com"
monitored_resource_id = os.environ['RESOURCE_ID']
full_endpoint = f"{regional_monitoring_url}{monitored_resource_id}/metrics"
tenant_id = os.environ['AZURE_TENANT_ID']
context = adal.AuthenticationContext(f'https://login.microsoftonline.com/{tenant_id}')
token = context.acquire_token_with_client_credentials("https://monitoring.azure.com/", os.environ['AZURE_CLIENT_ID'], os.environ['AZURE_CLIENT_SECRET'] )
bearer_token = token['accessToken']
json = req.get_json()
headers = {"Authorization": 'Bearer ' + bearer_token}
result = requests.post(url = full_endpoint, headers = headers, json = json)
return func.HttpResponse(f"Done - {result.status_code} {result.text}", status_code=200)
This obviously relies on me creating a Service Principal with the relevant permissions. I'm trying to work out how to use the automatic Managed Identity authorisation that the C# libraries have.
I know ADAL should be replaced by MSAL but I can't work out how/if that automagically handles Managed Identities so I tried azure-identity:
from azure.identity import DefaultAzureCredential
credential = DefaultAzureCredential()
token = credential.get_token("https://monitoring.azure.com/.default")
bearer_token = token.token
This gets me a token but because it requires a scope rather than a resource, which means adding .default to the resource URL, when I send the bearer token to the monitoring endpoint it complains the resource doesn't match and must be exactly "https://monitoring.azure.com/"
Is this just not currently possible or am I missing something with either azure-identity or the MSAL Python modules?
According to my research, when werequest an Azure AD token to emit custom metrics, ensure that the audience the token is requested for is https://monitoring.azure.com/. For more details, please refer to here. So we should update scope as https://monitoring.azure.com//.default
For example
def main(req: func.HttpRequest) -> func.HttpResponse:
logging.info('Python HTTP trigger function processed a request.')
credential = DefaultAzureCredential()
token = credential.get_token("https://monitoring.azure.com//.default")
bearer_token = token.token
#full_endpoint=""
json = req.get_json()
headers = {"Authorization": 'Bearer ' + bearer_token}
#result = requests.post(url = full_endpoint, headers = headers, json = json)
return func.HttpResponse(f"Done - {bearer_token}", status_code=200)
Related
I'm trying to use the sandbox from https://fhir.epic.com/ for Backend Services.
I am following this tutorial : https://fhir.epic.com/Documentation?docId=oauth2§ion=BackendOAuth2Guide :
I already register a new app, created a JWT (using SSL keys) tested the JWT on https://jwt.io/ : works fine! When I POST the JWT to the endpoint (https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token) I get an access token.
Using this access token, I can access a single patient Appointment ressource but that's it. I don't understand how to access other Resources like DiagnosticReport, Observations, etc. I added them in the scope of my App of course but still doesn't seems to work.
What am I missing here ?
This is my code where I can access the Appointment resource:
import json
from datetime import datetime, timedelta, timezone
import requests
from requests.structures import CaseInsensitiveDict
from jwt import (
JWT,
jwk_from_dict,
jwk_from_pem,
)
from jwt.utils import get_int_from_datetime
import random
import xmltodict
def main():
instance = JWT()
message = {
# Client ID for non-production
'iss': 'my_iss_here',
'sub': 'my_sub_here',
'aud': 'https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token',
'jti': f'7777-7777-7777-7777-7777{random.randint(100,1000)}',
'iat': get_int_from_datetime(datetime.now(timezone.utc)),
'exp': get_int_from_datetime(datetime.now(timezone.utc) + timedelta(minutes=2)),
}
# Load a RSA key from a PEM file.
with open('./privatekey.pem', 'rb') as fh:
signing_key = jwk_from_pem(fh.read())
compact_jws = instance.encode(message, signing_key, alg='RS384')
headers = {}
headers['Content-Type'] = 'application/x-www-form-urlencoded'
data = {
'grant_type': 'client_credentials',
'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
'client_assertion': compact_jws
}
x = requests.post('https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token', headers=headers, data=data)
access_token = x.json()['access_token']
headers = {}
headers['Authorization'] = f'Bearer {access_token}'
x = requests.get('https://fhir.epic.com/interconnect-fhir-oauth/api/FHIR/STU3/Appointment/eWLhfjXHp4RUczv2om.1Ii2uiHcDc6rMEjO0xHBA3', headers=headers)
print(x.content)
When I change the request for one of these two (of the online tutorial), it doesn't work:
x = requests.get('https://fhir.epic.com/interconnect-fhir-oauth/api/FHIR/R4/Observation/erXuFYUfucBZaryVksYEcMg3', headers=headers)
I'm using the FHIR ID of Camilia Lopez (test patient). She supposed to have a Observation Ressource but I get an error :
<OperationOutcome xmlns="http://hl7.org/fhir"><issue><severity value="fatal" /><code value="not-found" /><details><coding><system value="urn:oid:1.2.840.114350.1.13.0.1.7.2.657369" /><code value="59008" /><display value="The FHIR ID provided was not found." /></coding><text value="The FHIR ID provided was not found." /></details><diagnostics value="Invalid FHIR ID provided" /><location value="/f:id" /><expression value="id" /></issue></OperationOutcome>
Neither of those appear to be valid FHIR IDs for the resources requested in the Epic on FHIR sandbox. The IDs in the tutorial are just examples. You should use the test data reference here for identifying Patient and other resources that are available to test with. You should also ensure you are including all the necessary headers in your calls.
I'm trying to create an httptrigger function that calls a Google Chat API webhook. I'm doing this so I can provide another API with a simple callback URL.
Basically I've written the following but getting the usual unhelpful 500 error
import logging
import azure.functions as func
# for chat API call
import http.client
import json
# end for chat API call
def main(req: func.HttpRequest) -> func.HttpResponse:
logging.info('Python HTTP trigger function processed a request.')
# call the other API
conn = http.client.HTTPSConnection("chat.googleapis.com")
payload = "{'text':'hello world, from my azure function.'\r\n}"
headers = {'Content-Type': 'application/json'}
conn.request("POST", "/v1/spaces/Ajdf9u4jf4/messages?key=f03j4fij43", payload, headers)
res = conn.getresponse()
data = res.read()
print(data.decode("utf-8"))
# end call
name = req.params.get('name')
if not name:
try:
req_body = req.get_json()
except ValueError:
pass
else:
name = req_body.get('name')
if name:
return func.HttpResponse(f"Hello, {name}. This HTTP triggered function executed successfully.")
else:
return func.HttpResponse(
"This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.",
status_code=200
)
I'm getting a 200 on the function but I don't see a message in Google Chat. The python I have added to this function to post to the Chat webhook works in Postman, but not here. Any suggestions?
i am new in this part of programming and i have few questions. First of all my project. At one side i have a Flutter App and at the other side a MS SQL Server with data. This data i need on my device logically. I read the best way is to use FastAPI, its easy and has a good performance but i am not sure about security. I read something about OAuth2 but it looks to much because just one user will have permission to use the data (the server owner). Is it possible just to use a simple api key as a parameter? Something like this...
from fastapi import FastAPI
from SqlServerRequest import SqlServerRequest
app = FastAPI()
#app.get("/openOrders/{key}")
async def openOrders(key):
if key == "myverysecurekey":
return "SQLDATA"
else
return "Wrong key"
That way works but i am not sure about the security
What would you say?
I have been dealing with the same issue for a while. Instead of using a oauth I needed a simple X-API-Key in the header.
You can do that with the following code
from fastapi import FastAPI, Depends
from fastapi.security import APIKeyHeader
import os
os.environ['API-KEY'] = '1234'.
# You would use as an environment var in real life
X_API_KEY = APIKeyHeader(name='X-API-Key')
def api_key_auth(x_api_key: str = Depends(X_API_KEY)):
""" takes the X-API-Key header and validate it with the X-API-Key in the database/environment"""
if x_api_key != os.environ['API-KEY']:
raise HTTPException(
status_code=401,
detail="Invalid API Key. Check that you are passing a 'X-API-Key' on your header."
)
app = FastAPI()
#app.get("/do_something", dependencies=[Depends(api_key_auth)])
async def do_something():
return "API is working OK."
If your use case is just to serve a single user, and is not mission-critical, this might be a good way to start.
main.py
import os
import uvicorn
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
from starlette import status
# Use token based authentication
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# Ensure the request is authenticated
def auth_request(token: str = Depends(oauth2_scheme)) -> bool:
authenticated = token == os.getenv("API_KEY", "DUMMY-API-KEY")
return authenticated
app = FastAPI()
#app.get("/openOrders")
async def open_orders(authenticated: bool = Depends(auth_request)):
# Check for authentication like so
if not authenticated:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated")
# Business logic here
return {"message": "Authentication Successful"}
if __name__ == '__main__':
uvicorn.run("main:app", host="127.0.0.1", port=8080)
You can run this using python main.py
The client can then make requests like so:
import requests
url = "http://127.0.0.1:8080/openOrders"
payload={}
# The client would pass the API-KEY in the headers
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer DUMMY-API-KEY'
}
response = requests.request("GET", url, headers=headers, data=payload)
print(response.text)
Client code in Dart
final response = await http.get(
Uri.parse('http://127.0.0.1:8080/openOrders'),
// Send authorization headers to the backend.
headers: {
HttpHeaders.authorizationHeader: 'Bearer DUMMY-API-KEY',
},
);
After reading the documentation I am struggling to understand how to use Authlib to implement Authorize Code Flow for an OpenID Connect provider. After reading the documentation I have had a go at implementing the following code listed below.
The /login endpoint uses authlib to redirect to authorization of Identity Provider, in this case Cognito. This redirects to /aws_cognito_redirect which I have currently implemented myself to resolve the code challenge to retrieve tokens.
My questions are:
How to use authlib to also resolve the code challenge instead of implementing this part myself?
Does Authlib provide functionality to return token(s) in HTTP Only cookie and verify tokens in subsequent requests containing the cookie? For example, does Authlib allow an endpoint to be decorated/marked to as protected, in which case it will verify the tokens in HTTP Only cookie?
Update
After inspecting the source code I eventually figured out how to resolve the code challenge using Authlib with FastAPI. The source code is included at the end of this question.
I am leaving the question open since the second part remains unanswered.
Currently, this question suggests that it is possible to use ResourceProtector class that would do what I need. However, that has a parse_request_authorization method that inspects the Authorisation header of a request for a bearer token. So...I am assuming the approach is to subclass ResourceProtector class and override this method to inspect request for HTTP only cookie and extract the JWT contained within for verification?? Is this feature implemented and provided by Authlib?
Alternatively, also investigating to see if I can integrate fastapi-login to achieve this functionality.
Appendix: Source Code
Initial Source Code With Custom Implementation For Resolving Code Challenge
import base64
from functools import lru_cache
import httpx
from authlib.integrations.starlette_client import OAuth
from fastapi import Depends, FastAPI, Request, Response
from fastapi.responses import RedirectResponse
from starlette.middleware.sessions import SessionMiddleware
from . import config
#lru_cache()
def get_settings() -> config.Settings:
"""Create config settings instance encapsulating app config."""
return config.Settings()
def get_auth_base_url(region: str, userpool_id: str) -> str:
# base_url = "https://cognito-idp.us-east-2.amazonaws.com/us-east-2_QqNgzdtT5"
base_url = "https://cognito-idp." + region + ".amazonaws.com/" + userpool_id
return base_url
app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key="secretly")
oauth = OAuth()
oauth.register(
"cognito",
client_id=get_settings().client_id,
client_secret=get_settings().client_secret,
server_metadata_url=get_auth_base_url(
get_settings().region, get_settings().userpool_id
)
+ "/.well-known/openid-configuration",
client_kwargs={"scope": "openid email"},
)
def encode_auth_header(client_id: str, client_secret: str) -> str:
"""Encode client id and secret as base64 client_id:client_secret."""
secret = base64.b64encode(
bytes(client_id, "utf-8") + b":" + bytes(client_secret, "utf-8")
)
return "Basic " + secret.decode()
#app.get("/login")
async def login(request: Request):
"""Redirect to /aws_cognito_redirect endpoint."""
cognito = oauth.create_client("cognito")
redirect_uri = request.url_for("read_code_challenge")
return await cognito.authorize_redirect(request, redirect_uri)
#app.get("/aws_cognito_redirect")
async def read_code_challenge(
request: Request,
response: Response,
settings: config.Settings = Depends(get_settings),
):
"""Retrieve tokens from oauth2/token endpoint and return session cookie."""
code = request.query_params["code"]
print("/aws_cognito_redirect received code := ", code)
auth_secret = encode_auth_header(settings.client_id, settings.client_secret)
headers = {"Authorization": auth_secret}
print("Authorization:" + str(headers["Authorization"]))
payload = {
"client_id": settings.client_id,
"code": code,
"grant_type": "authorization_code",
"redirect_uri": settings.redirect_uri,
}
token_url = (
"https://"
+ settings.domain
+ ".auth."
+ settings.region
+ ".amazoncognito.com/oauth2/token"
)
async with httpx.AsyncClient() as client:
tokens = await client.post(
token_url,
data=payload,
headers=headers,
)
tokens.raise_for_status()
print("Tokens\n" + str(tokens.json()))
response.set_cookie(key="jwt", value=tokens.content, httponly=True)
Updated Source Code To Demonstrate How To Resolve Code Challenge Using Authlib
import base64
from functools import lru_cache
from authlib.integrations.starlette_client import OAuth
from fastapi import FastAPI, Request
from starlette.middleware.sessions import SessionMiddleware
from . import config
#lru_cache()
def get_settings() -> config.Settings:
"""Create config settings instance encapsulating app config."""
return config.Settings()
#lru_cache
def get_auth_base_url(region: str, userpool_id: str) -> str:
"""Return cognito discover points base url from region and userpool ID."""
return ("https://cognito-idp." + region + ".amazonaws.com/" + userpool_id)
app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key="some-random-string")
oauth = OAuth()
cognito = oauth.register(
"cognito",
client_id=get_settings().client_id,
client_secret=get_settings().client_secret,
server_metadata_url=get_auth_base_url(
get_settings().region, get_settings().userpool_id
)
+ "/.well-known/openid-configuration",
client_kwargs={"scope": "openid email"},
)
def encode_auth_header(client_id: str, client_secret: str) -> str:
"""Encode client id and secret as base64 client_id:client_secret."""
secret = base64.b64encode(
bytes(client_id, "utf-8") + b":" + bytes(client_secret, "utf-8")
)
return "Basic " + secret.decode()
#app.get("/")
async def login(request: Request):
"""Redirect to /aws_cognito_redirect endpoint after sign-in."""
redirect_uri = request.url_for("read_code_challenge")
return await cognito.authorize_redirect(request, redirect_uri)
#app.get("/aws_cognito_redirect")
async def read_code_challenge(request: Request):
"""Request a token from cognito using code challenge response."""
return await cognito.authorize_access_token(request)
I am trying to reach my automL model prediction endpoint that I have setup, I have created a service account and added the correct role to it, now I am trying to call the endpoint in Python, but I am not sure how to call it: Here is what I have tried. The key file is downloaded from google so it's good.
from google.oauth2 import service_account
project_id = 'aaa'
endpoint_id = 'bbb'
with open('./ServiceAccountKey.json') as source:
info = json.load(source)
credentials = service_account.Credentials.from_service_account_info(info)
scoped_credentials = credentials.with_scopes(
['https://www.googleapis.com/auth/cloud-platform'])
access_token = scoped_credentials.get_access_token()
endpoint = f"https://us-central1-aiplatform.googleapis.com/v1alpha1/projects/{project_id}/locations/us-central1/endpoints/{endpoint_id}:predict"
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer " + access_token}
payload = {}
x = requests.post(endpoint, data = payload, header=headers)
print(x)
The error I get is this:
AttributeError: 'Credentials' object has no attribute 'get_access_token'
Permissions I have given the service account:
The best way is to use the AI Platform Python client library. Set the GOOGLE_APPLICATION_CREDENTIALS environment variable to the path to your service account file and you are good to go.
Better to not deal with access token and refresh tokens yourself if there is a ready-made library available.