Server to Server Authentication in GCP using Service Accounts - python

We have two Django backend applications running on GCP, let’s call it A and B. Both of these applications have a URL which can be accessed via web and many of the endpoints are secured, i.e; you require to be logged in to access the endpoint. Apart from the user authenticated URLs, I want a secure endpoint (let’s call it /server-secure) in application server B to be accessible ONLY by application server A. Which means I need to authorise and verify requests coming in at /server-secure URL to make sure they are coming from server A.
I would like to use the server A’s metadata to generate a signed instance token which I will use to verify the identity of the server. This is not an issue, because I can easily achieve this using Google Auth python library:
import google.auth
import google.oauth2.id_token
import google.auth.transport.requests
request = google.auth.transport.requests.Request()
target_audience = "https://pubsub.googleapis.com"
token = google.oauth2.id_token.fetch_id_token(request, target_audience)
I have also been able to decode the token on the other end:
from google.oauth2 import id_token
request = google.auth.transport.requests.Request()
print(id_token.verify_token(token, request))
My main issue is that I would like the communication to happen only and only if server A has a certain service account attached. There is one way to check this; which is to use the email key in the decoded token dictionary and check if it’s value is equal to some service account, but just out of curiosity, is there a better way to this?
Or, is it possible to create custom role (like "access-to-server-B") and authorise the request ONLY if the service account contains this specific role???

Related

How to get authenticated identity response from AWS Cognito using boto3

I would like to use boto3 to get temporary credentials for access AWS services. The use case is this: A user in my Cognito User Pool logs in to my server and I want the server code to provide that user with temporary credentials to access other AWS services.
I have a Cognito User Pool where my users are stored. I have a Cognito Identity Pool that does NOT allow unauthorized access, only access by users from the Cognito User Pool.
So here is the code I am starting with:
import boto3
client = boto3.client('cognito-identity','us-west-2')
resp = client.get_id(AccountId='<ACCNTID>',
IdentityPoolId='<IDPOOLID>')
However, just running these three lines of code throws an exception:
botocore.errorfactory.NotAuthorizedException: An error
occurred (NotAuthorizedException) when calling
the GetId operation: Unauthenticated access is not
supported for this identity pool.
Since my Cognito Identity Pool is not set up for unauthenticated access, it seems that I cannot call get_id until I somehow authenticate somewhere.
How do I solve this? What exactly do I need to do to authenticate so I can call get_id?
UPDATE: Looks like I need to pass a Logins field and data to the get_id function call, but to do that I need the login JWT token. If I am running this inside a webapp (eg a Django backend) where I use the AWS Cognito prepackaged login screens, then yes I can get this from the homepage URL after redirection from successful login. But now I am writing some test scripts that have nothing to do with a website. Is there a way to use boto or boto3 or some other python package to login with username and password and get JWT token?
Just to add to the answer from Arka Mukherjee above, to get the token I do this:
auth_data = { 'USERNAME':username , 'PASSWORD':password }
provider_client=boto3.client('cognito-idp', region_name=region)
resp = provider_client.admin_initiate_auth(UserPoolId=user_pool_id, AuthFlow='ADMIN_NO_SRP_AUTH', AuthParameters=auth_data, ClientId=client_id)
token = resp['AuthenticationResult']['IdToken']
Here I have to use the username and password of the Cognito user, client_id is the app client id for the app client that I set up thru Cognito, and user_pool_id is the user pool id.
Note that my app client has this option checked/selected: Enable sign-in API for server-based authentication (ADMIN_NO_SRP_AUTH) and I created that app client with no secret key (apparently that is important for web clients especially).
To pass the Cognito User Pool JWT Token, you would need to use the Logins Map in the GetId API call. You could try the following Python code out on your end, after replacing the necessary placeholders.
response = client.get_id(
AccountId='string',
IdentityPoolId='string',
Logins={
'cognito-idp.<region>.amazonaws.com/<YOUR_USER_POOL_ID>': '<JWT ID Token>'
}
)
If you do not provide a Logins Map, Amazon Cognito treats the authentication event as Unauthenticated, and hence, you are facing this error.

Google Admin Directory API - Send a query via apiclient

I am retrieving a ChromeOS device MAC address via the Google Admin Directory API using the device's Serial Number as reference, and am making my calls through
apiclient.
service = discovery.build('admin', 'directory_v1', developerKey=settings.API_KEY)
Here are the calls available for ChromeOS devices; my issue is that I require a Device ID in order to execute the following:
service.chromeosdevices().get(customerId=settings.CID, deviceId=obtained_id, projection=None).execute()
I can send a GET query via the following format:
https://www.googleapis.com/admin/directory/v1/customer/my_customer/devices/chromeos?projection=full&query=id:" + serial + "&orderBy=status&sortOrder=ascending&maxResults=10", "GET")
... but I'm trying to avoid using OAuth2 and just use my API key. Passing the key in a GET request doesn't work either, as it still returns a "Login Required" notice.
How do I squeeze the above query into an apiclient-friendly format? The only option I found via the above calls was to request every device we have (via list), then sift through the mountain of data for the matching Serial number, which seems silly and excessive.
I did notice I could call apiclient.http.HttpRequests, but I couldn't find a way to pass the API key through it either. There's new_batch_http_request, but I can't discern from the docs how to simply pass a URL to it.
Thank you!
Got it!
You can't use just a key for Directory API queries, you need a Service account.
I'm using google-auth (see here) since oauth2client is deprecated.
You also need to:
Delegate the necessary permissions for your service account (mine has the role of Viewer and has scope access to https://www.googleapis.com/auth/admin.directory.device.chromeos.readonly)
Delegate API access to it separately in the Admin Console (Security -> Advanced Settings -> Authentication)
Get your json client secret key and place it with your app (don't include it in your VCS)
Obtain your credentials like this:
credentials = service_account.Credentials.from_service_account_file(
settings.CLIENT_KEY,
scopes=settings.SCOPES,
subject=settings.ADMIN_USER)
where ADMIN_USER is the email address of an authorized Domain admin.
Then you send a GET request like so:
authed_session = AuthorizedSession(credentials)
response = authed_session.get(request_id_url)
This returns a Requests object you can read via response.content.
Hope it helps someone else!

Automating 3-factor authentication through a script for the Lyft Api

I'm trying to use the Lyft rides python API to access Lyft data. Specifically, I'm trying to access the ride estimate endpoint .
from lyft_rides.auth import ClientCredentialGrant
from lyft_rides.session import Session
from lyft_rides.client import LyftRidesClient
auth_flow = ClientCredentialGrant(client_id=MY_ID, client_secret=MY_SECRET, scopes="public")
session = auth_flow.get_session()
client = LyftRidesClient(session)
response = client.get_cost_estimates(start_latitude=start_lat, start_longitude=start_long, end_latitude=end_lat, end_longitude=end_long)
However, the surge rate in the response data is always 0, even during surge hours, and I've diagnosed that it's because I'm not utilizing the 3-legged authentication.
From the lyft developer docs,
3-Legged flow for accessing user-specific endpoints.
To make ride
requests or otherwise access user data, the user must grant you
access. Users who don't have a Lyft account will be prompted to create
a new account if they are directed through the following flow.
From the python docs,
Authorization
If you need access to a Lyft user’s account in order to make requests
on their behalf, you will go through a “3-legged” flow. In this case,
you will need the user to grant access to your application through the
OAuth 2.0 Authorization Code flow. See Lyft API docs.
The Authorization Code flow is a two-step authorization process. The
first step is having the user authorize your app and the second
involves requesting an OAuth 2.0 access token from Lyft. This process
is mandatory if you want to take actions on behalf of a user or access
their information.
from lyft_rides.auth import AuthorizationCodeGrant
auth_flow = AuthorizationCodeGrant(
YOUR_CLIENT_ID,
YOUR_CLIENT_SECRET,
YOUR_PERMISSION_SCOPES,
)
auth_url = auth_flow.get_authorization_url()
Navigate the user to the auth_url where they can grant access to your
application. After, they will be redirected to a redirect_url with the
format REDIRECT_URL?code=UNIQUE_AUTH_CODE. Use this redirect_url to
create a session and start LyftRidesClient.
session = auth_flow.get_session(redirect_url)
client = LyftRidesClient(session)
credentials = session.oauth2credential
Keep credentials information in a secure data store and reuse them to
make API calls on behalf of your user. The SDK will handle the token
refresh for you automatically when it makes API requests with a
LyftRidesClient.
Question
I'm trying to automate the python request within a script. Given that the 3rd leg of the authentication requires manually visiting a url and obtaining a code, is it possible to do this through a script?
[Full Disclosure: I'm one of Lyft's Developer Advocates]
The only way to get that data is by requesting therides.request scope is through the 3-legged OAuth flow (sorry about that). However, you only need to request this external authorization once if you ask for the offline scope as part of the initial authorization. If you have that scope requested initially, you can use refresh_tokens as outlined here and not get prompted for the external URL:
https://developer.lyft.com/docs/authentication#section-step-5-refreshing-the-access-token
If you're only using this script locally I'd recommend going through this authorization once and then building in refresh token logic into your script if your token has expired. Hope that helps!

Passing token to the client

I'm developing an iOS app and using Django for backend. There are two apps I'm using in Django
Django OAuth Toolkit to support OAuth authentication
Python Social Auth to support social authentication
The social authentication process should be:
GET localhost/login/{application}
Authentication on Application site
Redirect to localhost/complete/{application}
Fetch {application}'s access token
Create a new user with my server's access token, and associate it with {application}'s access token
Redirect to localhost/accounts/profile
Then, I can use my server's access token to communicate with {application}.
But the client will see that the browser start with localhost/login/{application} and end with localhost/accounts/profile, and still don't know what is my server's access token, so my question is how to pass the access token to the client?
One solution is that redirect with access token as localhost/accounts/profile?token=MyServerToken, but how to add parameter when redirecting to profile url?
You likely already have what you need on the Django session for your user in question. That is, provided you are using the session middleware (auth of this type is nearly impossible without it), your identity provider specific tokens will usually be populated in the extra_data dict on the SocialUser model for the specific provider in question.
For example, assuming you have a reference to the Django user model (lets call it user):
access_token = user.social_auth.get(provider='google-oauth2').extra_data['access_token']
Unfortunately the specifics will vary depending on the backend you're working with. Remember that these tools are designed to let users authenticate against your app rather than to let you perform arbitrary actions against the product-specific APIs exposed by the various identity providers.
As for passing these tokens to the client, I'd need to know more about your use case. Chances are the identity provider in question set some session cookies on the client during its authentication flow. For example, if you sign in with Facebook, they set a few cookies which are automatically retrieved by the Facebook client-side javascript API. As such, there's no explicit sharing of tokens necessary between the server and the client.
Otherwise, if you must do it yourself, store them on a secure session cookie as follows:
response.set_cookie(social_auth_tokens,
value=your_data_here,
max_age=None, #cookie will expire at end of user session
expires=None,
path='/',
domain=None, #only readable by this domain
secure=True, #only transmitted over https
httponly=False) #readable by scripts running on the page
You should not pass the access token in the query string like /?token=my_token. Its not a secure way and definitely not recommended.
Some other approaches you can use are:
Approach-1: Setting server_access_token in response headers
You can set the access token in the response headers and send it using HTTPS protocol.
The token will be sent once and consumed by the client. Since the response headers are not passed in the subsequent requests, the token will be passed only once to the client. Client will then use it to make further requests by setting the token in the request headers.
class MySocialApplicationRedirectView(View):
def get(self, request, *args, **kwargs):
# Here, write your code to fetch the {application}'s access token,
# creating a new user with your server's access token, and then
# associating it with {application}'s access token
# assign the response to a variable and set the access token as a header in the response
response = HttpResponseRedirect('/accounts/profile/')
response['X-Auth-Token'] = 'my_server_access_token'
# can also use the below name as 'X-' prefixed headers are deprecated
# response['Auth-Token'] = 'my_server_access_token'
return response
Client can then retrieve the token from the headers and use this token to make further requests. In further requests, he must send the access token in request headers.
Approach-2: Setting server_access_token as a cookie
Another option is to set the server_access_token cookie in your response as #Ben mentioned.
response.set_cookie() would set the server_access_token cookie in the response and then the client can read the cookie and send this in further requests in the request headers.
class MySocialApplicationRedirectView(View):
def get(self, request, *args, **kwargs):
# Here, write your code to fetch the {application}'s access token,
# creating a new user with your server's access token, and then
# associating it with {application}'s access token
# assign the response to a variable and set the access token as a cookie in the response object
response = HttpResponseRedirect('/accounts/profile/')
response.set_cookie(key, value='my_server_access_token', ..other parameters )
return response
Note: For safety and security, all requests (both to obtain and use the tokens) must use HTTPS endpoints.
It does not answer your specific question, but I've solved similar problem using TastyPie. It was very straightforward, didn't have to handle more than one application though, but since it provides an API for any given application, shouldn't be a problem.

How to create python test script to access services hosted in GAE which require login

buddies
One of my GAE restful service needs login with admin account. And I'm writing an automation script in python for testing this service. The script simply do a HTTP POST and then check the returned response. The difficult part for me is how to authenticate the test script as an admin user.
I created an admin account for testing purpose. But I'm not sure how to use that account in my test script. Is there a way that my test script can use oath2 or other approach to authenticate itself as a test admin account?
Ok I think this might be what you are looking for, client libraries to authenticate and yeah I believe appengine now recommends using the oauth2 for any kind of authentication:
https://developers.google.com/accounts/docs/OAuth2#libraries
Then you get an auth token where you pass in headers on your restful request like:
# Your authenticated request
Authorization: Bearer TokenHere
Then in your handler you get it like:
try:
user = oauth.get_current_user('https://www.googleapis.com/auth/userinfo')
except NotAllowedError:
user = None
# then from the first link you should be able to access if
user.is_current_user_admin()
This is how I authenticate on android, but I only do this once and store it in session and just enable cookie jar on the httpclient.

Categories