I have an application that must verify the identity of its caller. For this I need ID tokens (the JWTs from Google's OpenID Connect implementation) from each client calling the service.
I would like to write client code that works both locally using the default user credentials—for testing and development—and on a Compute Engine instance in production. The official Python auth SDK generally does a good job of handling those cases and saving me the trouble of checking the environment, e.g. I can just call google.auth.default and it figures out where to get credentials.
However, that google.auth package only seems to be able to give me auth tokens, not ID tokens in an environment-independent way. Here is what I tried:
import google.auth
from google.auth.transport import requests
credentials, project = google.auth.default(scopes=["openid"])
req = requests.Request()
credentials.refresh(req)
print(credentials.id_token)
This works on my laptop with my default credentials, but on the Compute Engine instance I instead get an error AttributeError: 'Credentials' object has no attribute 'id_token'
According to this page in the docs, you are supposed to fetch an ID token for an instance by requesting it from the metadata server...
import requests
audience = 'service_identifier'
metadata_server_token_url = 'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience='
token_request_url = metadata_server_token_url + audience
token_request_headers = {'Metadata-Flavor': 'Google'}
token_response = requests.get(token_request_url, headers=token_request_headers)
jwt = token_response.content.decode("utf-8")
print(jwt)
I don't want to do that. I don't want to manually check the environment. The SDK is supposed to handle that complexity for me. Shouldn't there be a way to leverage the google-auth SDK to generate an ID token in an environment-independent way?
EDIT 1: Why I need this
The application is based on Cloud Functions, and it returns highly sensitive data. Only a specific set of subjects—some trusted devs and services—should be able to access that data. So, the cloud functions must verify the ID of the caller (user or service account) using an ID token signed by Google. Specifically, I need to know the sub claim, the "subject" in the JWT. It is effectively the same issue that the IAM features are meant to solve, documented here. However, I cannot use these because they are still in beta. So, I'm writing identity checks manually into the cloud functions for the time being.
I think I have an answer to this question. I could always get it to work locally or in the cloud, the trick was to find a way of combining the two. I colleague of mine actually showed me how to do this and I just wanted to share this with others who are looking for a solution.
import google.auth
from google.auth.transport.requests import AuthorizedSession, Request
from google.oauth2.id_token import fetch_id_token
import requests
def GetIdToken(audience):
credentials, _ = google.auth.default()
session = AuthorizedSession(credentials)
request = Request(session)
credentials.refresh(request)
if hasattr(credentials, "id_token"):
return credentials.id_token
return fetch_id_token(request, audience)
def ProcessPayload(url, payload):
# Get the ID Token
id_token = GetIdToken(url)
# Process post request
headers = {'Authorization': f'Bearer {id_token}'}
response = requests.post(url, json=payload, headers=headers)
Related
Is there a way to programmatically access the email of the currently used Service Account on a GCP instance when no GOOGLE_APPLICATION_CREDENTIALS is set? (ie. when using the default Service Account)
I've looked through the GCP documentation, but the only resource I found can't be used with the default Service Account when no GOOGLE_APPLICATION_CREDENTIALS is set. I know that it is possible to do so using gcloud (see this SO question or documentation), however these solutions aren't applicable when running on a ContainerOptimisedOS. I've spent a couple of weeks back and forth with the GCP support team, but they concluded with not being able to help me and redirected me to Stack Overflow for help.
The solution of John works great, on any language without any external library. However, it works only on Google Cloud environment, when a metadata server is deployed. You can't perform this test on your computer.
I propose just bellow a piece of Python code (with Google OAuth library, but it works in other languages that have this library) to ask the library the current credential. If the credential is a service account (from GOOGLE_APPLICATION_CREDENTIALS on your computer, the ADC (Application Default Credential) or from the metadata server), you have the email printed, else, you have warning message because you use your user account credential
import google.auth
credentials, project_id = google.auth.default()
if hasattr(credentials, "service_account_email"):
print(credentials.service_account_email)
else:
print("WARNING: no service account credential. User account credential?")
Note that if the default service account is used this method will print default instead of the entire email address.
EDIT 1
ctx := context.Background()
credential,err := google.FindDefaultCredentials(ctx)
content := map[string]interface{}{}
json.Unmarshal(credential.JSON,&content)
if content["client_email"] != nil {
fmt.Println(content["client_email"])
} else {
fmt.Println("WARNING: no service account credential. User account credential?")
}
Just adding to the accepted answer. As stated in the answer this seems to return "default":
import google.auth
credentials, project_id = google.auth.default()
# returns "default"
print(credentials.service_account_email)
I found to get the email name of the GSA currently active (via the metadata api) I had to manually refresh first:
import google.auth
import google.auth.transport.requests
credentials, project_id = google.auth.default()
request = google.auth.transport.requests.Request()
credentials.refresh(request=request)
# returns "mygsa#myproject.iam.gserviceaccount.com"
print(credentials.service_account_email)
I'm using workload ID. I think maybe there is a race condition and I'm trying to read the service_account_email property before the creds get initialized for the first time when the pod starts?
The _retrieve_info() function is called when refresh() is called and it appears this is the function that grabs the email name.
If I had my script sleep for a few seconds on start up would service_account_email eventually be populated with the email name of the GSA or does that not happen until you manually refresh or initialize a client API or something?
If you are interested in getting the exact email and not just the "default" alias (when you are using the compute engine), you can fetch it using the credentials metadata. This was particularly helpful in determining which service account is being used by AI Platform jobs.
import google.auth
from google.auth.transport.requests import Request
from google.auth.compute_engine import _metadata
if hasattr(credentials, "service_account_email"):
print(credentials.service_account_email)
# edits made on top of the top answer's code
info = _metadata.get_service_account_info(Request(),
service_account=credentials.service_account_email)
print(f"Service account email: {info.email}")
print(f"Service account aliases: {info.aliases}")
print(f"Service account scopes: {info.scopes}")
else:
print("WARNING: no service account credentials available.")
Quick Overview: I have a ruby app that runs nightly and does something with a user's google calendar. The user has already given access via a separate react app. I'm having trouble getting the ruby app to access the user's calendar with the authorization code from the react app.
Details: I have a React front-end that can sign in a user using gapi and subsequently sign the user into Firebase. Here is how I configure the gapi obj:
this.auth2 = await loadAuth2WithProps({
apiKey: config.apiKey, // from firebase
clientId: config.clientId, // from gcp
// ....
access_type: "offline", // so we get get authorization code
})
Here is sign in:
doSignInWithGoogle = async () => {
const googleUser = await this.auth2.signIn();
const token = googleUser.getAuthResponse().id_token;
const credential = app.auth.GoogleAuthProvider.credential(token);
return this.auth.signInWithCredential(credential);
};
The user's next step is to grant the app offline access to their calendar:
doConnectGoogleCalendar = async () => {
const params = {scope:scopes};
const result = await this.auth2.grantOfflineAccess(params);
console.log(result.code); // logs: "4/ygFsjdK....."
};
At this point the front end has the authorization code that can be passed to a server-side application to be exchanged for access and refresh tokens. I haven't been able to find a good way to use a user supplied auth-code to make calls to available scopes. This is how I've configured the oauth client:
auth_client = Google::APIClient::ClientSecrets.load(
File.join(Rails.root,'config','client_secrets.json') // downloaded from GCP
).to_authorization
^ I'm using the same GCP Credentials on the backend that I'm using for the frontend. It is a "OAuth 2.0 Client ID" type of credential. I'm unsure if this is good practice or not. Also, do I need to define the same config that I do on the frontend (like access_type and scope)?.
Next I do what the docs say to get the access and refresh tokens(click Ruby):
auth_client.code = authorization_code_from_frontend
auth_client.fetch_access_token!
---------
Signet::AuthorizationError (Authorization failed. Server message:)
{
"error": "invalid_grant",
"error_description": "Bad Request"
}
Is there something I'm missing in setting up a separate backend application that can handle offline access to a user granted scope? There is so much different information on these libraries but I haven't been able to distill it down to something that works.
UPDATE
I found this page describing the "one-time-code flow" which I haven't found anywhere else is all of the docs I've gone through. It does answer one of my minor questions above: Yes, you can use the same client secrets as the web application for the backend. (see the full example at the bottom where they do just that). I'll explore it more and see if my bigger problem can be resolved. Also going to update the title to include one-time-code flow.
After a good amount of digging through code samples and source code, I have a clean working solution. Once I found the page in my "update" it led me to finding out that ClientSecrets way I was doing things had been deprecated in favor of the google-auth-library-ruby project. I'm glad I found it because it seems to be a more complete solution as it handles all of the token management for you. Here is code to setup everything:
def authorizer
client_secrets_path = File.join(Rails.root,'config','client_secrets.json')
client_id = Google::Auth::ClientId.from_file(client_secrets_path)
scope = [Google::Apis::CalendarV3::AUTH_CALENDAR_READONLY]
redis = Redis.new(url: Rails.application.secrets.redis_url)
token_store = Google::Auth::Stores::RedisTokenStore.new(redis: redis)
Google::Auth::WebUserAuthorizer.new(client_id, scope, token_store, "postmessage")
end
and then this is how I use the authorization code:
def exchange_for_token(user_id,auth_code)
credentials_opts = {user_id:user_id,code:auth_code}
credentials = authorizer.get_and_store_credentials_from_code(credentials_opts)
end
after calling that method the library will store the exchanged tokens in Redis (you can configure where to store) for later use like this:
def run_job(user_id)
credentials = authorizer.get_credentials(user_id)
service = Google::Apis::CalendarV3::CalendarService.new
service.authorization = credentials
calendar_list = service.list_calendar_lists.items
# ... do more things ...
end
There is so much info out there that it is difficult to isolate what applies to each condition. Hopefully this helps anyone else that gets stuck with the "one-time-code flow" so they don't spend days banging their head on their desk.
My Current Setup
Google Cloud Endpoints hosted on Google App Engine.
Google Echo Tutorial (https://cloud.google.com/endpoints/docs/frameworks/python/get-started-frameworks-python)
Python local server making requests to the echo API.
The echo tutorial is up and running. I can make calls to open endpoints and the one requiring an API key using a python script on my machine. I have not been able to make an authorized API call with a Google ID token. None of the Google examples have worked so far.
From my understanding, the workflow should be
Use a key file to authorize the service account to generate a JWT.
Use the JWT to generate a Google ID token.
Google Example: https://cloud.google.com/endpoints/docs/openapi/service-account-authentication#using_a_google_id_token (Key File)
The code fails. Function get_id_token() return res['id_token'] fails with no id_token in res.
Has anyone gotten the example to work? Does anyone have an example of making an authorized API call to an Endpoint API with a Google ID token from a service account?
The main issue was generating the JWT and the code that works for me is below. I have yet to find a better way to do this that works. If you know of a better way please submit your answers below or add a comment. The code that generates the Google ID Token from JWT is exactly from Google documentation here (https://github.com/GoogleCloudPlatform/python-docs-samples/blob/master/endpoints/getting-started/clients/service_to_service_google_id_token/main.py) get_id_token function.
def generate_jwt(audience, json_keyfile, service_account_email):
"""Generates a signed JSON Web Token using a Google API Service Account.
https://github.com/GoogleCloudPlatform/python-docs-samples/blob/master/endpoints/getting-started/clients/google-jwt-client.py
"""
# Note: this sample shows how to manually create the JWT for the purposes
# of showing how the authentication works, but you can use
# google.auth.jwt.Credentials to automatically create the JWT.
# http://google-auth.readthedocs.io/en/latest/reference/google.auth.jwt.html#google.auth.jwt.Credentials
signer = google.auth.crypt.RSASigner.from_service_account_file(json_keyfile)
now = int(time.time())
expires = now + 3600 # One hour in seconds
payload = {
'iat': now,
'exp': expires,
'aud': 'https://www.googleapis.com/oauth2/v4/token',
# target_audience must match 'audience' in the security configuration in your
# openapi spec. It can be any string.
'target_audience': audience,
'iss': service_account_email
}
jwt = google.auth.jwt.encode(signer, payload)
return jwt
Let's say I have an AngularJS application that consumes the REST API of a Django application.
The Django application has got a built-in OAuth2 provider that can be called to retrieve an access token and use the protected endpoints of the API. This provider is using django-oauth-toolkit.
Let's assume there is a registered client with "password" grant type, so that the end users only need to provide their credentials in the front-end in order to get an access token from the back-end.
At some point we want to add some support for social networks login and we decide to use python-social-auth (PSA) to that end. Here is the workflow I want to achieve:
The user logs in on Facebook from the front-end (via the Facebook SDK) and we get an access token back from the OAuth2 provider of Facebook.
We send the Facebook token to an endpoint of our REST API. This endpoint uses the Facebook token and django-social-auth to authenticate the user in our Django application (basically matching a Facebook account to a standard account within the app).
If the authentication succeeds, the API endpoint requests an access token from the OAuth2 provider for this newly authenticated user.
The Django access token is sent back to the front-end and can be used to access the REST API in exactly the same way that a regular user (i.e. logged in with his credentials) would do.
Now my problem is: how do I achieve step 3? I first thought I would register a separate OAuth2 client with Client Credentials Grant but then the generated token is not user-specific so it does not make sense. Another option is to use the TokenAuthentication from DRF but that would add too much complexity to my project. I already have an OAuth server and I don't want to set up a second token provider to circumvent my problem, unless this is the only solution.
I think my understanding of PSA and django-oauth-toolkit is not deep enough to find the best way of reaching my goal, but there must be a way. Help!
I managed to get something working using urllib2. I can't speak towards whether or not this is good practice, but I can successfully generate an OAuth2 token within a view.
Normally when I'd generate an access token with cURL, it'd look like this:
curl -X POST -d "grant_type=password&username=<user_name>&password=<password>" -u"<client_id>:<client_secret>" http://localhost:8000/o/token/
So we're tasked with making urllib2 accomplish this. After playing around for some bit, it is fairly straightforward.
import urllib, urlib2, base64, json
# Housekeeping
token_url = 'http://localhost:8000/auth/token/'
data = urllib.urlencode({'grant_type':'password', 'username':<username>, 'password':<password>})
authentication = base64.b64encode('%s:%s' % (<client_id>, <client_secret>))
# Down to Business
request = urllib2.Request(token_url, data)
request.add_header("Authorization", "Basic %s" % authentication)
access_credentials = urllib2.urlopen(request)
json_credentials = json.load(access_credentials)
I reiterate, I do not know if this is in bad practice and I have not looked into whether or not this causes any issues with Django. AFAIK this will do this trick (as it did for me).
I'm having difficulties using Google new Admin SDK. In particular the Directory API using Oauth2.
I think I'm almost there but I've got stuck trying to retrieve a users details using the Directory API (I'm using a Google Education Edition domain).
Basically what I'm trying to do is write a python script that provisions or de-provisions users based on their enrollment status which is managed by our AD. I've got a script that does this using Oauth1 but want to update it to use Oauth2.
Here is a code snippet based on some examples I found.
f = file('test_key.p12', 'rb')
key = f.read()
f.close()
credentials = SignedJwtAssertionCredentials(
'606346240424-10stfco1ukj9h4m4b4r40#developer.gserviceaccount.com',
key,
scope= 'https://www.googleapis.com/auth/admin.directory.user')
http = httplib2.Http()
http = credentials.authorize(http)
service = build(serviceName='admin', version='directory_v1', http=http)
lists = service.users().get(userKey='joe.blogs#mydomain.com').execute(http=http)
pprint.pprint(lists)
This piece of code appears to connect correctly but when I try to execute the query I get a 403 error.
ERROR: https://www.googleapis.com/admin/directory/v1/users/joe.blogs#mydomain.com?alt=json returned "Not Authorized to access this resource/api">
My first thought was because I haven't turned on this API on the administrators console (Google API's console) but I have. (Actually I turned on the Admin SDK and not the Directory API because there is no Directory API to turn on and seeing that it's part of the Admin SDK it would work?).
Is there another step I'm missing or have I made a silly mistake somewhere?
Bruce,
you're pretty close.
Couple of items:
If you're using App Engine, need to convert p12 key to pem and strip header
Need to include user with super user credentials (who has permission to do these operations) whom you're impersonating (not the user who is being changed) using the sub= parameter
So full code will look a bit like this:
# domain configuration settings
import domainconfig
f = file(domainconfig.KEY_FILE, "rb") # b reads file in binary mode; not strictly necessary, but safer to avoid strange Windows EOL characters: https://stackoverflow.com/questions/9644110/difference-between-parsing-a-text-file-in-r-and-rb-mode
key = f.read()
f.close()
credentials = SignedJwtAssertionCredentials(
domainconfig.SERVICE_ACCOUNT_EMAIL,
key,
scope = domainconfig.SCOPE,
sub=domainconfig.SUB_ACCOUNT_EMAIL # 'sub' supercedes the deprecated 'prn'
)
http = httplib2.Http()
http = credentials.authorize(http)
directoryservice = build("admin", "directory_v1", http=http)
users = directoryservice.users()
response = users.get(userKey='joe.blogs#mydomain.com').execute()
This should be of help: https://developers.google.com/drive/delegation
When asserting the credentials you need to connect it to the user that is going to be changed. From the link above, note this section:
credentials = SignedJwtAssertionCredentials(SERVICE_ACCOUNT_EMAIL, key,
scope='https://www.googleapis.com/auth/drive', sub=user_email)