I am trying to make a web application is ASP.NET MVC 5 with which I can authenticate a user with a Google Account and then read data from his/her spreadsheets stored in Google Drive/Google Sheets.
I am using Google API to authenticate a user. After a user is successfully authenticated, I get the credentials back from Google in an object which is of type Google.Apis.Auth.OAuth2.Web AuthResult.UserCredential
I can then successfully create a service to list files from Drive using code similar to
var driveService = new DriveService(new BaseClientService.Initializer
{
HttpClientInitializer = result.Credential,
ApplicationName = "ASP.NET MVC Sample"
});
Now, I want to use GData API to read content from spreadsheets in Drive. For this to work, I need to have a SpreadsheetsService object and then set it's RequestFactory parameter to an instance of GOAuth2RequestFactory and this in turn needs OAuth2 parameters to be specified in an instance of class OAuth2Parameters.
How can I reuse the credentials obtained using the Google Api in GData API?
I am already doing the thing you want to do,
Code for how I passed the GData tokens
Issue with OAuth2 authentication with google spreadsheet
i.e. I use a single OAuth2 access/refresh token set. Using the same tokens for both gdata calls & drive API calls.
This is the code that finally worked for me
public class AppFlowMetadata : FlowMetadata
{
private static readonly IAuthorizationCodeFlow flow =
new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = "randomstring.apps.googleusercontent.com",
ClientSecret = "shhhhhh!"
},
Scopes = new[] { DriveService.Scope.Drive, "https://spreadsheets.google.com/feeds", "https://docs.google.com/feeds", "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile" },
DataStore = new FileDataStore("Drive.Api.Auth.Store")
});
public override string GetUserId(Controller controller)
{
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; } }
}
And then, in the controller, OAuth2 parameters were copied to GData
var result = await new AuthorizationCodeMvcApp(this, new AppFlowMetadata()).
AuthorizeAsync(cancellationToken);
OAuth2Parameters parameters = new OAuth2Parameters();
parameters.ClientId = "somestring.apps.googleusercontent.com";
parameters.ClientSecret = "shhhhhh!";
parameters.Scope = result.Credential.Token.Scope;
parameters.AccessToken = result.Credential.Token.AccessToken;
parameters.RefreshToken = result.Credential.Token.RefreshToken;
GOAuth2RequestFactory requestFactory = new GOAuth2RequestFactory(null, "MySpreadsheetIntegration-v1", parameters);
SpreadsheetsService service = new SpreadsheetsService("MySpreadsheetIntegration-v1");
service.RequestFactory = requestFactory;
Related
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.
I am using Google DRIVE API from .NET google client library and wanted to impersonate user from service account . I have read many other users facing same issue but none of fix worked for me. Below is detail.
Create service account and enabled domain wide delegation (more than 3 hours now).
Downloaded *.p12 file and noted down secret password
Added permission drive scope with service account client id
Using below code to create service and upload/get data from google drive
code
var certificate = new X509Certificate2(keyfilepath, "notasecret", X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet);
string[] scopes = new string[] {
DriveService.Scope.Drive,
DriveService.Scope.DriveFile,
DriveService.Scope.DriveAppdata,
DriveService.Scope.DriveMetadata
};
var credential = new ServiceAccountCredential(new ServiceAccountCredential.Initializer(serviceaccountemail)
{
Scopes = scopes,
User = "abcr#mydomain.com"
}.FromCertificate(certificate));
// Create the service.
var service = new DriveService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "Drive API Sample"
//HttpClientFactory = new ProxySupportedHttpClientFactory()
});
var keyname = "1231312.p12";
var newservicact = "asdfasdf#gsuite-migration-4564654.iam.gserviceaccount.com";
_service = this.AuthorizeServiceAccountwithMimic(newservicact,keyname);
Google.Apis.Drive.v3.Data.File body = new Google.Apis.Drive.v3.Data.File();
body.Name = System.IO.Path.GetFileName(_uploadFile);
body.Description = _descrp;
body.MimeType = GetMimeType(_uploadFile);
body.Properties = customcolumns;
byte[] byteArray = System.IO.File.ReadAllBytes(_uploadFile);
System.IO.MemoryStream stream = new System.IO.MemoryStream(byteArray);
try
{
FilesResource.CreateMediaUpload request = _service.Files.Create(body, stream, GetMimeType(_uploadFile));
request.SupportsTeamDrives = true;
request.ProgressChanged += Request_ProgressChanged;
request.ResponseReceived += Request_ResponseReceived;
request.Upload();
return request.ResponseBody;
}
I am getting below error on first chuck of data being sent in Request_ProgressChanged event.
When exeucting google API methods, it throw below errror
Error:"unauthorized_client", Description:"Client is unauthorized to
retrieve access tokens using this method.", Uri:""
I have checked many forums enabled DWD and aaded application scope also....
anyone any idea please help,
There are sevral ways to access google apis. using Oauth2 and using service accounts.
The client created and the code used for the service account is different from the client created for a browser application and the code used to authenticate with it. If you go to google developer console and create a browser client you can not use the service account code with this client. The same goes for service account code with a browser clinet id.
Error:"unauthorized_client", Description:"Client is unauthorized to retrieve access tokens using this method.", Uri:""
Basically means that the client you are using is not the correct type for the code you are using.
Go back to google developer console create a service account download the service account .json file
ServiceAccount.cs
using Google.Apis.Drive.v3;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Services;
using System;
using System.IO;
using System.Security.Cryptography.X509Certificates;
namespace GoogleSamplecSharpSample.Drivev3.Auth
{
public static class ServiceAccountExample
{
/// <summary>
/// Authenticating to Google using a Service account
/// Documentation: https://developers.google.com/accounts/docs/OAuth2#serviceaccount
/// </summary>
/// <param name="serviceAccountEmail">From Google Developer console https://console.developers.google.com</param>
/// <param name="serviceAccountCredentialFilePath">Location of the .p12 or Json Service account key file downloaded from Google Developer console https://console.developers.google.com</param>
/// <returns>AnalyticsService used to make requests against the Analytics API</returns>
public static DriveService AuthenticateServiceAccount(string serviceAccountEmail, string serviceAccountCredentialFilePath, string[] scopes)
{
try
{
if (string.IsNullOrEmpty(serviceAccountCredentialFilePath))
throw new Exception("Path to the service account credentials file is required.");
if (!File.Exists(serviceAccountCredentialFilePath))
throw new Exception("The service account credentials file does not exist at: " + serviceAccountCredentialFilePath);
if (string.IsNullOrEmpty(serviceAccountEmail))
throw new Exception("ServiceAccountEmail is required.");
// For Json file
if (Path.GetExtension(serviceAccountCredentialFilePath).ToLower() == ".json")
{
GoogleCredential credential;
using (var stream = new FileStream(serviceAccountCredentialFilePath, FileMode.Open, FileAccess.Read))
{
credential = GoogleCredential.FromStream(stream)
.CreateScoped(scopes);
}
// Create the Analytics service.
return new DriveService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "Drive Service account Authentication Sample",
});
}
else if (Path.GetExtension(serviceAccountCredentialFilePath).ToLower() == ".p12")
{ // If its a P12 file
var certificate = new X509Certificate2(serviceAccountCredentialFilePath, "notasecret", X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable);
var credential = new ServiceAccountCredential(new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
Scopes = scopes
}.FromCertificate(certificate));
// Create the Drive service.
return new DriveService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "Drive Authentication Sample",
});
}
else
{
throw new Exception("Unsupported Service accounts credentials.");
}
}
catch (Exception ex)
{
throw new Exception("CreateServiceAccountDriveFailed", ex);
}
}
}
}
If it still does not work then it means you have not properly configured domain wide delegation to the service account delegatingauthority
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
I have a web api in my organization built with aspnet core. We want to publish that api to be consumed by an android app, a mvc5 app and an aspnet core mvc6 app. How can I configure the web api in azure so that the apps that consume it don't ask to login. The web apps, are already protected with azure, but when I protect the web api with azure I get a 401 when I make a request to it. I don't know how to configure the app in azure or the code I must configure in the api. I've read a lot but I don't find a way to acomplish this. All I want is to login in my web app, and the web app starts to ask data to the web api through ajax. I should send in the ajax request some sort of bareer token, but i don`t know what config i must do in azure and in the apps. I hope you can help me.
After you protected the web API with Azure AD, we need to send to access token with request for the web API for authorization. And we can get the access token when the users call the web API from web app. Here is the code to acquire the token in the web app for your reference:
public async Task<IActionResult> Index()
{
AuthenticationResult result = null;
List<TodoItem> itemList = new List<TodoItem>();
try
{
string userObjectID = (User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value;
AuthenticationContext authContext = new AuthenticationContext(Startup.Authority, new NaiveSessionCache(userObjectID, HttpContext.Session));
ClientCredential credential = new ClientCredential(Startup.ClientId, Startup.ClientSecret);
result = await authContext.AcquireTokenSilentAsync(Startup.TodoListResourceId, credential, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
//
// Retrieve the user's To Do List.
//
HttpClient client = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, TodoListBaseAddress + "/api/todolist");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
HttpResponseMessage response = await client.SendAsync(request);
//
// Return the To Do List in the view.
//
if (response.IsSuccessStatusCode)
{
List<Dictionary<String, String>> responseElements = new List<Dictionary<String, String>>();
JsonSerializerSettings settings = new JsonSerializerSettings();
String responseString = await response.Content.ReadAsStringAsync();
responseElements = JsonConvert.DeserializeObject<List<Dictionary<String, String>>>(responseString, settings);
foreach (Dictionary<String, String> responseElement in responseElements)
{
TodoItem newItem = new TodoItem();
newItem.Title = responseElement["title"];
newItem.Owner = responseElement["owner"];
itemList.Add(newItem);
}
return View(itemList);
}
else
{
//
// If the call failed with access denied, then drop the current access token from the cache,
// and show the user an error indicating they might need to sign-in again.
//
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
var todoTokens = authContext.TokenCache.ReadItems().Where(a => a.Resource == Startup.TodoListResourceId);
foreach (TokenCacheItem tci in todoTokens)
authContext.TokenCache.DeleteItem(tci);
ViewBag.ErrorMessage = "UnexpectedError";
TodoItem newItem = new TodoItem();
newItem.Title = "(No items in list)";
itemList.Add(newItem);
return View(itemList);
}
}
}
catch (Exception ee)
{
if (HttpContext.Request.Query["reauth"] == "True")
{
//
// Send an OpenID Connect sign-in request to get a new set of tokens.
// If the user still has a valid session with Azure AD, they will not be prompted for their credentials.
// The OpenID Connect middleware will return to this controller after the sign-in response has been handled.
//
return new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme);
}
//
// The user needs to re-authorize. Show them a message to that effect.
//
TodoItem newItem = new TodoItem();
newItem.Title = "(Sign-in required to view to do list.)";
itemList.Add(newItem);
ViewBag.ErrorMessage = "AuthorizationRequired";
return View(itemList);
}
//
// If the call failed for any other reason, show the user an error.
//
return View("Error");
}
And below is the code sample which use JwtBearerAppBuilderExtensions to add OpenIdConnect Bearer authentication capabilities to an HTTP application pipeline for the web API to verify the token:
public class Startup
{
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// Add the console logger.
loggerFactory.AddConsole(LogLevel.Debug);
// Configure the app to use Jwt Bearer Authentication
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
Authority = String.Format(Configuration["AzureAd:AadInstance"], Configuration["AzureAD:Tenant"]),
Audience = Configuration["AzureAd:Audience"],
});
}
}
The full code sample you can refer here.
Note: to run this sample successfully, we need to modify the Title and Owner to lowercase title, owner in the ToDoController of web app:
foreach (Dictionary<String, String> responseElement in responseElements)
{
TodoItem newItem = new TodoItem();
newItem.Title = responseElement["title"];
newItem.Owner = responseElement["owner"];
itemList.Add(newItem);
}
You can use Azure OpenIdConnect for federated authentication. A good article from microsoft below -
Calling a web API in a web app using Azure AD and OpenID Connect
I've been doing a lot tinkering around with the authentication stuff using the .NET libraries provided by Google.
We have both a desktop and web-app side, and what we want to achieve is to authenticate ONCE, either on the desktop or the web side, and store the refresh token, and reuse it both on the web side and the desktop side.
So the situation is like so, on the desktop side, when there's no saved existing AccessToken's and RefreshToken's, we will ask the user to authenticate via this code:
using (var stream = new FileStream("client_secrets_desktop.json", FileMode.Open, FileAccess.Read))
{
credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(GoogleClientSecrets.Load(stream).Secrets,
new[] { GmailService.Scope.GmailReadonly, GmailService.Scope.GmailCompose },
"someemail#gmail.com", CancellationToken.None);
}
In this case the Client ID and Secret is of an Application type Installed Application.
On the web-application side, if there's also no refresh token yet then I'm using DotNetOpenAuth to trigger the authentication, here's the code snippet:
const string clientID = "someclientid";
const string clientSecret = "somesecret";
const string redirectUri = "http://localhost/Home/oauth2callback";
AuthorizationServerDescription server = new AuthorizationServerDescription
{
AuthorizationEndpoint = new Uri("https://accounts.google.com/o/oauth2/auth"),
TokenEndpoint = new Uri("https://accounts.google.com/o/oauth2/token"),
ProtocolVersion = ProtocolVersion.V20
};
public ActionResult AuthenticateMe()
{
List<string> scope = new List<string>
{
GmailService.Scope.GmailCompose,
GmailService.Scope.GmailReadonly,
GmailService.Scope.GmailModify
};
WebServerClient consumer = new WebServerClient(server, clientID, clientSecret);
// Here redirect to authorization site occurs
OutgoingWebResponse response = consumer.PrepareRequestUserAuthorization(
scope, new Uri(redirectUri));
response.Headers["Location"] += "&access_type=offline&approval_prompt=force";
return response.AsActionResult();
}
public void oauth2callback()
{
WebServerClient consumer = new WebServerClient(server, clientID, clientSecret);
consumer.ClientCredentialApplicator =
ClientCredentialApplicator.PostParameter(clientSecret);
IAuthorizationState grantedAccess = consumer.ProcessUserAuthorization(null);
string accessToken = grantedAccess.AccessToken;
}
Here is where I want to confirm my suspicions.
When there is a RefreshToken that exists, we use the following code snippet to call the Gmail API's
UserCredential uc = new UserCredential(flow, "someemail#gmail.com", new TokenResponse()
{
AccessToken = "lastaccesstoken",
TokenType = "Bearer",
RefreshToken = "supersecretrefreshtoken"
});
var refreshState = await uc.RefreshTokenAsync(CancellationToken.None);
var svc = new GmailService(new BaseClientService.Initializer()
{
HttpClientInitializer = uc,
ApplicationName = "Gmail Test",
});
Here's the thing I noticed is that, for me to be able to use the refresh token to refresh from either the desktop or the web side, the refresh token needs to be generated through the same client ID/secret combination. I've tested it and it seems like it's fine if we use Installed application as the application type for the Client ID for both the desktop and the web, but my question I guess is, these application type's for the client IDs, do they matter so much?
Am I doing anything wrong to do it this way?
Thanks in advance