Is it possible to use an Authorization: Bearer … header to make a request through Identity Aware Proxy to my protected application? (Using a service account, of course. From outside GCP.)
I would like to not perform the OIDC token exchange, is this supported?
If so, does anyone have any examples?
So far, I have the following but it doesn't work:
iat = time.time()
exp = iat + 3600
payload = {'iss': account['client_email'],
'sub': account['client_email'],
'aud': '/projects/NNNNN/apps/XXXXXXX',
'iat': iat,
'exp': exp}
additional_headers = {'kid': account['private_key']}
signed_jwt = jwt.encode(payload, account['private_key'], headers=additional_headers,
algorithm='RS256')
signed_jwt = signed_jwt.decode('utf-8')
This produces: Invalid IAP credentials: JWT signature is invalid.
this is not currently supported. IAP is expecting a signature generated by the Google accounts infrastructure using its private key, so that's why the signature check is failing. Could you tell me more about why you'd like to avoid the OIDC token exchange? --Matthew, Google IAP Engineering
Related
I'm trying to retrieve mails from my organization's mailbox, and I can do that via Graph Explorer. However, when I use the same information that I used in Graph Explorer, the generated token returns an error stating '/me request is only valid with delegated authentication flow.' in me/messages endpoint.
So, how can I generate the acceptable token for the /me endpoint?
An example python code or example Postman request would be amazing.
It sounds like the endpoint you're using in Graph Explorer is something like this
https://graph.microsoft.com/v1.0/me/messages
/me is referring to the user signed into Graph Explorer. If you want to read another user's messages you would use
https://graph.microsoft.com/v1.0/users/user#domain.com/messages
When connecting to Graph API as an application with no user interaction, you can never use /me endpoints, as there's no user logged in.
Reference
https://learn.microsoft.com/en-us/graph/api/user-list-messages?view=graph-rest-1.0
Python example to list messages
import requests
def get_messages(access_token, user):
request_url = f"https://graph.microsoft.com/v1.0/users/{user}/messages"
request_headers = {
"Authorization": "Bearer " + access_token
}
result = requests.get(url = request_url, headers = request_headers)
return(result)
msgs = get_messages(access_token = token['access_token'], user = "userPrincipalName#domain.com")
print(msgs.content)
Additional example of obtaining a token, using an app registration and client secret
import msal
def get_token_with_client_secret(client_id, client_secret, tenant_id):
# This function is to obtain a bearer token using the client credentials flow, with a client secret instead of a certificate
# https://docs.microsoft.com/en-us/graph/sdks/choose-authentication-providers?tabs=CS#client-credentials-provider
app = msal.ConfidentialClientApplication(
client_id = client_id,
client_credential = client_secret,
authority = f"https://login.microsoftonline.com/{tenant_id}")
scopes = ["https://graph.microsoft.com/.default"]
token = app.acquire_token_for_client(scopes = scopes)
return(token)
I have a public Cloud Run, authenticated by JWT Token. Working 100%.
The logic inside the Cloud Run to decode the token is in python:
def decode_jwt(token: str) -> dict:
try:
decoded_token = jwt.decode(
token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
return decoded_token if decoded_token["expires"] >= time.time() else None
except Exception as e:
raise InvalidTokenError
The Cloud Run is publicly available using a custom domain.
Now, I want to do some requests to the Cloud Run, using Cloud Tasks (each request have different parameters, created previously by a Cloud Functions).
In the Cloud Tasks, I create each task with a "Bearer {token}" parameter
Cloud Task Headers Code:
task["http_request"]["headers"] = \
{"Authorization": f"Bearer {token}",
"Accept": "application/json"}
First situation:
When I create the task without the "oidc_token" parameter in the http_request creation.
Cloud Run returns "403 Forbidden", and never reach the decode_jwt function inside cloud run.
Cloud Task http_request Code:
task = {
"http_request": {
"http_method": tasks_v2.HttpMethod.POST,
"url": url,
}
}
Second situation:
I add an "oidc_token".
task = {
"http_request": {
"http_method": tasks_v2.HttpMethod.POST,
"url": url,
"oidc_token": {
"service_account_email": "service-task#xxxxx.iam.gserviceaccount.com",
}
}
Now, the request reach the Cloud Run decode_jwt function, and the log in Cloud Run returns "InvalidTokenError".
Extra: I added a logging.info to expose the token received in Cloud Run, and is not the token I passed in the Cloud Task Creation.
Problem Summary:
you have a public (allUsers) Cloud Run service.
you have created your own authorization mechanism (HS256 - HMAC with SHA-256).
you want to assign a custom token for the HTTP Authorization Bearer value.
Cloud Run authorization is managed by IAP.
Authorization for the Cloud Run service is managed by the Identity Aware Proxy (IAP). If you add an HTTP Authorization Bearer token, IAP will verify that token. That step fails for your custom token which results in an HTTP 403 Forbidden error.
Cloud Tasks supports two types of HTTP Authorization Bearer tokens. OAuth Access tokens and OIDC Identity tokens. You cannot use your own token value to replace the supported types.
That leaves you with two options:
Enhance your code to support Google signed OIDC Identity Tokens.
Use a custom HTTP header that supports your custom token format.
Note: I do not recommend using HS256. HS256 is a symmetric algorithm which means the secret must be known to both sides in order to validate the payload. RS256 is an asymmetric algorithm which uses private/public key pairs. To verify only requires the public key. This is one of the strong design features of Google's use of private keys for service accounts and identities. If you switch to Google's method, all of the hard work is done for you.
You have to specificy the audience of your Cloud Run service, like that
task = {
"http_request": { # Specify the type of request.
"http_method": tasks_v2.HttpMethod.POST,
"url": url, # The full url path that the task will be sent to.
"oidc_token": {
"service_account_email": "service-task#xxxxx.iam.gserviceaccount.com",
"audience": base url of Cloud Run, no /sub/path
}
}
that is my first try with an API, said API being called OPS.
I would like to get information using the API (OAuth 2) within my python code.
The ressource URL is :
http://ops.epo.org/3.2/rest-services/register/{publication}/{EPODOC}/{EP2814089}/biblio
I also received :
Consumer Key: O220VlTQqAmodifiedsf0YeqgM6c
Consumer Secret Key: swWmodified3edjORU
The documentation states that:
OPS uses the OAuth framework for Authentication and Authorization. At this point in
time, only the “Client Credentials” flow is supported using a Consumer key and
Consumer secret.
The actual steps to follow are:
Step 1: Client converts Consumer key and Consumer secret to
Base64Encode(Consumer key:Consumer secret).
This should be done programmatically using the language you are developing the client
application in. For the purposes of this example, a public website was used to perform
this conversion.
By entering the colon separated Client credentials, an encoded response is generated.
This response is then be used for basic Authentication.
Step 2: Client requests an access token using Basic Authentication, supplying its
Consumer key and Consumer secret with base64Encoding over encrypted HTTPS
connection:
OPS authenticates the client credentials passed in the Authorization header using basic
authentication method.
If credentials are valid, OPS responds with a valid access token.
Step 3: Client accesses OPS resources with access token in authorization header
(bearer tokens) over encrypted HTTPS connection
I tried a few samples of code with requests but, until now, nothing worked.
The client credentials flow is described in the OAuth2 RFC-6749. The client id and secret are base64 encoded in a Basic authentication scheme as described in RFC-7617
You should be able to get a token using Python code like:
import requests
import base64
url = 'https://ops.epo.org/3.2/auth/accesstoken'
data = {"grant_type": "client_credentials"}
creds = base64.b64encode("O220VlTQqAmodifiedsf0YeqgM6c:swWmodified3edjORU".encode())
headers = {'Authorization': 'Basic ' + creds.decode('UTF-8'), 'Content-Type': 'application/x-www-form-urlencoded'}
response = requests.post(url, headers=headers, data=data)
access_token = response.json()["access_token"]
When using the previous response I can obtain a token. (Thanks a lot for your answer)
So I tried :
myUrl = 'http://ops.epo.org/3.2/rest-services/register/publication/EPODOC/EP2814089/biblio'
header = {'PRIVATE-TOKEN': myToken}
response = requests.get(myUrl, headers=header)
print(response.text)
but I obtained a 403 error.
I finally got a specific library to do the job :
EPO OPS Library
But I still don't know how to do it on my own...
My goal is to reproduce/replicate the functionality of gcloud compute addresses create without depending on the gcloud binary.
I am trying to use python to authenticate a POST to a googleapis compute endpoint per the documentation at https://cloud.google.com/compute/docs/ip-addresses/reserve-static-external-ip-address about reserving a static external ip address
But my POST's return 401 every time.
I have created a JWT from google.auth.jwt python module and when I decode it the JWT has all the strings embedded that I would expect to be there.
I've also tried combinations of the following OAuth scopes to be included in the JWT:
- "https://www.googleapis.com/auth/userinfo.email"
- "https://www.googleapis.com/auth/compute"
- "https://www.googleapis.com/auth/cloud-platform"
this is my function for getting a JWT using the information in my service account's JSON key file
def _generate_jwt( tokenPath, expiry_length=3600 ):
now = int(time.time())
tokenData = load_json_data( tokenPath )
sa_email = tokenData['client_email']
payload = {
'iat': now,
# expires after 'expiry_length' seconds.
"exp": now + expiry_length,
'iss': sa_email,
"scope": " ".join( [
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/compute",
"https://www.googleapis.com/auth/userinfo.email"
] ),
'aud': "https://www.googleapis.com/oauth2/v4/token",
'email': sa_email
}
# sign with keyfile
signer = google.auth.crypt.RSASigner.from_service_account_file( tokenPath )
jwt = google.auth.jwt.encode(signer, payload)
return jwt
once I have the JWT then I make the following post that fails, 401, ::
gapiURL = 'https://www.googleapis.com/compute/v1/projects/' + projectID + '/regions/' + region + '/addresses'
jwtToken = _generate_jwt( servicetoken )
headers = {
'Authorization': 'Bearer {}'.format( jwtToken ),
'content-type' : 'application/json',
}
post = requests.post( url=gapiURL, headers=headers, data=data )
post.raise_for_status()
return post.text
I received a 401 no matter how many combinations of scopes I used in the JWT or permissions I provided to my service account. What am I doing wrong?
edit: many thanks to #JohnHanley for pointing out that I'm missing the next/second POST to https://www.googleapis.com/oauth2/v4/token URL in GCP's auth sequence. So, you get a JWT to get an 'access token.'
I've changed my calls to use the python jwt module rather than the google.auth.jwt module in-combo with the google.auth.crypt.RSASigner. So the code is a bit simpler and I put it in a single method
## serviceAccount auth sequence for google :: JWT -> accessToken
def gke_get_token( serviceKeyDict, expiry_seconds=3600 ):
epoch_time = int(time.time())
# Generate a claim from the service account file.
claim = {
"iss": serviceKeyDict["client_email"],
"scope": " ".join([
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/userinfo.email"
]),
"aud": "https://www.googleapis.com/oauth2/v4/token",
"exp": epoch_time + expiry_seconds,
"iat": epoch_time
}
# Sign claim with JWT.
assertion = jwt.encode( claim, serviceKeyDict["private_key"], algorithm='RS256' ).decode()
data = urllib.urlencode( {
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": assertion
} )
# Request the access token.
result = requests.post(
url="https://www.googleapis.com/oauth2/v4/token",
headers={
"Content-Type": "application/x-www-form-urlencoded"
},
data=data
)
result.raise_for_status()
return loadJsonData(result.text)["access_token"]
In Google Cloud there are three types of "tokens" that grant access:
Signed JWT
Access Token
Identity Token
In your case you created a Signed JWT. A few Google services accept this token. Most do not.
Once you create a Signed JWT, then next step is to call a Google OAuth endpoint and exchange for an Access Token. I wrote an article that describes this in detail:
Google Cloud – Creating OAuth Access Tokens for REST API Calls
Some Google services now accept Identity Tokens. This is called Identity Based Access Control (IBAC). This does not apply to your question but is the trend for the future in Google Cloud Authorization. An example is my article on Cloud Run + Cloud Storage + KMS:
Google Cloud – Go – Identity Based Access Control
The following example Python code shows how to exchange tokens:
def exchangeJwtForAccessToken(signed_jwt):
'''
This function takes a Signed JWT and exchanges it for a Google OAuth Access Token
'''
auth_url = "https://www.googleapis.com/oauth2/v4/token"
params = {
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": signed_jwt
}
r = requests.post(auth_url, data=params)
if r.ok:
return(r.json()['access_token'], '')
return None, r.text
I am developing a Flask application which gives call to the REST service developed in Flask. The target REST service method is secured using Basic Authentication. I found that for this type of authentication, I have to use base64 encoding.
I am trying to pass the credentials to the service in this way:
headers = {'username': base64.b64encode(g.user['username'])}
response = requests.post('http://127.0.0.1:3000/api/v1.0/follower/' + username, headers=headers)
And at the service side, the username is fetched as :
user_name = request.authorization.username
However, the service is not able to authorize the provided credentials and it is throwing an error 401.
Is there any issue with the authorization at the service side and at the application side?
You are not creating a proper Basic Authorization header.
You'd have to call the header Authorization, and then set the header value to the string Basic <base64-of-username-and-password-separated-by-a-colon>.
If we assume an empty password, that would look like:
headers = {
'Authorization': 'Basic {}'.format(
base64.b64encode(
'{username}:{password}'.format(
username=g.user['username'],
password='')
)
),
}
See the Wikipedia description of the client side of the protocol.
However, there is no need to construct this manually, as requests will create the header for you when you pass in a username and password as a tuple to the auth keyword:
response = requests.post(
'http://127.0.0.1:3000/api/v1.0/follower/' + username,
auth=(g.user['username'], ''))
for me the working code was, but may have some error.
headers = {
'Authorization': 'Basic {}'.format(
base64.b64encode(
'{username}:{password}'.format(
username=g.user['username'],
password='').encode()
).decode()
)
}