node.jsfirebasegoogle-cloud-functionsgoogle-site-verification-api

Accessing Google Site Verification API from a Firebase function


I am trying to use the Google Site Verification API from a Firebase function using Node.js.

The README available in the google-api-nodejs-client repository on Github recommends using the default application method instead of manually creating an OAuth2 client, JWT client, or Compute client.

I wrote the following example that I tried to run locally (emulated function environment) and remotely on a Firebase function:

const google = require('googleapis');

google.auth.getApplicationDefault(function (err, authClient, projectId) {
    if (err) {
        console.log('Authentication failed because of ', err);
        return;
    }

    if (authClient.createScopedRequired && authClient.createScopedRequired()) {
        authClient = authClient.createScoped([
            'https://www.googleapis.com/auth/siteverification'
        ]);
    }

    const siteVerification = google.siteVerification({
        version: 'v1',
        auth: authClient
    });

    siteVerification.webResource.get({
        id: 'test.com'
    }, {}, function (err, data) {
        if (err) {
            console.log('siteVerification get error:', err);
        } else {
            console.log('siteVerification result:', data);
        }
    });
});

In both cases, upon execution, I get the following error:

siteVerification get error: { Error: A Forbidden error was returned while attempting to retrieve an access token for the Compute Engine built-in service account. This may be because the Compute Engine instance does not have the correct permission scopes specified. Insufficient Permission
    at Request._callback (/user_code/node_modules/googleapis/node_modules/google-auth-library/lib/transporters.js:85:15)
    at Request.self.callback (/user_code/node_modules/googleapis/node_modules/request/request.js:188:22)
    at emitTwo (events.js:106:13)
    at Request.emit (events.js:191:7)
    at Request.<anonymous> (/user_code/node_modules/googleapis/node_modules/request/request.js:1171:10)
    at emitOne (events.js:96:13)
    at Request.emit (events.js:188:7)
    at IncomingMessage.<anonymous> (/user_code/node_modules/googleapis/node_modules/request/request.js:1091:12)
    at IncomingMessage.g (events.js:292:16)
    at emitNone (events.js:91:20)
  code: 403,
  errors: 
   [ { domain: 'global',
       reason: 'insufficientPermissions',
       message: 'Insufficient Permission' } ] }

Please note the site verification API is enabled for the Cloud project associated to Firebase.

UPDATE:

Creating a service account with Project owner role and authenticating with the JWT method leads to the following permission error:

info: siteVerification get error: { Error: You are not an owner of this site.
    at Request._callback
    ...
    at IncomingMessage.g (events.js:292:16)
    at emitNone (events.js:91:20)
  code: 403,
  errors: 
   [ { domain: 'global',
       reason: 'forbidden',
       message: 'You are not an owner of this site.' } ] }

This error is for a get with an ID of a site I know to own since I made the call with the same ID using the API explorer and this one returns details.

I don't know whether some permissions must be configured in the Google cloud console or if the authentication method should be different. I have the feeling that only OAuth 2.0 client with manual user auth is allowed...

Help is welcome.


Solution

  • The site verification API allows OAuth 2.0 with manual authentication only. The getting started documentation contains a few lines regarding this:

    Your application must use OAuth 2.0 to authorize requests. No other authorization protocols are supported. If your application uses Google Sign-In, some aspects of authorization are handled for you.

    As a workaround, I generated an access token with its associated refresh token. Once you have both, you can use them on your server function. If you use the official Google NodeJS client for the site verification API, access token refresh is managed for you. Otherwise, you must refresh your access token when it expires.

    Below are Firebase functions you can use to easily create your access token.

    function oauth2Client() {
        return new google.auth.OAuth2(
            config.site_verification_api.client_id,
            config.site_verification_api.client_secret,
            'http://localhost:8080/oauth'
        );
    }
    
    exports.oauth2GetAuthorizationCode = functions.https.onRequest((req, res) => {
    
        const client = oauth2Client();
    
        const url = client.generateAuthUrl({
            access_type: 'offline',
            scope: [
                'https://www.googleapis.com/auth/siteverification'
            ]
        });
    
        res.status(200).send({url: url});
    });
    
    exports.oauth2GetAccessToken = functions.https.onRequest((req, res) => {
    
        const client = oauth2Client();
        const code = req.query.code;
    
        client.getToken(code, (err, tokens) => {
            if (!err) {
                res.status(200).send({tokens});
            } else {
                console.error('Error while getting access token:', err);
                res.sendStatus(500);
            }
        });
    });
    

    When you invoke the HTTP endpoint associated to oauth2GetAuthorizationCode, an URL is returned. Open this URL on your browser. This redirects to a local URL that includes an authorization code as a query parameter. Get this parameter and invoke the second HTTP endpoint associated to oauth2GetAccessToken. This last invocation should return your access and refresh tokens.

    Once you have both tokens, you can store them in your Firebase environment configuration (along with your client ID and secret) and access the site verification API as follows:

    function oauth2ClientWithCredentials() {
        const client = oauth2Client();
    
        client.setCredentials({
            access_token: config.site_verification_api.access_token,
            refresh_token: config.site_verification_api.refresh_token
        });
    
        return client;
    }
    
    function invokeSiteVerificationApi() {
    
        const client = oauth2ClientWithCredentials();
    
        const siteVerification = google.siteVerification({
            version: 'v1',
            auth: client
        });
    
        siteVerification.webResource.get({
            id: 'dns%3A%2F%2F' + domain
        }, null, (err, result) => {
            // ...
        });
    }