I'm trying to write an API in Google App Engine Standard using Endpoints Framework.
I followed the tutorial at https://cloud.google.com/endpoints/docs/frameworks/python/get-started-frameworks-python. "Opened" methods works fine, but I have problems when I want to secure the API with Firebase authentication. Following the tutorial at https://cloud.google.com/endpoints/docs/frameworks/python/authenticating-users, I wrote the following code:
import endpoints
from endpoints import message_types
from endpoints import messages
from endpoints import remote
class EchoRequest(messages.Message):
message = messages.StringField(1)
class EchoResponse(messages.Message):
"""A proto Message that contains a simple string field."""
message = messages.StringField(1)
ECHO_RESOURCE = endpoints.ResourceContainer(
EchoRequest,
n=messages.IntegerField(2, default=1))
#endpoints.api(
name='echo',
version='v1',
issuers={'firebase': endpoints.Issuer(
'https://securetoken.google.com/project_name',
'https://www.googleapis.com/service_accounts/v1/metadata/x509/securetoken#system.gserviceaccount.com')})
class EchoApi(remote.Service):
#endpoints.method(
# This method takes a ResourceContainer defined above.
ECHO_RESOURCE,
# This method returns an Echo message.
EchoResponse,
path='test_firebase',
http_method='POST',
name='test_firebase')
def test_firebase(self, request):
user = endpoints.get_current_user()
# If there's no user defined, the request was unauthenticated, so we
# raise 401 Unauthorized.
if not user:
raise endpoints.UnauthorizedException
output_message = ' '.join([request.message] * 3)
return EchoResponse(message=output_message)
When I try this method using the following curl:
TOKEN="token_string"
curl -i \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-X POST -d '{"message":"hello world"}' https://project_name.appspot.com/_ah/api/echo/v1/test_firebase
I have the following error:
HTTP/2 401
content-type: application/json
x-cloud-trace-context: ffd02c47eb5885d6a93c31ac8aae26cc;o=1
date: Fri, 20 Dec 2019 17:55:57 GMT
server: Google Frontend
content-length: 150
alt-svc: quic=":443"; ma=2592000; v="46,43",h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000
{
"error": {
"code": 401,
"errors": [
{
"domain": "global",
"message": "",
"reason": "required"
}
],
"message": ""
}
}
Where is the error?
(From alessandromercadante#'s comment, which does NOT work for me. Since it works for some and there is no answer yet, I copied it here to keep the thread going)
In the signature of the method, the keyword audiences must be passed:
#endpoints.method(
ECHO_RESOURCE,
EchoResponse,
path='test_firebase',
audiences={ "firebase": [project_id]},
http_method='POST',
name='test_firebase')
def test_firebase(self, request):
user = endpoints.get_current_user()
if not user:
raise endpoints.UnauthorizedException
Related
I am trying to programmatically access an IAP-protected App Engine Standard app via Python from outside of the GCP environment.
I have tried various methods, including the method shown in the docs here: https://cloud.google.com/iap/docs/authentication-howto#iap-make-request-python. Here is my code:
from google.auth.transport.requests import Request
from google.oauth2 import id_token
import requests
def make_iap_request(url, client_id, method='GET', **kwargs):
"""Makes a request to an application protected by Identity-Aware Proxy.
Args:
url: The Identity-Aware Proxy-protected URL to fetch.
client_id: The client ID used by Identity-Aware Proxy.
method: The request method to use
('GET', 'OPTIONS', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE')
**kwargs: Any of the parameters defined for the request function:
https://github.com/requests/requests/blob/master/requests/api.py
If no timeout is provided, it is set to 90 by default.
Returns:
The page body, or raises an exception if the page couldn't be retrieved.
"""
# Set the default timeout, if missing
if 'timeout' not in kwargs:
kwargs['timeout'] = 90
# Obtain an OpenID Connect (OIDC) token from metadata server or using service
# account.
open_id_connect_token = id_token.fetch_id_token(Request(), client_id)
print(f'{open_id_connect_token=}')
# Fetch the Identity-Aware Proxy-protected URL, including an
# Authorization header containing "Bearer " followed by a
# Google-issued OpenID Connect token for the service account.
resp = requests.request(
method, url,
headers={'Authorization': 'Bearer {}'.format(
open_id_connect_token)}, **kwargs)
print(f'{resp=}')
if resp.status_code == 403:
raise Exception('Service account does not have permission to '
'access the IAP-protected application.')
elif resp.status_code != 200:
raise Exception(
'Bad response from application: {!r} / {!r} / {!r}'.format(
resp.status_code, resp.headers, resp.text))
else:
return resp.text
if __name__ == '__main__':
res = make_iap_request(
'https://MYAPP.ue.r.appspot.com/',
'Client ID from IAP>App Engine app>Edit OAuth Client>Client ID'
)
print(res)
When I run it locally, I have the GOOGLE_APPLICATION_CREDENTIALS environment variable set to a local JSON credential file containing the keys for the service account I want to use. I have also tried running this in Cloud Functions so it would presumably use the metadata service to pick up the App Engine default service account (I think?).
In both cases, I am able to generate a token that appears valid. Using jwt.io, I see that it contains the expected data and the signature is valid. However, when I make a request to the app using the token, I always get this exception:
Bad response from application: 401 / {'X-Goog-IAP-Generated-Response': 'true', 'Date': 'Tue, 09 Feb 2021 19:25:43 GMT', 'Content-Type': 'text/html', 'Server': 'Google Frontend', 'Content-Length': '47', 'Alt-Svc': 'h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"'} / 'Invalid GCIP ID token: JWT signature is invalid'
What could I be doing wrong?
The solution to this problem is to exchange the Google Identity Token for an Identity Platform Identity Token.
The reason for the error Invalid GCIP ID token: JWT signature is invalid is caused by using a Google Identity Token which is signed by a Google RSA private key and not by a Google Identity Platform RSA private key. I overlooked GCIP in the error message, which would have told me the solution once we validated that the token was not corrupted in use.
In the question, this line of code fetches the Google Identity Token:
open_id_connect_token = id_token.fetch_id_token(Request(), client_id)
The above line of code requires that Google Cloud Application Default Credentials are setup. Example: set GOOGLE_APPLICATION_CREDENTIALS=c:\config\service-account.json
The next step is to exchange this token for an Identity Platform token:
def exchange_google_id_token_for_gcip_id_token(google_open_id_connect_token):
SIGN_IN_WITH_IDP_API = 'https://identitytoolkit.googleapis.com/v1/accounts:signInWithIdp'
API_KEY = '';
url = SIGN_IN_WITH_IDP_API + '?key=' + API_KEY;
data={
'requestUri': 'http://localhost',
'returnSecureToken': True,
'postBody':'id_token=' + google_open_id_connect_token + '&providerId=google.com'}
try:
resp = requests.post(url, data)
res = resp.json()
if 'error' in res:
print("Error: {}".format(res['error']['message']))
exit(1)
# print(res)
return res['idToken']
except Exception as ex:
print("Exception: {}".format(ex))
exit(1)
The API Key can be found in the Google Cloud Console -> Identity Platform. Top right "Application Setup Details". This will show the apiKey and authDomain.
More information can be found at this link:
Exchanging a Google token for an Identity Platform token
I am using goodle gmail send API for sending the mails.
POST https://www.googleapis.com/gmail/v1/users/me/messages/send?key=[My_API_KEY]
Authorization: Bearer [ACCESS_TOKEN]
Accept: application/json
Content-Type: application/json
{
"raw": "SGkgVGVhbSwKVGVzdGluZyBFbWFpbCBBdXRoZW50aWNhdGlvbgoKCgoK"
}
When I am executing this I am getting an error like:
{
"error": {
"errors": [
{
"domain": "global",
"reason": "forbidden",
"message": "Delegation denied for mymailaddress#example.com"
}
],
"code": 403,
"message": "Delegation denied for mymailaddress#example.com"
}
}
Could anyone help me on this.
Thank you in advance.
Try these-
1. best thing to do is to just always have ' userId="me" ' in your requests. That tells the API to just use the authenticated user's mailbox--no need to rely on email addresses.
2. The access token and other parameters present in JSON are not associated with new email id/account. So, in order make it run you just have to delete the '.credentails' folder and run the program again. Now, the program opens the browser and asks you to give permissions.
To delete the folder containing files in python
import shutil
shutil.rmtree("path of the folder to be deleted")
you may add this at the end of the program
I'm trying to lease an app engine task from a pull queue in a compute engine instance but it keeps giving this error:
{
"error": {
"errors": [
{
"domain": "global",
"reason": "forbidden",
"message": "you are not allowed to make this api call"
}
],
"code": 403,
"message": "you are not allowed to make this api call"
}
}
This is the code I'm using:
import httplib2, json, urllib
from oauth2client.client import AccessTokenCredentials
from apiclient.discovery import build
def FetchToken():
METADATA_SERVER = ('http://metadata/computeMetadata/v1/instance/service-accounts')
SERVICE_ACCOUNT = 'default'
http = httplib2.Http()
token_uri = '%s/%s/token' % (METADATA_SERVER, SERVICE_ACCOUNT)
resp, content = http.request(token_uri, method='GET',
body=None,
headers={'Metadata-Flavor': 'Google'})
print token_uri
print content
if resp.status == 200:
d = json.loads(content)
access_token = d['access_token'] # Save the access token
credentials = AccessTokenCredentials(d['access_token'],
'my-user-agent/1.0')
autho = credentials.authorize(http)
print autho
return autho
else:
print resp.status
task_api = build('taskqueue', 'v1beta2')
lease_req = task_api.tasks().lease(project='project-name',
taskqueue='pull-queue',
leaseSecs=30,
numTasks=1)
result = lease_req.execute(http=FetchToken()) ####ERRORS HERE
item = result.items[0]
print item['payload']
It seems like an authentication issue but it gives me the exact same error if I do the same lease request using a bullshit made-up project name so I can't be sure.
I also launched the instance with taskqueue enabled.
Any help would be greatly appreciated
In case anyone else is stuck on a problem like this I'll explain how it's working now.
Firstly I'm using a different (shorter) method of authentication:
from oauth2client import gce
credentials = gce.AppAssertionCredentials('')
http = httplib2.Http()
http=credentials.authorize(http)
credentials.refresh(http)
service = build('taskqueue', 'v1beta2', http=http)
Secondly, the reason my lease request was being denied is that in queue.yaml my service account email was set as a writer email. In the documentation it's mentioned that an email ending with #gmail.com will not have the rights of a user email when set as a writer email. It's not mentioned that that extends to emails ending with #developer.gserviceaccount.com.
In the Cloud Endpoints documentation for exception handling, it is recommended to subclass the endpoints.ServiceException class to provide a custom http_status for 409 Conflict errors. This answer to another question indicates that some of the supported status codes get mapped by Google's infrastructure to other status codes, but 409 isn't one of the mapped status codes.
Using the ConflictException class from the documentation:
import endpoints
import httplib
class ConflictException(endpoints.ServiceException):
"""Conflict exception that is mapped to a 409 response."""
http_status = httplib.CONFLICT
When I raise the ConflictException:
#endpoints.method(request_message=apimodels.ClientMessage,
response_message=apimodels.ClientMessage,
name='insert',
path='/clients',
http_method='POST'
)
def insert(self, request):
client = models.Client.get_by_id(request.client_code)
if client:
raise ConflictException('Entity with the id "%s" exists.' % request.client_code)
...
I'm getting a 400 Bad Request as the response:
400 Bad Request
Content-Length: 220
Content-Type: application/json
Date: Thu, 27 Feb 2014 16:11:36 GMT
Server: Development/2.0
{
"error": {
"code": 400,
"errors": [
{
"domain": "global",
"message": "Entity with the id \"FOO\" exists.",
"reason": "badRequest"
}
],
"message": "Entity with the id \"FOO\" exists."
}
}
I'm getting the same 400 response code on both the local dev_appserver and deployed to App Engine (on 1.9.0). Stepping into the App Engine ProtoRPC code, the following line appears to be mapping all remote.ApplicationError types to a 400 status code.
If I update the endpoints.apiserving._ERROR_NAME_MAP dict to add my custom ConflictException class, I'm able to return a 409 successfully:
import endpoints
import httplib
from endpoints.apiserving import _ERROR_NAME_MAP
class ConflictException(endpoints.ServiceException):
"""Conflict exception that is mapped to a 409 response."""
http_status = httplib.CONFLICT
_ERROR_NAME_MAP[httplib.responses[ConflictException.http_status]] = ConflictException
Is this the correct way to implement endpoints.ServiceException subclasses?
It seems to be a bug as according to the bug report filed by Chris.
I'm creating an API using Google Cloud Endpoints where I would like to return a "no content" HTTP 204 response if there's nothing to return. I tried returning null, which throws an error on the development server, and a non-empty result on production with status code 200.
It is possible to send out a true 204 empty response or other types or custom responses?
To return a 204 No Content for a production Python Cloud Endpoints API, you can use VoidMessage.
from google.appengine.ext import endpoints
from protorpc import messages
from protorpc import message_types
from protorpc import remote
class MyMessage(messages.Message):
...
#endpoints.api('someapi', 'v1', 'Description')
class MyApi(remote.Service):
#endpoints.method(MyMessage, message_types.VoidMessage,
...)
def my_method(self, request):
...
return message_types.VoidMessage()
This currently returns a 200 on the development server, thanks for finding this bug!
This probably doesn't help, but the only way I know to manipulate the status code is by raising an exception. There are a default set of exceptions provided which map to 400, 401, 403, 404 and 500. The docs say you can subclass endpoints.ServiceException to generate other status codes, however I haven't been able to get this to work. If you set http_status to anything other than one of those listed above, it always results in a 400.
class TestException(endpoints.ServiceException):
http_status = httplib.NO_CONTENT
I run a test in my handler like this:
raise TestException('The status should be 204')
And I see this output when testing it using the API explorer:
400 Bad Request
- Show headers -
{
"error": {
"errors": [
{
"domain": "global",
"reason": "badRequest",
"message": "The status should be 204"
}
],
"code": 400,
"message": "The status should be 204"
}
}