0
votes

I've configured a cloud endpoint that executes a GCF. Everything works fine when the cloud run service is allowing allUsers to call the API.

Once I remove the allUsers and authenticate using the service account, I get 403 errors showing up in the Cloud run console:

The request was not authenticated. Either allow unauthenticated invocations or set the proper Authorization header. Read more at https://cloud.google.com/run/docs/securing/authenticating

Chrome JS console shows the following error message:

Access to fetch at 'https://.run.app/do-this&key=' from origin 'http://0.0.0.0:8080' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

This is my JS code running in the browser:

        let options: RequestInit = {
            headers: {
                'Authorization': `Bearer ${token}`,
            },
        }
        
        const result = await fetch(fetchURL, options);

When running curl with the same token, I get the expected response

curl -H "Authorization: Bearer ${token}" 'https://<my-api>.run.app/do-this&key=<key>'

For completeness here is also the endpoints yaml

swagger: '2.0'
info:
  title: My first widget
  description: This is a great widget
  version: 1.0.0
host: <my-api>.run.app
schemes:
  - https
produces: 
  - application/json
paths:
  /do-this:
    get:
      summary: Do-this
      operationId: doit
      x-google-backend:
        address: https://<project-id>.cloudfunctions.net/do-that
      responses:
        '200':
          description: A successful response.
          schema:
            type: string
        '403':
          description: An error occurred
          schema:
            type: string
      security:
        - api_key: []

securityDefinitions:
  # This section configures basic authentication with an API key.
  api_key:
    type: "apiKey"
    name: "key"
    in: "query"

Command to update esp:

gcloud run services update <my-api> --set-env-vars="^|^ENDPOINTS_SERVICE_NAME=<my-api>.run.app|ESP_ARGS=--rollout_strategy=managed,--cors_preset=basic" --project=<project-id> --platform=managed --region=europe-west1

Update

Enabling cors browser side did not help.

The Google docs mention that it should be possible to call from outside GCP

If you're invoking a service from a compute instance that doesn't have access to compute metadata (e.g. your own server), you'll have to manually generate the proper token:
Self-sign a service account JWT with the target_audience claim set to the URL of the receiving service. Exchange the self-signed JWT for a Google-signed ID token, which should have the aud claim set to the above URL. Include the ID token in an Authorization: Bearer ID_TOKEN header in the request to the service. Although Identity-Aware Proxy is not yet supported for Cloud Run (fully managed), you can examine the Identity-Aware Proxy sample code for code examples of the steps above.

The end-users section: mentions CORS though

When you build a web app, you have to account for Cross-Origin Resource Sharing (CORS) issues. For example, CORS preflight requests are sent without an Authorization header, so they are rejected on a non-public service. Because the preflight requests fail, the main request will also fail.
To work around this, you can host your web app and service(s) on the same domain to avoid CORS preflight requests. You can achieve that by using Firebase Hosting.

I tried hosting the JS script and HTML on Firebase hosting, yet the issue persists.

Another question that comes to mind is: do I need to set OAuth alongside API key authentication in the open api specification?

Update 2

This discussion suggests it is not possible to use Cloud Run with Authentication supporting CORS. I'm yet wondering why it's possible in curl. I'm using a service account token for auth, not end user.

2
Are you using the Fetch API in the browser or in your backend application?Ahmet Alp Balkan
@AhmetB: fetch API in the browser.bhr
This seems relevant to you stackoverflow.com/a/45640164/54929 I didn't think you can send arbitrary headers, but apparently it's possible with CORS.Ahmet Alp Balkan
Thank you @AhmetB. I went over the thread and tried setting credentials=true. Digging further I found two other threads related to this subject. Tried those approaches too without success (yet). stackoverflow.com/a/55873861/967327 stackoverflow.com/a/63471693/967327bhr

2 Answers

0
votes

The cors aren't activated for Cloud Endpoint. update your openAPI spec like this

swagger: '2.0'
info:
  title: My first widget
  description: This is a great widget
  version: 1.0.0
host: <my-api>.run.app
x-google-endpoints:
- name: <my-api>.run.app
  allowCors: True
...
...
...

Or set no-cors check in your call as described by the error message.

0
votes

I've made it work the following way:

  1. Add oauth as a security definition to the OpenAPI spec and use it alongisde the api key for each API path
  2. Deploy endpoint with --set-env-vars="^|^ENDPOINTS_SERVICE_NAME=<my-api>.run.app|ESP_ARGS=--cors_preset=basic,--rollout_strategy=managed"
  3. In the Cloud Function, set the Access-Control-Allow-Origin to empty string '': res.setHeader("Access-Control-Allow-Origin", '')
  4. Allow the Cloud Run container to be accessible by allUsers

While the Cloud Run container is accessible by everyone, the endpoint is taking care of the authentication.

One thing that surprised me is that the CF is automatically adding the calling host (e.g. mydomain.com) and * in the Access-Control-Allow-Origin header. Multiple items are not permitted in this header and I'm therefore getting rid of the mydomain.com and keep *.

I'm going to play around with the different options and once done will provide a how-to with all the steps involved. Any comments/suggestions are very much apprecated!

UPDATE

After digging deeper, I understand now that Access-Control-Allow-Origin is automatically added by const cors = require('cors')({origin: true});

For my use case I don't require cors inside the CFs because they're only accessible from Cloud Run ESP.

Therefore the important steps are:

  • add oauth to OpenAPI spec
  • enable cors through ESP_ARGS
  • allow cloud run container to be accessible by allUsers