1
votes

I need help getting my Firebase Apollo/GraphQL Cloud Function to authenticate and receive query requests.

The Apollo/GraphQL function works fine (tested with playground) when permissions are set to allUsers. After setting permissions to allAuthenticatedUsers and attempting to send authenticated queries I am receiving the following error response:

Bearer error="invalid_token" error_description="The access token could not be verified"

I believe I am making a mistake with the request sent by the client, and or the handling of the verification and "context" of the ApolloServer. I have confirmed the initial user token is correct. My current theory is that I am sending the wrong header, or messing up the syntax somehow at either the client or server level.

To explain what I believe the appropriate flow of the request should be:

  1. Token generated in client
  2. Query sent from client with token as header
  3. ApolloServer cloud function receives request
  4. Token is verified by Firebase, provides new verified header token
  5. Server accepts query with new verified header token and returns data

If anyone can explain how to send valid authenticated client queries to a Firebase Apollo/GraphQL Cloud Function the help would be greatly appreciated. Code for server and client below.

Server.js (ApolloServer)

/* Assume proper imports */
/* Initialize Firebase Admin SDK */
admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: "[db-url]",
});

/* Async verification with user token */
const verify = async (idToken) => {
  var newToken = idToken.replace("Bearer ", "");
  let header = await admin.auth().verifyIdToken(newToken)
    .then(function(decodedToken) {
      let uid = decodedToken.uid;
      // Not sure if I should be using the .uid from above as the token?
      // Also, not sure if returning the below object is acceptable, or
      // if this is even the correct header to send to firebase from Apollo
      return {
        "Authorization": `Bearer ${decodedToken}`
      }
    }).catch(function(error) {
      // Handle error
      return null
    });
  return header
}

/* Server */
function gqlServer() {
  const app = express();

  const apolloServer = new ApolloServer({
    typeDefs: schema,
    resolvers,
    context: async ({ req, res }) => {
      const verified = await verify(req.headers.Authorization)
      console.log('log verified', verified)
      return {
        headers: verified ? verified: '',
        req, 
        res,
      }
    },
    // Enable graphiql gui
    introspection: true,
    playground: true
  });
  
  apolloServer.applyMiddleware({app, path: '/', cors: true});

  return app;
}

export default gqlServer;

Client.js (ApolloClient)

Client query constructed using these instructions.

/* Assume appropriate imports */
/* React Native firebase auth */
firebase.auth().onAuthStateChanged(async (user) => {
    const userToken = await user.getIdToken();
    
    /* Client creation */
    const client = new ApolloClient({
      uri: '[Firebase Cloud Function URL]',
      headers: {
        Authorization: userToken ? `Bearer ${userToken}` : ''
      },
      cache: new InMemoryCache(),
    });
    /* Query test */
    client.query({
      query: gql`
        {
          hello
        }
      `
    }).then(
      (result) => console.log('log query result', result)
    ).catch(
      (error) => console.log('query error', error)
    )
})

UPDATE 05/03/20

I may have found the source of the error. I won't post an answer until I confirm, but here's the update. Looks like allAuthenticatedUsers is a role specific to Google accounts and not Firebase. See this part of the google docs and this stackoverflow answer.

I will do some testing but the solution may be to change the permissions to allUsers which may still require authentication. If I can get it working I will update with an answer.

1
" Authorization: Bearer ${userToken} ? userToken : '' " Is this line correct?gstvg
@gstvg, according to step 2 in the instructions regarding the client side this is the header that should be sent Authorization: Bearer ID_TOKEN. So I think so yes. Docs for ref -cloud.google.com/functions/docs/securing/…,Jason
Yes, you've got the docs right, and formatting of my comment also make hard to identify, but i think there's a little typo on this line: you are doing a ternay operation which always return the userToken alone, without the "Bearer " part, it isn't?gstvg
@gstvg good catch, I did have those flipped accidentally. Unfortunately after fixing the operation and attempting the query again the result is still the same error. I've updated the above question to reflect the correct operation.Jason
Any update on the solution?JKleinne

1 Answers

1
votes

I was able to get things working. Working requests required the following changes:

  1. Change cloud function "invoker" role to include allUsers instead of allAuthenticatedUsers. This because the allUsers role makes the function available to http requests (you can still require authentication through sdk verification)
  2. Adjusting the code for the server and client as shown below. Minor change to header string construction.

Server.js (ApolloServer)

/* Assume proper imports */
/* Initialize Firebase Admin SDK */
admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: "[db-url]",
});

/* Async verification with user token */
const verify = async (idToken) => {
  if (idToken) {
    var newToken = idToken.replace("Bearer ", "");
    // var newToken = idToken
    let header = await admin.auth().verifyIdToken(newToken)
      .then(function(decodedToken) {
        // ...
        return {
          "Authorization": 'Bearer ' + decodedToken
        }
      }).catch(function(error) {
        // Handle error
        return null
      });
    return header
  } else {
    throw 'No Access' 
  }
}

/* Server */
function gqlServer() {
  const app = express();

  const apolloServer = new ApolloServer({
    typeDefs: schema,
    resolvers,
    context: async ({ req, res }) => {
      // headers: req.headers,
      const verified = await verify(req.headers.authorization)
      console.log('log verified', verified)
      return {
        headers: verified ? verified: '',
        req, 
        res,
      }
    },
    // Enable graphiql gui
    introspection: true,
    playground: true
  });
  
  apolloServer.applyMiddleware({app, path: '/', cors: true});

  return app;
}

export default gqlServer;

Client.js (ApolloClient)

/* Assume appropriate imports */
/* React Native firebase auth */
firebase.auth().onAuthStateChanged(async (user) => {
    const userToken = await user.getIdToken();
    
    /* Client creation */
    const userToken = await user.getIdToken();
    const client = new ApolloClient({
      uri: '[Firebase Cloud Function URL]',
      headers: {
        "Authorization": userToken ? 'Bearer ' + userToken : ''
      }, 
      cache: new InMemoryCache(),
    });
    client.query({
      query: gql`
        {
          hello
        }
      `
    }).then(
      (result) => console.log('log query result', result)
    ).catch(
      (error) => console.log('query error', error)
    )
})