How do I use a Service Account to Access the Google Analytics API V3 with .NET C#?
Asked Answered
B

5

16

I realized this question has been previously asked but with little in the way of example code, so I am asking again but with at least a little bit of direction.

After hours of searching, I have come up with the following partial implementation.

namespace GoogleAnalyticsAPITest.Console
{
    using System.Security.Cryptography.X509Certificates;
    using DotNetOpenAuth.OAuth2;
    using Google.Apis.Analytics.v3;
    using Google.Apis.Analytics.v3.Data;
    using Google.Apis.Authentication.OAuth2;
    using Google.Apis.Authentication.OAuth2.DotNetOpenAuth;

    class Program
    {
        static void Main(string[] args)
        {
            log4net.Config.XmlConfigurator.Configure();
            string Scope = Google.Apis.Analytics.v3.AnalyticsService.Scopes.Analytics.ToString().ToLower();
            string scopeUrl = "https://www.googleapis.com/auth/" + Scope;
            const string ServiceAccountId = "nnnnnnnnnnn.apps.googleusercontent.com";
            const string ServiceAccountUser = "[email protected]";
            AssertionFlowClient client = new AssertionFlowClient(
                GoogleAuthenticationServer.Description, new X509Certificate2(@"7039572692013fc5deada350904f55bad2588a2a-privatekey.p12", "notasecret", X509KeyStorageFlags.Exportable))
            {
                Scope = scopeUrl,
                ServiceAccountId = ServiceAccountId//,ServiceAccountUser = ServiceAccountUser
            };
            IAuthorizationState state = AssertionFlowClient.GetState(client);
            AnalyticsService service = new AnalyticsService(authenticator);
            string profileId = "ga:xxxxxxxx";
            string startDate = "2010-10-01";
            string endDate = "2010-10-18";
            string metrics = "ga:visits";
            DataResource.GaResource.GetRequest request = service.Data.Ga.Get(profileId, startDate, endDate, metrics);
            request.Dimensions = "ga:date";
            GaData data = request.Fetch();
        }
    }
}

I have a couple issues. The call to AssertionFlowClient.GetState(client) results in a "invalid_scope" response as seen in the DotNetOpenAuth log of

2012-10-19 13:27:36,272 (GMT-4) [8] INFO DotNetOpenAuth - DotNetOpenAuth, Version=4.0.0.11165, Culture=neutral, PublicKeyToken=2780ccd10d57b246 (official) 2012-10-19 13:27:36,284 (GMT-4) [8] DEBUG DotNetOpenAuth.Messaging.Channel - Preparing to send AssertionFlowMessage (2.0) message. 2012-10-19 13:27:36,294 (GMT-4) [8] INFO DotNetOpenAuth.Messaging.Channel - Prepared outgoing AssertionFlowMessage (2.0) message for https://accounts.google.com/o/oauth2/token: grant_type: assertion assertion_type: http://oauth.net/grant_type/jwt/1.0/bearer assertion: (a bunch of encoded characters go here)

2012-10-19 13:27:36,296 (GMT-4) [8] DEBUG DotNetOpenAuth.Messaging.Channel - Sending AssertionFlowMessage request. 2012-10-19 13:27:36,830 (GMT-4) [8] DEBUG DotNetOpenAuth.Http - HTTP POST https://accounts.google.com/o/oauth2/token 2012-10-19 13:27:36,954 (GMT-4) [8] ERROR DotNetOpenAuth.Http - WebException from https://accounts.google.com/o/oauth2/token: { "error" : "invalid_scope" }

I have tried specifying one or both of ServiceAccountId and ServiceAccountUser with no luck.

Second, even if I get an IAuthorizationState, I am not sure how I get an IAuthenticator that can be passed to the AnalyticsService constructor.

The following is the web.config I use to enable DotNetOpenAuth logging.

<?xml version="1.0"?>
<configuration>
  <configSections>
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net, Version=1.2.10.0, Culture=neutral, publicKeyToken=1b44e1d426115821" />
    <!--<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler" requirePermission="false"/>-->
    <sectionGroup name="dotNetOpenAuth" type="DotNetOpenAuth.Configuration.DotNetOpenAuthSection, DotNetOpenAuth">
      <section name="openid" type="DotNetOpenAuth.Configuration.OpenIdElement, DotNetOpenAuth" requirePermission="false" allowLocation="true"/>
      <section name="oauth" type="DotNetOpenAuth.Configuration.OAuthElement, DotNetOpenAuth" requirePermission="false" allowLocation="true"/>
      <section name="messaging" type="DotNetOpenAuth.Configuration.MessagingElement, DotNetOpenAuth" requirePermission="false" allowLocation="true"/>
      <section name="reporting" type="DotNetOpenAuth.Configuration.ReportingElement, DotNetOpenAuth" requirePermission="false" allowLocation="true"/>
    </sectionGroup>
  </configSections>
  <log4net>
    <appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">
      <file value="DotNetOpenAuth.log"/>
      <appendToFile value="true"/>
      <rollingStyle value="Size"/>
      <maxSizeRollBackups value="10"/>
      <maximumFileSize value="100KB"/>
      <staticLogFileName value="true"/>
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%date (GMT%date{%z}) [%thread] %-5level %logger - %message%newline"/>
      </layout>
    </appender>
    <appender name="TracePageAppender" type="OpenIdProviderWebForms.Code.TracePageAppender, OpenIdProviderWebForms">
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%date (GMT%date{%z}) [%thread] %-5level %logger - %message%newline"/>
      </layout>
    </appender>
    <!-- Setup the root category, add the appenders and set the default level -->
    <root>
      <level value="ALL"/>
      <appender-ref ref="RollingFileAppender"/>
      <appender-ref ref="TracePageAppender"/>
    </root>
    <!-- Specify the level for some specific categories -->
    <logger name="DotNetOpenAuth">
      <level value="ALL"/>
    </logger>
  </log4net>
  <dotNetOpenAuth>
    <!-- This is an optional configuration section where aspects of dotnetopenauth can be customized. -->
    <!-- For a complete set of configuration options see http://www.dotnetopenauth.net/developers/code-snippets/configuration-options/ -->
    <!--<messaging clockSkew="00:10:00" lifetime="00:03:00" strict="true">-->
    <!--<messaging>
      <untrustedWebRequest timeout="00:00:30" readWriteTimeout="00:00:01.500" maximumBytesToRead="1048576" maximumRedirections="10">
        <whitelistHosts>
          -->
    <!-- Uncomment to enable communication with localhost (should generally not activate in production!) -->
    <!--
          <add name="localhost"/>            
        </whitelistHosts>
      </untrustedWebRequest>
    </messaging>-->
    <!-- Allow DotNetOpenAuth to publish usage statistics to library authors to improve the library. -->
    <reporting enabled="false"/>
  </dotNetOpenAuth>
  <appSettings>
    <!--<add key="log4net.Internal.Debug" value="true" />-->
  </appSettings>
  <runtime>
  </runtime>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
  </startup>
</configuration>
Belfort answered 19/10, 2012 at 18:23 Comment(0)
B
11

The following code, corrected from my original question, is based on the example provided by Ian Fraser at:

https://groups.google.com/forum/#!msg/google-search-api-for-shopping/4uUGirzH4Rw/__c0e4hj0ekJ

His code addressed three issues:

  1. It appears as though AnalyticsService.Scopes.AnalyticsReadonly does not work, at least not for me or the way I am doing it.
  2. For some reason, the ServiceAccountUser must be assigned to the ServiceAccountId property of the AssertionFlowClient instance.
  3. OAuth2Authenticator provides the IAuthenticator that I was looking for.

In your project, include references to:

  • Lib\DotNetOpenAuth.dll
  • Lib\Google.Apis.dll
  • Lib\Google.Apis.Authentication.OAuth2.dll
  • Services\AnalyticsService\Google.Apis.Analytics.v3.dll

-

namespace GoogleAnalyticsAPITest.Console
{
    using System.Security.Cryptography.X509Certificates;
    using Google.Apis.Analytics.v3;
    using Google.Apis.Analytics.v3.Data;
    using Google.Apis.Authentication.OAuth2;
    using Google.Apis.Authentication.OAuth2.DotNetOpenAuth;
    using Google.Apis.Util;

    class Program
    {
        static void Main(string[] args)
        {
            log4net.Config.XmlConfigurator.Configure();            
            const string ServiceAccountId = "nnnnnnnnnnn.apps.googleusercontent.com";
            const string ServiceAccountUser = "[email protected]";
            AssertionFlowClient client = new AssertionFlowClient(
                GoogleAuthenticationServer.Description, new X509Certificate2(@"value-privatekey.p12", "notasecret", X509KeyStorageFlags.Exportable))
            {
                Scope = AnalyticsService.Scopes.AnalyticsReadonly.GetStringValue(),
                ServiceAccountId = ServiceAccountUser //Bug, why does ServiceAccountUser have to be assigned to ServiceAccountId
                //,ServiceAccountUser = ServiceAccountUser
            };
            OAuth2Authenticator<AssertionFlowClient> authenticator = new OAuth2Authenticator<AssertionFlowClient>(client, AssertionFlowClient.GetState);            
            AnalyticsService service = new AnalyticsService(authenticator);            
            string profileId = "ga:64968920";
            string startDate = "2010-10-01";
            string endDate = "2010-10-31";
            string metrics = "ga:visits";
            DataResource.GaResource.GetRequest request = service.Data.Ga.Get(profileId, startDate, endDate, metrics);
            request.Dimensions = "ga:date";
            GaData data = request.Fetch();            
        }

    }
}
Belfort answered 22/10, 2012 at 14:11 Comment(11)
It looks like the correct read only scope is googleapis.com/auth/analytics.readonly. That is different to converting the enumeration value to a string. Also, I didn't have to set the ServiceAccountUser.Anandrous
@BenMills can you clarify how you authenticated? I'm guessing that you are saying that you set the ServiceAccountId to a nnnnnnnnnnn.apps.googleusercontent.com value but I am not sure that is what you mean. I edited the code to set the scope to "analytics.readonly" since that is probably the primary way people will use this.Belfort
I'm sorry, you were correct. I set the ServiceAccountId to [email protected]. I didn't need to consider nnnnnnnnnnn.apps.googleusercontent.com in any way.Anandrous
In the namespace Google.Apis.Util there is an extension method for retreiving the string value from the AnalyticsService.Scopes enum, AnalyticsService.Scopes.AnalyticsReadonly.GetStringValue().Unbolted
@JohanPettersson Thank you very much for pointing out the extension method. It is odd that they did not simply make the AssertionFlowClient.Scope property of the same enum type and the fact that the extension is in a namespace separate from where the enum is declared is also odd. I updated the code based on your comment.Belfort
@RichardCollette maybe you should consider adding the flag X509KeyStorageFlags.MachineKeySet too? Otherwise the application pool identity needs a profile where it can store the certificate. new X509Certificate2(@"value-privatekey.p12", "notasecret", X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet)Unbolted
@JohanPettersson - point taken about the key. This was just a get off the ground and get data back example.Belfort
@Richard Collette Hi, I'm trying out this code but I'm getting the following error "best overloaded method match for.." one the line "new OAuth2Authenticator<AssertionFlowClient>(client, AssertionFlowClient.GetState);". How can I fix this?Tartrate
@Tartrate The code listed above works on my machine. There is not enough detail in your comment to discern what the cause might be. Check that you have the latest API version. Check your references. Trace assembly loading to insure the correct assemblies are loading. If you have modified or integrated the code above, make sure you have no namespace conflicts. Beyond that I cannot offer further assistance.Belfort
@Richard Collette Thanks I got it to working. Just getting an permission denied error now but that should be easy to fix. Question: can this code also be used to request data from youtube analytics api and youtube data api?Tartrate
Note that this example code uses obsolete code as of 2015. AssertionFlowClient has been removed for example. I suggest using Ramy's solution below instead: https://mcmap.net/q/390240/-how-do-i-use-a-service-account-to-access-the-google-analytics-api-v3-with-net-cGalvani
T
5

I was checking out the analytics API yesterday and noticed how undocumented it is and no samples etc.

Any ways, I have created a library that you could use to access analytics easily with couple of lines and make direct databinding to DataTables for data returned it's open source on the github so feel free to contribute :)

https://github.com/rmostafa/DotNetAnalyticsAPI

Usage

Analytics.AnalyticsManager manager = new Analytics.AnalyticsManager(Server.MapPath("~/bin/privatekey.p12"), "YOUR_EMAIL");
            manager.LoadAnalyticsProfiles();


List<Analytics.Data.DataItem> metrics = new List<Analytics.Data.DataItem>();
metrics.Add(Analytics.Data.Visitor.Metrics.visitors);
metrics.Add(Analytics.Data.Session.Metrics.visits);
List<Analytics.Data.DataItem> dimensions = new List<Analytics.Data.DataItem>();
dimensions.Add(Analytics.Data.GeoNetwork.Dimensions.country);
dimensions.Add(Analytics.Data.GeoNetwork.Dimensions.city);


System.Data.DataTable table = manager.GetGaDataTable(DateTime.Today.AddDays(-3), DateTime.Today, metrics, dimensions, null, metrics);

There is direct code mapping for All Google API Reporting commands categorized same way like the API so you could it even without reading the API Documentation at all since all features there are documented in the attributes, I have wrote code that parsed the complete api documentation and resourced the Metrics, Dimensions, Calculated Features in an XML that i generated from physical classes that you could use directly like the example above it's fun to play with :) enjoy

https://github.com/rmostafa/DotNetAnalyticsAPI

Tootsy answered 30/1, 2014 at 0:15 Comment(0)
G
3
string scope = Google.Apis.Util.Utilities.GetStringValue(AnalyticsService.Scopes.AnalyticsReadonly);
Giraffe answered 11/4, 2013 at 21:47 Comment(1)
This is an extension method. You do not need to call it this way. See the solution above.Belfort
J
2

Here is my working example posted here [1]: Automated use of google-api-dotnet-client with OAuth 2.0 I put a lot the research into finding and piecing the code together hope this saves you some time.

Janey answered 20/10, 2012 at 2:55 Comment(1)
I am sure you put a ton of time into this solution. It appears that you have had to go to a lower level than the API I am using. However, your example is using the NativeApplicationClient rather than the AssertionFlowClient. I appreciate the feedback.Belfort
F
1

Regarding Richard Collette answer, be careful that using his method if you want to use Analytics API in READONLY mode, the correct way to use it is:

string Scope = "analytics.readonly"

and not

string Scope = AnalyticsService.Scopes.AnalyticsReadOnly.ToString().ToLower()

as he seems to tell that there is a bug. In fact, the bug is that the .toString() method returns analyticsreadonly and NOT analytics.readonly which is the way that Google likes. That's it.

Fidellas answered 18/12, 2012 at 12:31 Comment(2)
Great information. I would say it's a bug in the enum then. What is the point of these enums if you cannot use them?Belfort
@RichardCollette In the namespace Google.Apis.Util there is an extension method for retreiving the string value from the AnalyticsService.Scopes enum, AnalyticsService.Scopes.AnalyticsReadonly.GetStringValue().Unbolted

© 2022 - 2024 — McMap. All rights reserved.