When I connect to an Account using the Drive API, I get an email with Appname of 'QuickStart' - google-api

I have recently setup an account in Google APIs and Services. In this account I enabled Google Drive integration. In the Drive UI Integration section, I have setup the Application Name as per this screenshot: https://www.screencast.com/t/5VphcOkfAXyN
In the Credentials section which I setup for this application, I also entered my application name: https://www.screencast.com/t/3XudlhbE
When I connect to my account via OpenAuth, I get a confirmation email to the connected account, from Google, which tells me that 'Quickstart connected to your Google Account'. I am expecting that the email would tell me 'My App Name connected to your Google Account': https://www.screencast.com/t/fmSXswXS
I have searched through my Google Account settings but I can't find anywhere where it says 'Quickstart'. What do I need to do in order to ensure that my actual app name is the one that appears on the confirmation email, and in the 'Apps with access to your account' permissions page (https://www.screencast.com/t/hcHSkDr5)?
Here is my FlowMetadata as requested in the comments:
public class GoogleDriveAppFlowMetadata : FlowMetadata
{
private static readonly IAuthorizationCodeFlow flow =
new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = AppConfig.CloudStorage.Google.Drive.ClientId,
ClientSecret = AppConfig.CloudStorage.Google.Drive.ClientSecret,
},
Scopes = new[] { DriveService.Scope.Drive },
DataStore = new GoogleDriveDataStore(),
});
public override string AuthCallback
{
get { return #"/GoogleDriveAuthCallback/IndexAsync"; }
}
public override string GetUserId(Controller controller)
{
if (controller.HttpContext.Session[SessionConstants.CloudStorageAccount] == null)
throw new Exception("CloudStorageAccount was not populated!");
CloudStorageDto cs = (CloudStorageDto)controller.HttpContext.Session[SessionConstants.CloudStorageAccount];
return $"{controller.User.GetSessionToken()}_{cs.Id}";
}
public override IAuthorizationCodeFlow Flow
{
get { return flow; }
}
}

I found the setting in the OAuth Consent Screen tab - this is the one that I needed: https://screencast.com/t/cI4lZKmNWeKp

Related

Unable to add service account to a site added in google search console via an API

TLDR version
Is there an API to do this - https://support.google.com/webmasters/answer/7687615?hl=en, I want to be able to map service account to user using some API, (I am able to do it manually, but the list is long)
Long version
From what I understand there are 2 types of users
Type 1 - Normal user (human) logging in and using google search console
Type 2 - Google service accounts, used by application to pull data
Now I want to add several hundreds of site in Google Search Console, I found C# clients/API to do that.
I am able to add/list sites using normal user account using API, and then verify by using UI to see them getting added.
I am able (no error returned) to add/list sites using service accounts using API, but then unable to
see service account user being added in the user list of the site. But I still see the site when I call the list api
when pulling data for this site, I get errors
Google.Apis.Requests.RequestError
User does not have sufficient permission for site 'https://www.example.com/th-th/city/'. See also: https://support.google.com/webmasters/answer/2451999. [403]
Errors [Message[User does not have sufficient permission for site 'https://www.example.com/th-th/city/'. See also: https://support.google.com/webmasters/answer/2451999.] Location[ - ] Reason[forbidden] Domain[global]
It points me to this link - https://support.google.com/webmasters/answer/7687615?visit_id=1621866886080-4412438468466383489&rd=2 where I can use the UI and manually add my service account and then everything works fine.
But I want to do the same thing via API, because I will be having hundreds of sites to add to.
Please advice on how to go about this one?
Seems like this user also had similar problem, but no solution - How to connect Google service account with Google Search Console
CODE
This is the code I use to create site using normal user and client id/secret, here if I create a site I am able to see it on UI but the API (https://developers.google.com/webmaster-tools/search-console-api-original/v3/sites/add) does not have option to use service account.
public class WebmastersServiceWrapper
{
private string user = "realemail#example.com";
private readonly ClientSecrets _clientSecrets = new ClientSecrets
{
ClientId = "example.apps.googleusercontent.com",
ClientSecret = "example"
};
private readonly string[] _scopes = {
WebmastersService.Scope.WebmastersReadonly,
WebmastersService.Scope.Webmasters
};
public async Task<WebmastersService> GetWebmastersService()
{
var credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(_clientSecrets, _scopes, user, CancellationToken.None);
var service = new WebmastersService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "WebMasters API Sample",
});
return service;
}
}
public class WebMasterSiteService
{
private readonly WebmastersServiceWrapper _connection;
public WebMasterSiteService()
{
_connection = new WebmastersServiceWrapper();
}
public async Task<IEnumerable<string>> GetSites()
{
var service = await _connection.GetWebmastersService();
var sitesResponse = await service.Sites.List().ExecuteAsync();
return SiteMapper.MapSites(sitesResponse);
}
public async Task DeleteSite(string site)
{
var service = await _connection.GetWebmastersService();
var response = await service.Sites.Delete(site).ExecuteAsync();
return;
}
public async Task AddSite(string site)
{
var service = await _connection.GetWebmastersService();
var response = await service.Sites.Add(site).ExecuteAsync();
return;
}
}
Here is the piece of code where I create sites using service worker, it gets created somewhere (as when I call list I get it back) but when I query that site using other APIs it fails with this error
Google.Apis.Requests.RequestError
User does not have sufficient permission for site 'https://www.example.com/th-th/city/'. See also: https://support.google.com/webmasters/answer/2451999. [403]
Errors [
Message[User does not have sufficient permission for site 'https://www.example.com/th-th/city/'. See also: https://support.google.com/webmasters/answer/2451999.] Location[ - ] Reason[forbidden] Domain[global]
]
public class SearchConsoleServiceWrapper
{
private readonly string[] _scopes = {
SearchConsoleService.Scope.WebmastersReadonly,
SearchConsoleService.Scope.Webmasters
};
public SearchConsoleService GetWebmastersService()
{
using var stream = new FileStream("key-downloaded-from-console-cloud-google.json", FileMode.Open, FileAccess.Read);
var credential = GoogleCredential.FromStream(stream)
.CreateScoped(_scopes)
.UnderlyingCredential as ServiceAccountCredential;
return new SearchConsoleService(new BaseClientService.Initializer
{
HttpClientInitializer = credential
});
}
}
public class SiteService
{
private readonly SearchConsoleServiceWrapper _connection;
public SiteService()
{
_connection = new SearchConsoleServiceWrapper();
}
public async Task<IEnumerable<string>> GetSites()
{
var service = _connection.GetWebmastersService();
var sitesResponse = await service.Sites.List().ExecuteAsync();
return SiteMapper.MapSites(sitesResponse);
}
public async Task DeleteSite(string site)
{
var service = _connection.GetWebmastersService();
var response = await service.Sites.Delete(site).ExecuteAsync();
return;
}
public async Task AddSite(string site)
{
var service = _connection.GetWebmastersService();
var response = await service.Sites.Add(site).ExecuteAsync();
return;
}
}
Final thoughts
Maybe I am missing something simple, also I haven't found a way to establish a relationship between my google search console account and my service account. But when I use my service account and add it as a user manually on a site, everything works and I am able to query properly.

Error accessing Google Calendar using OAuth2.0. and service account: "Invalid impersonation prn email address."

I am trying to use Google Calendar API to access the calendar of various users in our organization calendars using OAuth2.0 and a service account but I get an error
"invalid_request" "Invalid impersonation prn email address.".
In the Google console I have:
- Created a project
- Created a service account and enabled "Domain wide delegation" and given the "Project Owner" role, then got a P12 key.
- In Security > Advanced settings > Authentication > Manage API client access I have given the serviceaccount access to https://www.googleapis.com/auth/calendar.readonly.
using System;
using System.Windows.Forms;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Calendar.v3;
using Google.Apis.Services;
using System.Security.Cryptography.X509Certificates;
namespace Google_Calendar
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button2_Click(object sender, EventArgs e)
{
string GoogleCertificate = #"testcalendar-209521-772939e76cae.p12"; // keyfile filename
string GoogleEmail = #"myserviceaccount#testcalendar-209521.iam.gserviceaccount.com"; // serviceaccount mail
string GoogleUser = "MyServiceAccount"; // serviceaccount name
string[] Scopes = new string[] { "https://www.googleapis.com/auth/calendar.readonly" };
X509Certificate2 certificate = new X509Certificate2(GoogleCertificate, "notasecret", X509KeyStorageFlags.Exportable);
ServiceAccountCredential credential = new ServiceAccountCredential( new ServiceAccountCredential.Initializer(GoogleEmail)
{
Scopes = Scopes,
User = GoogleUser
}.FromCertificate(certificate));
CalendarService service = new CalendarService(new BaseClientService.Initializer() { HttpClientInitializer = credential, ApplicationName = "testcalendar" });
string CalenderID = "mathias#mydomain.com";
var CalRequest = service.Events.List(CalenderID);
CalRequest.TimeMin = DateTime.Now.AddMonths(-1); //optional parameter
CalRequest.TimeMax = DateTime.Now.AddMonths(+1); //optional parameter
do
{
var events = CalRequest.Execute(); // here I get the error
foreach (var item in events.Items)
{
// do stuff
}
CalRequest.PageToken = events.NextPageToken;
} while (CalRequest.PageToken != null);
}
}
}
Any ideas what the problem might be? I think the problem is in my settings in Google somewhere. Do I miss a step there?
With some help from Google support I solved the problem(s).
1: Where I had used the service account user
string GoogleUser = "MyServiceAccount";
I should have used an impersonate user
string GoogleUser = "MyAdminUser";
2: When I added the scopes on my Admin Console, I added it by using the Service Account email, which then got translated visually to the ClientID of my Project and everything seemed to be ok. But it was not. When I instead used the ClientID everything worked correct.

Login Required 401 using Google ServiceAccountCredential using Google Admin Directory API

I have tried to follow the simple example listed here: https://developers.google.com/admin-sdk/directory/v1/quickstart/dotnet
The difference is I generated a Service Account Credential, and assigned it as a Delegate with the Role Project Owner, so it has full access. I also assigned it the proper namespaces for scopes.
Here it has access to orgunits which is what I'm trying to list in the Directory API
Here is my service account defined
Here are my credentials
I downloaded the JSON for the credential and added it to my project. I can confirm that the code loades the ServiceAccountCredential and successfully authenticates and gets an access token by inspecting the debugger.
But then I pass the credential to the Service Initializer, and when I create and execute a request it fails with
{"Google.Apis.Requests.RequestError\r\nLogin Required [401]\r\nErrors [\r\n\tMessage[Login Required] Location[Authorization - header] Reason[required] Domain[global]\r\n]\r\n"}
Here's the code:
using Google.Apis.Auth.OAuth2;
using Google.Apis.Services;
using System;
using System.Collections.Generic;
using System.IO;
namespace DirectoryQuickstart
{
class Program
{
static string[] Scopes = { DirectoryService.Scope.AdminDirectoryUser, DirectoryService.Scope.AdminDirectoryOrgunit };
static string ApplicationName = "slea-crm";
static string Secret = "gsuite-secret.json";
static void Main(string[] args)
{
ServiceAccountCredential sac = GoogleCredential.FromFile(Secret).CreateScoped(Scopes).UnderlyingCredential as ServiceAccountCredential;
var token = sac.GetAccessTokenForRequestAsync().Result;
// Create Directory API service.
var service = new DirectoryService(new BaseClientService.Initializer()
{
HttpClientInitializer = sac,
ApplicationName = ApplicationName,
});
OrgunitsResource.ListRequest request = service.Orgunits.List(customerId: "REDACTED");
IList<OrgUnit> orgUnits = request.Execute().OrganizationUnits;
if (orgUnits != null && orgUnits.Count > 0)
{
foreach (var orgUnit in orgUnits)
{
Console.WriteLine("{0} ({1})", orgUnit.Name, orgUnit.OrgUnitPath);
}
}
else
{
Console.WriteLine("No orgunits found.");
}
Console.Read();
}
}
}
Here is the content of my JSON secret (with redactions)
What am I missing here?
EDIT: OK, I breakpoint the code while it generates the request, and I can see that no where does it set the Authorization token bearer in the headers. Why? I would expect this HttpClientInitializer class to take care of that, since the API docs say it knows how to handle that, and every example on the internet I've found shows it just passing the credential into the service initializer. But when I walked through it, even though the credential has already been granted an access token and one exists within it, nowhere does the request have the header updated.
The only thing I can see is there is some way to add an HTTP request interceptor where possibly I could do this myself, but wow, this seems really...bizarre -- after all this work they did on the dotnet client SDK, I honestly could have just written direct to the HTTP API and it would have been a lot simpler and easier to follow.
The missing piece of the puzzle is this line:
ServiceAccountCredential sac = GoogleCredential.FromFile(Secret)
.CreateScoped(Scopes)
.UnderlyingCredential as ServiceAccountCredential;
Needs to be modified to this:
static string userName = "admin#yourdomain.com" // valid user in your org
ServiceAccountCredential sac = GoogleCredential.FromFile(Secret)
.CreateScoped(Scopes)
.CreateWithUser(userName)
.UnderlyingCredential as ServiceAccountCredential;
Java/Python/Go sample of doing similar is here: https://developers.google.com/admin-sdk/directory/v1/guides/delegation#create_the_service_account_and_its_credentials
This has been answered but adding more details here. If anyone wants to impersonate user to upload file on google drive using Service account. Follow these steps
Create Service Account
Enable Site Wide delegation for service account
Get Service account client ID
Enable Client ID to use Google Drive API using Google Admin Console->Manage API
Use the below C# code to upload file
public static DriveService GetService()
{
string[] scopes = new string[] { DriveService.Scope.Drive };
//"SERVICE_ACCOUNT_EMAIL_HERE";
String serviceAccountEmail = "test-417#elated-graph-261115.iam.gserviceaccount.com";
// Scope and user email id which you want to impersonate
var initializer = new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
Scopes = scopes,
User = "yourEmail#domain.com"
};
//get private key, from .JSON file
var credential = new ServiceAccountCredential(initializer.FromPrivateKey("-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCkHeAicu6uFQn0\n7KUVTjgZ68nQui8+c8NmKW8aW8vhkBIKfdewXFECiUlTMPyI+HXbubsCK5Dl2xBS\nnphLq6YyE0xEQxNFLYHwfUKuzGQ2rV+qObcZ0mLZjCaf+pw3YiRVuU6OtslLJKJH\n-----END PRIVATE KEY-----\n"));
// Create the service.
var service = new DriveService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "DriveAPI",
});
service.HttpClient.Timeout = TimeSpan.FromMinutes(100);
return service;
}
That's it, we are done above Code is using Impersonation/Delegation for uploading file on Google Drive using Service account
Reference : Upload file to Google Drive using Service Account in C# MVC (With Impersonation)

Google OAuth Api not redirecting on Login

I try to authenticate my user using Google authentication services
When i run this code on local server its working fine (It redirects to google login and after successful login its hit call back on redirectPath).
But when publish this code on Production server then its not working.
When I debug this code, I found its redirect and open the google login page on hosted environment(Where application is published).
here is my code - Please help
string redirecrPath = "http://localhost:1212/Admin/YouTubeIntegration/Success";
UserCredential credential;
using (var stream = new FileStream(Server.MapPath("/XmlFile/client_secrets.json"), FileMode.Open, FileAccess.Read))
{
GoogleAuth.RedirectUri = redirecrPath;
credential = await GoogleAuth.AuthorizeAsync(
GoogleClientSecrets.Load(stream).Secrets,
new[] { YouTubeService.Scope.Youtube, YouTubeService.Scope.YoutubeReadonly, YouTubeService.Scope.YoutubeUpload },
"user",
CancellationToken.None,
new FileDataStore(this.GetType().ToString())
);
}
Please let me know if you need more information.
Thanks in Advance
The code to login from a web page is not the same as the code to login with an installed application. Installed applications can spawn the login screen directly on the current machine. If you tried to do that on a webserver it wouldnt work the following is the code for using web login
using System;
using System.Web.Mvc;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Auth.OAuth2.Flows;
using Google.Apis.Auth.OAuth2.Mvc;
using Google.Apis.Drive.v2;
using Google.Apis.Util.Store;
namespace Google.Apis.Sample.MVC4
{
public class AppFlowMetadata : FlowMetadata
{
private static readonly IAuthorizationCodeFlow flow =
new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = "PUT_CLIENT_ID_HERE",
ClientSecret = "PUT_CLIENT_SECRET_HERE"
},
Scopes = new[] { DriveService.Scope.Drive },
DataStore = new FileDataStore("Drive.Api.Auth.Store")
});
public override string GetUserId(Controller controller)
{
// In this sample we use the session to store the user identifiers.
// That's not the best practice, because you should have a logic to identify
// a user. You might want to use "OpenID Connect".
// You can read more about the protocol in the following link:
// https://developers.google.com/accounts/docs/OAuth2Login.
var user = controller.Session["user"];
if (user == null)
{
user = Guid.NewGuid();
controller.Session["user"] = user;
}
return user.ToString();
}
public override IAuthorizationCodeFlow Flow
{
get { return flow; }
}
}
}
copied from here

How do I authenticate to CRM Online using a non-interactive user?

I'm creating a console application that needs to access data in CRM Online 2016. This will run as a scheduled job and not interactively. It appears that I need to use OAuth for authentication.
I've already done the following:
I created a non-interactive user in CRM for the purpose of this integration. - I've already registered my app with Azure AD and have the Client Id.
What I can't figure out is how to authenticate to the web services. I was led to this MSDN article:
https://msdn.microsoft.com/en-us/library/gg327838.aspx
It shows how to authenticate but the example it shows causes a window to pop up asking the user to type a user/password. This won't work for me since this application will not be run interactively. I've looked everywhere but have not been able to find any documentation that shows me how to authenticate without having that window pop up.
It seems like I should be able to use AuthenticationContext.AcquireTokenByAuthorizationCode but I can't find any good examples for how that should be used.
Any direction is much appreciated!
Add a NuGet Reference to Microsoft.CrmSdk.XrmTooling.CoreAssembly.
Include the connection string in your app config (can include username and password as well)
<connectionStrings>
<add name="CrmService" connectionString="Url=https://UniqueOrgName.crm.dynamics.com;AuthType=Office365;"/>
</connectionStrings>
Call this code:
private static CrmServiceClient CreateCrmConnection(string userName, string password)
{
var url = ConfigurationManager.ConnectionStrings["CrmService"].ConnectionString;
var client = new CrmServiceClient(string.Format("{0}UserName={1};Password={2};", url, userName, password));
if (client.IsReady)
{
return client;
}
else
{
// Display the last error.
Console.WriteLine("Error occurred: {0}", client.LastCrmError);
// Display the last exception message if any.
Console.WriteLine(client.LastCrmException.Message);
Console.WriteLine(client.LastCrmException.Source);
Console.WriteLine(client.LastCrmException.StackTrace);
throw new Exception("Unable to Connect to CRM");
}
}
If you have a properly configured app registration with ClientId and ClientSecret, along with a few other organization specific variables, you can authenticate with Azure Active Directory (AAD) to acquire an oauth token and construct an OrganizationWebProxyClient. I've never found a complete code example of doing this, but I have developed the following for my own purposes. Note that the token you acquire has an expiry of 1 hr.
internal class ExampleClientProvider
{
// Relevant nuget packages:
// <package id="Microsoft.CrmSdk.CoreAssemblies" version="9.0.2.9" targetFramework="net472" />
// <package id="Microsoft.IdentityModel.Clients.ActiveDirectory" version="4.5.1" targetFramework="net461" />
// Relevant imports:
// using Microsoft.IdentityModel.Clients.ActiveDirectory;
// using Microsoft.Crm.Sdk.Messages;
// using Microsoft.Xrm.Sdk;
// using Microsoft.Xrm.Sdk.Client;
// using Microsoft.Xrm.Sdk.WebServiceClient;
private const string TenantId = "<your aad tenant id>"; // from your app registration overview "Directory (tenant) ID"
private const string ClientId = "<your client id>"; // from your app registration overview "Application (client) ID"
private const string ClientSecret = "<your client secret>"; // secret generated in step 1
private const string LoginUrl = "https://login.microsoftonline.com"; // aad login url
private const string OrganizationName = "<your organization name>"; // check your dynamics login url, e.g. https://<organization>.<region>.dynamics.com
private const string OrganizationRegion = "<your organization region>"; // might be crm for north america, check your dynamics login url
private string GetServiceUrl()
{
return $"{GetResourceUrl()}/XRMServices/2011/Organization.svc/web";
}
private string GetResourceUrl()
{
return $"https://{OrganizationName}.api.{OrganizationRegion}.dynamics.com";
}
private string GetAuthorityUrl()
{
return $"{LoginUrl}/{TenantId}";
}
public async Task<OrganizationWebProxyClient> CreateClient()
{
var context = new AuthenticationContext(GetAuthorityUrl(), false);
var token = await context.AcquireTokenAsync(GetResourceUrl(), new ClientCredential(ClientId, ClientSecret));
return new OrganizationWebProxyClient(new Uri(GetServiceUrl()), true)
{
HeaderToken = token.AccessToken,
SdkClientVersion = "9.1"
};
}
public async Task<OrganizationServiceContext> CreateContext()
{
var client = await CreateClient();
return new OrganizationServiceContext(client);
}
public async Task TestApiCall()
{
var context = await CreateContext();
// send a test request to verify authentication is working
var response = (WhoAmIResponse) context.Execute(new WhoAmIRequest());
}
}
See also https://stackoverflow.com/a/54775571/185200 if you're encountering access denied issues, and verify you've properly configured / authenticated the app.

Resources