Search code examples
google-cloud-platformrefresh-tokengoogle-identity-toolkit

How to access Google Cloud Token Service API with a Service Account?


I am trying to use this API through a service account.

https://securetoken.googleapis.com/v1/token?key=[API_KEY]

Reference: https://cloud.google.com/identity-platform/docs/use-rest-api#section-refresh-token

The problem is that I keep having the 403 FORBIDDEN error when trying to call it with credentials generated from my service account.

{"error"=>{"code"=>403, "message"=>"Request had insufficient authentication scopes.", "status"=>"PERMISSION_DENIED", "details"=>[{"@type"=>"type.googleapis.com/google.rpc.ErrorInfo", "reason"=>"ACCESS_TOKEN_SCOPE_INSUFFICIENT", "domain"=>"googleapis.com", "metadata"=>{"method"=>"google.identity.securetoken.v1.SecureToken.GrantToken", "service"=>"securetoken.googleapis.com"}}]}}

Here is what I checked.

  • My Token Service API is enabled on my GCP project.
  • My service account has the owner role.
  • When calling it with an API key, no problem with using the API (but I want to make it work with a service account...)

Here is the code that I am trying to make it work.

      SCOPE = %w[
        https://www.googleapis.com/auth/firebase
        https://www.googleapis.com/auth/identitytoolkit
      ]
  
      def initialize
        decoded_service_account_json = Base64.decode64(ENV.fetch("GCP_JSON_KEYFILE_BASE64"))
        io = StringIO.new(decoded_service_account_json)
        @credentials = Google::Auth::DefaultCredentials.make_creds(scope: SCOPE, json_key_io: io)
      end

      # Exchange a refresh token for an ID token
      # References:
      #  - https://cloud.google.com/identity-platform/docs/reference/rest/v1/accounts/accounts/getIdpAuthentication
      def exchange_refresh_token(refresh_token)
        uri = URI.parse("https://securetoken.googleapis.com/v1/token")
        http = Net::HTTP.new(uri.host, uri.port)
        http.use_ssl = true

        request = Net::HTTP::Post.new(uri.path, { 'Content-Type' => 'application/x-www-form-urlencoded' })
        request.body = URI.encode_www_form(
          'grant_type' => 'refresh_token',
          'refresh_token' => refresh_token
        )

        request.add_field('Authorization', @credentials.apply({})[:authorization])

        response = http.request(request)
        response_data = JSON.parse(response.body)

        if response.code.to_i == 200
          {
            id_token: response_data['id_token'],
            refresh_token: response_data['refresh_token'],
            expires_at: Time.now + response_data['expires_in'].to_i
          }
        else
          raise TokenExchangeError.new("Token exchange failed: #{response.code} - #{response_data['error_description']}")
        end
      end

Do you have any idea why it does not work? Or perhaps Token Service API cannot work with a service account?

Thank you in advance.


Solution

  • I found the solution by playing around and making guesses as the documentation was not providing any useful information.

    I needed to add the https://www.googleapis.com/auth/securetoken scope in my SCOPE in order to be able to use https://securetoken.googleapis.com/v1/token.

    SCOPE = %w[
            https://www.googleapis.com/auth/firebase
            https://www.googleapis.com/auth/identitytoolkit
            https://www.googleapis.com/auth/securetoken
          ]
      
          def initialize
            decoded_service_account_json = Base64.decode64(ENV.fetch("GCP_JSON_KEYFILE_BASE64"))
            io = StringIO.new(decoded_service_account_json)
            @credentials = Google::Auth::DefaultCredentials.make_creds(scope: SCOPE, json_key_io: io)
          end
    

    That was completely a random guess... I checked all the possible scopes here: https://developers.google.com/identity/protocols/oauth2/scopes

    But https://www.googleapis.com/auth/securetoken is not one of them. Even Google Search does not return any relevant information for the https://www.googleapis.com/auth/securetoken scope.

    This is quite frustrating. Could someone tell me if there is a way I could have found the solution without guessing? Where does https://www.googleapis.com/auth/securetoken come from?