How to use AWS Cognito with FastAPI authentication - python

I'm trying to add authentication to a FastAPI application using AWS Cognito. I can get valid JSON responses from Cognito, including AccessToken and RefreshToken. Using the FastAPI Oauth2 examples I've seen has led me to create code like this:
#router.post("/token")
async def get_token(form_data: OAuth2PasswordRequestForm = Depends()):
# get token from cognito
response = await concurrency.run_in_threadpool(
client.initiate_auth,
ClientId=settings.aws_cognito_app_client_id,
AuthFlow="USER_PASSWORD_AUTH",
AuthParameters={
"USERNAME": form_data.username,
"PASSWORD": form_data.password,
},
)
return response["AuthenticationResult"]["AccessToken"]
#router.post("/things")
async def things(token: str = Depends(oauth2_scheme)):
return {"token": token}
This seems to work as the "/things" endpoint is only accessible if authorized through the OpendAPI authentication popup. However, two things:
The token value is "undefined" in the things() handler, why is that?
How do I get the RefreshToken to the user?
Any suggestions or ideas are welcome.

Related

Error call Google Vertex AI endpoint from a python backend

I am trying to send an http post request to my google vertex ai endpoint for prediction. Though I do set the Bearer Token in the request header, the request still fails with the below error:
{
"error": {
"code": 401,
"message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
"status": "UNAUTHENTICATED",
"details": [
{
"#type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "ACCESS_TOKEN_TYPE_UNSUPPORTED",
"metadata": {
"service": "aiplatform.googleapis.com",
"method": "google.cloud.aiplatform.v1.PredictionService.Predict"
}
}
]
}
}
Since I am making this call from a python backend, I'm not sure if OAuth 2 as suggested in the message would be wise and applicable choice.
The model is already deployed and endpointed test on vertex ai and it worked fine. What I am trying to do is send same prediction task via an http post request using postman and this is what failed.
The request url looks like this:
https://[LOCATION]-aiplatform.googleapis.com/v1/projects/[PROJECT ID]/locations/[LOCATION]/endpoints/[ENDPOINT ID]:predict
Where token bearer is set in the potman authorization tab and instance set in request body.
I usually call Vertex AI endpoint this way:
from google.cloud import aiplatform
import os
os.environ["GOOGLE_APPLICATION_CREDENTIALS"]="/home/user/1a2b3c4d.json"
aip_endpoint_name = (
f"projects/your-project/locations/us-west1/endpoints/1234567"
)
endpoint = aiplatform.Endpoint(aip_endpoint_name)
Here, you encode the input according to your needs.
def encode_64(input):
message = input
message_bytes = message.encode('ascii')
base64_bytes = base64.b64encode(message_bytes)
base64_message = base64_bytes.decode('ascii')
return base64_message
Check the model signature for the input type:
saved_model_cli show --dir /home/jupyter/model --all
Then call the endpoint
instances_list = [{"input_1": {"b64": encode_64("doctor")}}]
instances = [json_format.ParseDict(s, Value()) for s in instances_list]
results = endpoint.predict(instances=instances)
print(results.predictions)
According to the type of the input you are submitting to Vertex AI endpoint (integer, array, string, image), you may have to change the encoding.
If you don't want to use oAuth 2.0 for authentication when making http post request, you may want to use Application Default Credentials. In this documentation you can follow the step by step on getting service account key to passing credentials via environment variable.
Can you try following:
import subprocess
import base64
import requests
import json
def get_headers():
gcloud_access_token = subprocess.check_output("gcloud auth print-access-token".split(' ')).decode().rstrip('\n')
return {"authorization": "Bearer " + gcloud_access_token}
def send_get_request(uri):
return requests.get(uri, headers=get_headers(), verify=False)
It should work ff you have gcloud configured (you can do this by using gcloud init) on the machine you are sending request from and have permissions to access Vertex Prediction.

Passwordless authentication flow using Cognito & API Gateway & Lambda (Python)

I've been trying to implement passwordless authentication using AWS Cognito & API Gateway & Lambda (Python)
I have followed these articles:
https://medium.com/digicred/password-less-authentication-in-cognito-cafa016d4db7
https://medium.com/#pjatocheseminario/passwordless-api-using-cognito-and-serverless-framework-7fa952191352
I have configured Cognito (to accept CUSTOM_AUTH), added the Lambdas, and created the API endpoints:
/sign-up
/initiate-auth (aka initiate login)
/respond-to-auth-challenge (aka (verify login)
When calling initiateAuth I receive the following response:
An error occurred (NotAuthorizedException) when calling the InitiateAuth operation: Incorrect username or password."
I'm using CUSTOM_AUTH which doesn't require password, and the user name is definitely correct because it actually initiates the authentication flow and I receive a code, however because boto3 doesn't respond with a session I can't continue the authentication.
This is how I call Cognito:
res = cognito.initiate_auth(
ClientId=client_id,
AuthFlow="CUSTOM_AUTH",
AuthParameters={
"USERNAME": email,
"PASSWORD": random_password
}
)
It's probably something small I'm missing but I can't figure out what.
Your client code looks OK, mine has ClientId param in it but if your code is not raising an exception then it should be fine. Unless you had Generate client secret option checked when you created your app client.
If that is the case then you have to pass in SECRET_HASH in AuthParameters like the following:
import hmac
import hashlib
import base64
def get_secret_hash(email, client_id, client_secret):
"""
A keyed-hash message authentication code (HMAC) calculated using
the secret key of a user pool client and username plus the client
ID in the message.
"""
message = email + client_id
client_secret = str.encode(client_secret)
dig = hmac.new(client_secret, msg=message.encode('UTF-8'), digestmod=hashlib.sha256).digest()
return base64.b64encode(dig).decode()
client.admin_initiate_auth(
UserPoolId=COGNITO_USER_POOL_ID,
ClientId=CLIENT_ID,
AuthFlow='CUSTOM_AUTH',
AuthParameters={
'USERNAME': email,
'SECRET_HASH': get_secret_hash(email, CLIENT_ID, CLIENT_SECRET) # Omit if secret key option is disabled.
},
)
Next, double check the following:
Under App clients > * > Auth Flows Configuration, is ALLOW_CUSTOM_AUTH option enabled for your client?
Under App integration > App client settings > * > Enabled Identity Providers, is your user pool selected?
If you have Cognito setup correctly and your code still doesn't work then it is probably the lambda code. You probably know this but for password-less custom auth you need to use 3 lambda triggers: Define Auth Challenge, Create Auth Challenge, and Verify Auth Challenge.
Custom auth lambdas events are triggered in the following order:
DefineAuthChallenge_Authentication:
Technically, issueTokens can be set to True here to return tokens without going through the rest of the steps.
def lambda_handler(event, context):
if event['triggerSource'] == 'DefineAuthChallenge_Authentication':
event['response']['challengeName'] = 'CUSTOM_CHALLENGE'
event['response']['issueTokens'] = False
event['response']['failAuthentication'] = False
if event['request']['session']: # Needed for step 4.
# If all of the challenges are answered, issue tokens.
event['response']['issueTokens'] = all(
answered_challenge['challengeResult'] for answered_challenge in event['request']['session'])
return event
CreateAuthChallenge_Authentication:
def lambda_handler(event, context):
if event['triggerSource'] == 'CreateAuthChallenge_Authentication':
if event['request']['challengeName'] == 'CUSTOM_CHALLENGE':
event['response']['privateChallengeParameters'] = {}
event['response']['privateChallengeParameters']['answer'] = 'YOUR CHALLENGE ANSWER HERE'
event['response']['challengeMetadata'] = 'AUTHENTICATE_AS_CHALLENGE'
return event
Then your client must respond to the challenge:
client.respond_to_auth_challenge(
ClientId=CLIENT_ID,
ChallengeName='CUSTOM_CHALLENGE',
Session=session,
ChallengeResponses={
'USERNAME': email,
'ANSWER': 'Extra Protection!',
'SECRET_HASH': get_secret_hash(email, CLIENT_ID, CLIENT_SECRET) # Omit if secret key option is disabled.
}
)
VerifyAuthChallengeResponse_Authentication:
def lambda_handler(event, context):
if event['triggerSource'] == 'VerifyAuthChallengeResponse_Authentication':
if event['request']['challengeAnswer'] == event['request']['privateChallengeParameters']['answer']:
event['response']['answerCorrect'] = True
return event
DefineAuthChallenge_Authentication:
Set event['response']['issueTokens'] to True to return tokens (code shown in step 1), or issue another challenge to keep repeating steps 1-3.
Finally, make sure that if case-insensitivity option is enabled for your user pool too. Also, I can't exactly recall if CUSTOM_AUTH flow worked if the user is in FORCE_CHANGE_PASSWORD status. If the user is in that state, then try settings a permanent password with the sdk to set the status to CONFIRMED.
I was facing the same error, and I think that the error message is misleading.
When you did not respond correctly in Create-Auth-Challenge lambda, you will get this error. So make sure everything is right in your lambda.

Errror parsing data in python FastAPI

I'm learning to use FastAPI, and I'm getting this error over and over again while implementing a simple API and I've not being able to figure out why
"detail": "There was an error parsing the body"
This happends me on this two endpoints:
Full code: Code Repository
snippet:
app_v1 = FastAPI(root_path='/v1')
# JWT Token request
#app_v1.post('/token')
async def login_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
jwt_user_dict = {"username": form_data.username, "password": form_data.password}
jwt_user = JWTUser(**jwt_user_dict)
user = authenticate_user(jwt_user)
if user is None:
return HTTP_401_UNAUTHORIZED
jwt_token = create_jwt_token(user)
return {"token": jwt_token}
request:
#app_v1.post("/user/photo")
async def update_photo(response: Response, profile_photo: bytes = File(...)):
response.headers['x-file-size'] = str(len(profile_photo))
response.set_cookie(key='cookie-api', value="test")
return {"profile photo size": len(profile_photo)}
request:
I acomplished to figure out, it was because when FastAPI was installed, it didn't install python-multipart, so with this package missing everything that needs multipart falls
After installing it works fine
Thanks
The problem with the first request is that you should be sending username and password in a form-data. Instead of x-www-form-urlencoded, use form-data and you should be fine.
I can't see the problem with the second one. Can you try using Swagger interface and see if the same happens there?

How to create a SECRET_HASH for AWS Cognito using boto3?

I want to create/calculate a SECRET_HASH for AWS Cognito using boto3 and python. This will be incorporated in to my fork of warrant.
I configured my cognito app client to use an app client secret. However, this broke the following code.
def renew_access_token(self):
"""
Sets a new access token on the User using the refresh token.
NOTE:
Does not work if "App client secret" is enabled. 'SECRET_HASH' is needed in AuthParameters.
'SECRET_HASH' requires HMAC calculations.
Does not work if "Device Tracking" is turned on.
https://stackoverflow.com/a/40875783/1783439
'DEVICE_KEY' is needed in AuthParameters. See AuthParameters section.
https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_InitiateAuth.html
"""
refresh_response = self.client.initiate_auth(
ClientId=self.client_id,
AuthFlow='REFRESH_TOKEN',
AuthParameters={
'REFRESH_TOKEN': self.refresh_token
# 'SECRET_HASH': How to generate this?
},
)
self._set_attributes(
refresh_response,
{
'access_token': refresh_response['AuthenticationResult']['AccessToken'],
'id_token': refresh_response['AuthenticationResult']['IdToken'],
'token_type': refresh_response['AuthenticationResult']['TokenType']
}
)
When I run this I receive the following exception:
botocore.errorfactory.NotAuthorizedException:
An error occurred (NotAuthorizedException) when calling the InitiateAuth operation:
Unable to verify secret hash for client <client id echoed here>.
This answer informed me that a SECRET_HASH is required to use the cognito client secret.
The aws API reference docs AuthParameters section states the following:
For REFRESH_TOKEN_AUTH/REFRESH_TOKEN: USERNAME (required), SECRET_HASH
(required if the app client is configured with a client secret),
REFRESH_TOKEN (required), DEVICE_KEY
The boto3 docs state that a SECRET_HASH is
A keyed-hash message authentication code (HMAC) calculated using the
secret key of a user pool client and username plus the client ID in
the message.
The docs explain what is needed, but not how to achieve this.
The below get_secret_hash method is a solution that I wrote in Python for a Cognito User Pool implementation, with example usage:
import boto3
import botocore
import hmac
import hashlib
import base64
class Cognito:
client_id = app.config.get('AWS_CLIENT_ID')
user_pool_id = app.config.get('AWS_USER_POOL_ID')
identity_pool_id = app.config.get('AWS_IDENTITY_POOL_ID')
client_secret = app.config.get('AWS_APP_CLIENT_SECRET')
# Public Keys used to verify tokens returned by Cognito:
# http://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-with-identity-providers.html#amazon-cognito-identity-user-pools-using-id-and-access-tokens-in-web-api
id_token_public_key = app.config.get('JWT_ID_TOKEN_PUB_KEY')
access_token_public_key = app.config.get('JWT_ACCESS_TOKEN_PUB_KEY')
def __get_client(self):
return boto3.client('cognito-idp')
def get_secret_hash(self, username):
# A keyed-hash message authentication code (HMAC) calculated using
# the secret key of a user pool client and username plus the client
# ID in the message.
message = username + self.client_id
dig = hmac.new(self.client_secret, msg=message.encode('UTF-8'),
digestmod=hashlib.sha256).digest()
return base64.b64encode(dig).decode()
# REQUIRES that `ADMIN_NO_SRP_AUTH` be enabled on Client App for User Pool
def login_user(self, username_or_alias, password):
try:
return self.__get_client().admin_initiate_auth(
UserPoolId=self.user_pool_id,
ClientId=self.client_id,
AuthFlow='ADMIN_NO_SRP_AUTH',
AuthParameters={
'USERNAME': username_or_alias,
'PASSWORD': password,
'SECRET_HASH': self.get_secret_hash(username_or_alias)
}
)
except botocore.exceptions.ClientError as e:
return e.response
I also got a TypeError when I tried the above solution. Here is the solution that worked for me:
import hmac
import hashlib
import base64
# Function used to calculate SecretHash value for a given client
def calculateSecretHash(client_id, client_secret, username):
key = bytes(client_secret, 'utf-8')
message = bytes(f'{username}{client_id}', 'utf-8')
return base64.b64encode(hmac.new(key, message, digestmod=hashlib.sha256).digest()).decode()
# Usage example
calculateSecretHash(client_id, client_secret, username)

Getting auth token from keystone in horizon

I want to get the auth token from keystone using horizon and then wants to pass that auth token to my backed code.
i don't know how to get this, please help me out.
I read many articles and blogs blogs but i am not able to find the answer. Please just point me into the right direction.
Easiest is to use a Rest client to login and just take the token from the response. I like the Firefox RESTClient add-on but you can use any client you want.
Post a request to the Openstack Identity URL:
POST keystone_ip:port/version/tokens
(e.g. 127.0.0.1:5000/v2.0/tokens)
with header:
Content-Type: application/json
and body:
{
"auth": {
"tenantName": "enter_your_tenantname",
"passwordCredentials": {
"username": "enter_your_username",
"password": "enter_your_password"
}
}
}
Note: If you're not sure what is the correct identity (keystone) URL you can log in manually to Horizon and look for a list of API endpoints.
The response body will include something like this:
{
"token": {
"issued_at": "2014-02-25T08:34:56.068363",
"expires": "2014-02-26T08:34:55Z",
"id": "529e3a0e1c375j498315c71d08134837"
}
}
Use the returned token id as a header in new rest calls. For example, to get a list of servers use request:
GET compute_endpoint_ip:port/v2/tenant_id/servers
with Headers:
X-Auth-Token: 529e3a0e1c375j498315c71d08134837
Content-Type: application/json
As an example of how to get at it:
import keystoneclient.v2_0.client as ksclient
# authenticate with keystone to get a token
keystone = ksclient.Client(auth_url="http://192.168.10.5:35357/v2.0",
username="admin",
password="admin",
tenant_name="admin")
token = keystone.auth_ref['token']['id']
# use this token for whatever other services you are accessing.
print token
You can use python-keystoneclient. To authenticate the user, use for example
username='admin'
password='1234'
tenant_name='admin'
auth_url='http://127.0.0.1:5000/v2.0'
keystone = client.Client(username=username, password=password, tenant_name=tenant_name, auth_url=auth_url)
Once, the user is authenticated, a token is generated. The auth_ref property on the client ( keystone variable in this example) will give you a dictionary like structure having all the information you need about the token, which will enable you to re-use the token or pass it to the back-end in your case.
token_dict = keystone.auth_ref
Now,the token_dict is the variable that you can pass to your back-end.
Go to the node where you have installed Keystone services. Open vi /etc/keystone/keystone.conf
Check for the third line starting admin_token. It should be a long random string:
admin_token = 05131394ad6b49c56f217
That is your keystone token. Using python:
>>> from keystoneclient.v2_0.client as ksclient
>>> keystone = ksclient.Client(auth_url="http://service-stack.linxsol.com:35357/v2.0", username="admin", password="123456", tenant_name="admin")
Ofcourse, you will change auth_url, *username, password* and tenant_name to your choice. Now you can use keystone to execute all the api tasks:
keystone.tenants.list()
keystone.users.list()
keystone.roles.list()
Or use dir(keystone) to list all the available options.
You can reuse the token as follows:
auth_ref = keystone.auth_ref or token = ksclient.get_raw_token_from_identity_service(auth_url="http://service-stack.linxsol.com:35357/v2.0", username="admin", password="123456", tenant_name="admin")
But remember it returns a dictionary and a raw token not in a form of a token as you can see above.
For further information please check the python-keystoneclient.
I hope that helps.
Use the python-keystoneclient package.
Look into the Client.get_raw_token_from_identity_service method.
First you have to install python-keystoneclient.
To generate the token you can use the following code, here I want to mention you can change the authentication url with your server url but port number will be same,
from keystoneclient.v2_0 import client
username='admin'
password='1234'
tenant_name='demo'
auth_url='http://10.0.2.15:5000/v2.0' # Or auth_url='http://192.168.206.133:5000/v2.0'
if your username, password, or tenant_name is wrong then you will get keystoneclient.openstack.common.apiclient.exceptions.Unauthorized: Invalid user / password
keystone = client.Client(username=username, password=password, tenant_name=tenant_name, auth_url=auth_url)
token_dict = keystone.auth_ref
token_dict

Categories

Resources