Google App Engine inter module communication authorization (python) - python

Citing Google App Engine inter module communication authorization the problem I have is that in the Docs (communication between modules) says:
You can configure any manual or basic scaling module to accept
requests from other modules in your app by restricting its handler to
only allow administrator accounts, specifying login: admin for the
appropriate handler in the module's configuration file. With this
restriction in place, any URLFetch from any other module in the app
will be automatically authenticated by App Engine, and any request
that is not from the application will be rejected.
And this is exactly the configuration I have for my module called "api1". In my app.yaml file I have:
# can accept requests from other modules.
# with login: admin and they are authenticated automatically.
- url: /.*
script: _go_app
login: admin
I'm trying now, from a different module in the same app, to make a service call as suggested in the doc using urfetch.fetch() method, and my implementation is:
from google.appengine.api import urlfetch, modules, app_identity
from rest_framework.response import Response, status
#api_view(['POST'])
def validate_email(request):
url = "http://%s/" % modules.get_hostname(module="api1")
payload = json.dumps({"SOME_KEY":"SOME_VALUE"})
appid = app_identity.get_application_id()
result = urlfetch.fetch(url + "emails/validate/document",
follow_redirects=False,
method=urlfetch.POST,
payload=payload,
headers={"Content-Type":"application/json")
return Response({
'status_code': result.status_code,
'content': result.content
}, status=status.HTTP_200_OK)
According to the documentation, having specified the follow_redirects=False, fetch() will automatically insert an header in my call (I've even tried to add it explicitly) with the "X-Appengine-Inbound-Appid" : MY-APP-ID.
Unfortunately I get as result of the fetch call a 302 redirect, if I follow it, it's a redirect to the authentication form. This occurs in Development server as well as in Production.
Can you please let me know how can I call my api1 service inside my validate_email method (belonging to a different module in the same app)?
Is there another way to authenticate the call since it seems the way suggested inside the documentation is not working?
Thank you

As written here this is a tracked issue now on google appengine public issue tracker. So everyone can go there to check for updates.
In the meanwhile I solved the issue removing the login: admin from the app.yaml and in the handler of my service I've checked manually for the existence of the header X-Appengine-Inbound-Appid and its value.

Related

How to change the Redirect URI from HTTP to HTTPS within a FastAPI web app using fastapi_msal?

I am trying to setup Azure AD authentication for a web application using FastAPI. I am using the fastapi_msal python package to do this. The problem I am having is that when I go to the web app, I am able to login, but once i am authenticated, it says the redirect URI that the application is using begins with HTTP. However, Azure requires the redirect uri begin with HTTPS unless running the app locally. Does anyone know how I can change the redirect uri to begin with https instead?
The code for my project pretty much exactly resembles the code from this example project here. However, I have found a similar project using Flask instead of FastAPI. And there is a specific portion of the code that addresses this redirect uri problem:
# This section is needed for url_for("foo", _external=True) to automatically
# generate http scheme when this sample is running on localhost,
# and to generate https scheme when it is deployed behind reversed proxy.
# See also https://flask.palletsprojects.com/en/1.0.x/deploying/wsgi-standalone/#proxy-setups
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
Does anyone know how I can do something like this for a web app using FastAPI instead?
The full source code for the Flask app can be found here

Flask redirect url_for generates Cloud Run service URL instead of domain

Background:
I've built and deployed an app with Google Cloud Firebase. At a high level, I have a Python Flask server running on Cloud Run, and I serve static JS files with Firebase hosting.
Issue:
Sometimes, I want to redirect the user, but I'm sending them to the Cloud Run service URL rather than my app domain.
EDIT: I'm NOT experiencing this in the JS on the browser, but ONLY in the Python on the server.
Python
If a user navigates to a page without being signed in, e.g. following a link, they are redirected to my login page. For example, if someone who is not signed in tries to look at someone else's profile, the following code redirects them to the authentication blueprint's login endpoint:
if not session.get('user'):
return redirect(url_for('authentication.login'))
I would expect them to be redirected to my-app-name.web.app/auth/login but instead they're routed to my-cloudrun-service-name-XXXXX-region.run.app/auth/login. While the pages loaded look the same, they're really not. The second one redirects to the Cloud Run service URL, which doesn't have my static files served by Firebase Hosting.
I'm not really sure, but I believe this happens because Flask is generating the URL using the request context. So the request from the browser hits Cloud Run Load Balancer, which directs the request to my Cloud Run instance, but that means the Flask app in my Cloud Run instance only sees the internal Google Cloud redirect, and it doesn't know my domain.
I've tried solving this by setting app.config['SEVER_NAME'] = my-app-name.web.app, but I just get the "Not Found" page on each request. Is SEVER_NAME the solution but I'm not implementing it correctly, or is there another way to fix the Flask url_for generation?
I've found what I deem to be an effective solution / workaround.
I set the app config to store my BASE_URL when the app is created:
app.config['BASE_URL'] = 'https://my-url.domain'
Then I can access this as application context during requests, even from blueprints:
#blueprint.route('my_route/')
def my_route():
if not session.get('user'):
return redirect(current_app.config['BASE_URL'] + url_for('authentication.login', 302)
This has two key benefits for me:
There's only one place, the app config, to update if I change the domain
url_for is still used, so there's no hardcoding in case blueprints or routes change

How to obtain the caller's IP address in Flask/Connexion?

I'm using an OpenAPI 3.0 specification (swagger.yml) and use Swagger Codegen to create the corresponding Python Flask application stubs. This is how I run the application to expose my Swagger API:
app = connexion.App(__name__, specification_dir='./swagger/')
app.app.json_encoder = encoder.JSONEncoder
app.add_api('swagger.yaml', arguments={'title': 'My Test API'})
# add CORS support to send Access-Control-Allow-Origin header
CORS(app.app)
So far so good. The application logic is handled within the generated Python stubs which are linked by the x-openapi-router-controller: swagger_server.controllers.user_controller.
I now however need to access HTTP Request specific information within the application itself to for example react differently based on the HTTP_CLIENT_IP address
How can I obtain that information within my controller endpoint?
Use Flask's request context.
For example, to get the HTTP_CLIENT_IP, use:
from flask import request
http_client_ip = request.remote_addr
You can read more about request here.
Attached two related links addressing the same issue on request header parameters and how connexion does not forward them to custom controllers. I ended up manually accessing them via
access_token = connexion.request.headers['access_token']

app.yaml handler login: admin option not effective on standard env python GAE app?

I was working on some security checks for my standard env python GAE app and I was surprised to see that the login: admin option appears to be non-effective.
I want to secure a portion of a request namespace to just the app itself, not external requests. The app sends these requests through a push task queue.
This is the respective handler configuration, which I checked in StackDriver to be the actual code that handled the particular request in question:
- url: /ci/ci_msg* # external requests OK
script: apartci.app
secure: always
- url: /ci/.* # internal requests only
script: apartci.app
secure: always
login: admin
This is the handler code, hacked to log an error to check if the request actually hits the app code, also verified in StackDriver to be the actual handling code:
def post(self):
logging.error('in post')
self.handle_post()
I sent the external request to the exact same path that only the internal task queue requests should be accepted, using the Firefox HttpRequester add-on. The request body failed the additional checks in self.handle_post(), but that's irrelevant for this question.
The response I got in HttpRequester (rather irrelevant as well):
<html>
<head>
<title>203 Non-Authoritative Information</title>
</head>
<body>
<h1>203 Non-Authoritative Information</h1>
<br /><br />
</body>
</html>
I checked the app logs in StackDriver. To my surprise I found the logging.error('in post') app log from my handler's post() method attached to the request log, indicating that the request made it to my app:
For comparison - the log from the same request sent from the app itself (coincidentally just ~1 second before the external one and handled by the exact same instance - which contributed to my confusion):
My expectation was for the external request to not make it to the handler code, according to the login row in Handlers element:
admin
As with required, performs auth_fail_action if the user is
not signed in. In addition, if the user is not an administrator for
the application, they are given an error message regardless of the
auth_fail_action setting. If the user is an administrator, the
handler proceeds.
When a URL handler with a login setting other than optional
matches a URL, the handler first checks whether the user has signed in
to the application using its authentication option. If not, by
default, the user is redirected to the sign-in page. You can also use
auth_fail_action to configure the app to simply reject requests
for a handler from users who are not properly authenticated, instead
of redirecting the user to the sign-in page.
Note: the admin login restriction is also satisfied for internal
requests for which App Engine sets appropriate X-Appengine special
headers. For example, cron scheduled tasks satisfy the admin
restriction, because App Engine sets an HTTP header
X-AppEngine-Cron: true on the respective requests. However, the requests would not satisfy the required login restriction,
because cron scheduled tasks are not run as any user.
So my question is why/how did the external request manage to hit the handler code? Am I missing something?
Mistery solved: apparently the Firefox HttpRequester add-on is smart enough to automatically pull the google credentials from Firefox and use them. The updated image in the question now has a pointer showing the username info I blacked out but didn't regard as a clue. Those credentials have admin permissions to the GAE app, which explains why that request made it to the handler code.
To confirm this theory I tried the same request but this time sent using curl:
$ curl --request POST --data '{"task": "project_integrity_check_task",
"obj_id": 4841240159846400, "ci_proj": 4841240159846400, "obj_cls":
"Project"}' [url_redacted]
The response is indeed a 302 and the app error log is missing, indicating that this time the request didn't make it to the handler code, as expected:

How to authenticate requests across internal App Engine modules?

I have an application in Google App Engine that consists in 2 modules (A and B). A handles user requests and it's available without authentication. B is a microservice that perform certain tasks when A requires it. So we have A making requests to B using urlfetch:
from google.appengine.api import urlfetch
from google.appengine.api import app_identity
rpc = urlfetch.create_rpc()
urlfetch.make_fetch_call(
rpc,
"https://b-dot-my-project.appspot.com/some/url",
method='GET',
follow_redirects=False,
headers = {
'X-Appengine-Inbound-Appid': 'my-project',
},
)
response = rpc.get_result()
B's app.yaml looks something like:
runtime: python27
api_version: 1
threadsafe: yes
service: b
handlers:
- url: /.*
script: my_module.app
login: admin
auth_fail_action: unauthorized
In the docs, they suggest:
When issuing a request to another App Engine app, your App Engine app
must assert its identity by adding the header
X-Appengine-Inbound-Appid to the request. If you instruct the URL
Fetch service to not follow redirects, App Engine will add this header
to requests automatically.
No matter what I do, I keep getting a 401 when making this request. Both A and B are deployed in the same project. Tried setting follow_redirects=False and adding the headers X-Appengine-Inbound-Appid manually (though I didn't expect it to work for the reasons described here), still not sure if the header is being set, as the logs for B don't include request headers and the failure condition happens before my handler module gets executed.
I would rather if possible to rely on A authenticating to B rather than just dropping the option login: admin and rely only on the header, as it is nicer to be able to call B from a project admin account (for debugging purposes for example).
Instead of specifying login: admin in your config, use the python library instead: https://cloud.google.com/appengine/docs/standard/python/refdocs/google.appengine.api.users This way you can check for the app engine header first, and fallback to the admin google user.
Instead of login:admin, you could check the header in module B request for 'HTTP_USER_AGENT': 'AppEngine-Google; (+http://code.google.com/appengine; appid: s~my-project)'. That tells you it came from urlfetch, taskqueue, or cron job.

Categories