Search code examples
node.jsgoogle-cloud-platformgoogle-cloud-functionsgcloud

Invoking GCloud Function without Authentication


I am pretty new to using gcloud functions.

I have a google cloud function which is supposed to start up a VM instance, but I want to invoke this function from a website without authentication (for now) when sending the HTTP request. How can I do this?

I also don't need to send any params, just need to invoke function

Here is my code for a simple website to invoke the cloud function, though this seems to be using auth which I don't want to do, I have disabled authentication when making the function in the console:

<!DOCTYPE html>
<html lang="en">

<body>
    <div class="bordered-text-block">
        <h1>VM Controls</h1>
        <button id="id_start_button" onclick="startVM()">Start
        </button>
        <button id="id_stop_button" onclick="stopVM()">Stop
        </button>
    </div>

    <script>
        function startVM() {
            const fetch = require('node-fetch');

            // TODO(developer): set these values
            const REGION = '<my region>';
            const PROJECT_ID = '<project name>';
            const RECEIVING_FUNCTION = '<function name>';

            // Constants for setting up metadata server request
            // See https://cloud.google.com/compute/docs/instances/verifying-instance-identity#request_signature
            const functionURL = `https://${REGION}-${PROJECT_ID}.cloudfunctions.net/${RECEIVING_FUNCTION}`;
            const metadataServerURL =
                'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=';
            const tokenUrl = metadataServerURL + functionURL;

            exports.callingFunction = async (req, res) => {
                // Fetch the token
                const tokenResponse = await fetch(tokenUrl, {
                    headers: {
                        'Metadata-Flavor': 'Google',
                    },
                });
                const token = await tokenResponse.text();

                // Provide the token in the request to the receiving function
                try {
                    const functionResponse = await fetch(functionURL, {
                        headers: { Authorization: `bearer ${token}` },
                    });
                    res.status(200).send(await functionResponse.text());
                } catch (err) {
                    console.error(err);
                    res.status(500).send('An error occurred! See logs for more details.');
                }
            };
        }
    </script>
</body>

</html>

Solution

  • Following up on my comment, here's the simplest answer that should work.

    NOTE I think you must ensure your Cloud Functions returns appropriate CORS headers

    • Access-Control-Allow-Origin: *
    • Access-Control-Allow-Methods: GET

    I wrote a Cloud Function in Golang:

    package p
    
    import (
        "encoding/json"
        "net/http"
    )
    
    type Message struct {
        Name string `json:"name"`
    }
    
    func WebPage(w http.ResponseWriter, r *http.Request) {
        m := Message{
            Name: "Freddie",
        }
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "GET")
        w.Header().Set("Content-Type", "application/json")
        err := json.NewEncoder(w).Encode(m)
        if err != nil {
            http.Error(w, err.Error(), 500)
            return
        }
    }
    

    I deployed this Function:

    PROJECT="<your-project>"
    REGION="<your-region>"
    FUNCTION="<your-function>"
    
    gcloud projects create ${PROJECT}
    
    BILLING=$(gcloud beta billing accounts list --format="value(name)")
    gcloud beta billing projects link ${PROJECT} --billing-account=${BILLING}
    
    for SERVICE in "cloudbuild" "cloudfunctions"
    do
      gcloud services enable ${SERVICE}.googleapis.com \
      --project=${PROJECT}
    done
    
    gcloud functions deploy ${FUNCTION} \
    --entry-point=WebPage \
    --source=. \
    --project=${PROJECT} \
    --allow-unauthenticated \
    --region=${REGION} \
    --runtime=go113 \
    --trigger-http
    

    You can grab your function's endpoint in curl:

    URL=$(\
      gcloud functions describe ${FUNCTION} \
      --project=${PROJECT} \
      --format="value(httpsTrigger.url)")
    
    curl \
    --silent \
    --include \
    --header "Accept: application/json" \
    --request GET \
    ${URL}
    

    Yields:

    HTTP/2 200 
    access-control-allow-methods: GET
    access-control-allow-origin: *
    content-type: application/json
    ...
    
    {"name":"Freddie"}
    

    Then you can GET the value and put it into an HTML id:

    <!DOCTYPE html>
    <html lang="en">
    
    <body>
    <div id="result">/div>
    
    <script>
        const PROJECT = "<your-project>";
        const REGION = "<your-region>";
        const FUNCTION = "<your-function>";
    
        const ID = "result";
    
        const URL = `https://${REGION}-${PROJECT}.cloudfunctions.net/${FUNCTION}`;
    
        fetch(URL,{
            method: "GET",
            headers: {
                "Accept": "application/json"
            }
        })
        .then(resp => resp.json())
        .then(json => JSON.stringify(json))
        .then(html => document.getElementById(ID).innerHTML = html);
    </script>
    </body>
    </html>
    

    I tested this (locally) using Caddy:

    docker run \
    --rm \
    --name=caddy \
    --interactive --tty \
    --publish=80:80 \
    --volume=$PWD/index.html:/usr/share/caddy/index.html \
    --volume=caddy_data:/data \
    caddy
    

    And then browsing http://localhost:80, I get:

    {"name":"Freddie"}