Create Google Calendar Event With Credentials To Impersonate User with Google Calendar API Library .NET 7 C# - c#

In .NET 7, without specifying a User in the
ServiceAccountCredential.Initializer(original.Id)
block, my calendar event can be created with no errors. But if I specify a user as shown below, I get this error:
Error: unauthorized_client,
Description: Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested.
My service account has domain-wide delegation, allowed scope of
calendar via OAuth2, and I have specifically granted the SA access to the user's calendar (when I try on accounts that have not specifically granted access, I can't create a calendar entry at all).
How do I get appropriate credentials for the service account to impersonate the end user within my domain?
Credit for getting this far to creating-a-serviceaccountcredential-for-a-user-from-a-systems-account
Previous answers don't seem to be working on latest release.
In my API code, which calls the authentication service then creates the event:
Google.Apis.Calendar.v3.CalendarService CS = await SA.AuthenticateServiceAccountSO();
var result = CS.Events.Insert((Event)newEvent, clientemail).Execute();
In Service AuthenticateServiceAccountSO():
public async Task<CalendarService> AuthenticateServiceAccountSO()
{
await Task.Delay(1);
string? serviceAccountCredentialFilePath = _config.GetValue<string>("ServiceAccountFilePath");
string? ClientEmail = _config.GetValue<string>("TestingUserAccountEmail");
string[] scopes = new string[] { CalendarService.Scope.Calendar };
ServiceAccountCredential original = (ServiceAccountCredential)GoogleCredential.FromFile(serviceAccountCredentialFilePath).UnderlyingCredential;
var initializer = new ServiceAccountCredential.Initializer(original.Id)
{
User = ClientEmail,
Key = original.Key,
Scopes = scopes
};
var credential = new ServiceAccountCredential(initializer);
CalendarService service = new(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "Calendar_Appointment event Using Service Account Authentication"
});
return service;
}

You're currently copying relatively little of the service account credential - in particular, there's no KeyId. But you can do it all much more simply anyway:
var credential = GoogleCredential.FromFile(serviceAccountCredentialFilePath)
.CreateScoped(CalendarService.Scope.Calendar)
.CreateWithUser(ClientEmail);

Related

Authentication with p12 key give error User cannot access account

I have a MVC web-application that update the products on the eCommerce site. Now we enrolled into the google merchant center and my objective is to update the products at the same time. I am using the Google.Apis.ShoppingContent.v2_1 API.
This is my API Credentials
This is my API service account
I have used the google account email address for the user as well as the service account email but with the same result.
I have the following
static string[] Scopes = { ShoppingContentService.Scope.Content};
static string P12Secret = #"~\Content\XXXXXX-5cab03fb904a.p12";
static string userName = "serviceaccount#gserviceaccount.com";
static public async Task RunTest2()
{
var certificate = new X509Certificate2(P12Secret, "notasecret", X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable);
var credential = new ServiceAccountCredential(new ServiceAccountCredential.Initializer(userName)
{
Scopes = Scopes
}.FromCertificate(certificate));
var service = new ShoppingContentService(new BaseClientService.Initializer
{
ApplicationName = ApplicationName,
HttpClientInitializer = credential
});
try
{
var result = await service.Products.List("My MerchantID").ExecuteAsync();
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
When I execute var result = await service.Products.List("My MerchantID").ExecuteAsync(); I get the error
e.Message = "Google.Apis.Requests.RequestError\nUser cannot access account 123456789 [401]\r\nErrors [\r\n\tMessage[User cannot access account 123456789 ] Location[ - ] Reason[auth/account_access_denied] Domain[content.ContentErrorDomain]\r\n]\r\n"
Documentaiton
Service accounts are special Google accounts that can be used by applications to access Google APIs programmatically via OAuth 2.0. A service account uses an OAuth 2.0 flow that does not require human authorization. Instead, it uses a key file that only your application can access. This guide discusses how to access the Content API for Shopping with service accounts.
Service accounts need to be pre authorized. If its not then it doesnt have access to any data.
User cannot access account 123456789
Means that it does not have access you have forgotten to grant it access. Check the Documentaiton look for the section below follow all of the steps.

B2B access in multi tenant application

I have succesfully setup a multi tenant application.
For now, I am able to authenticate the user and use tokens to access other resources. (Microsoft Graph & Microsoft AD Graph)
Now I want to get B2B working.
Current flow:
- User signs in
- AuthorizationCodeReceived gets the acquires the token (via $commonAuthority endpoint)
- When requesting a token for the Ad Graph, I am using the $tenantAuthority
This works perfectly when $tenantAuthority is the same tenant authority as the one where the account was created in.
However, if I login with another user (from another tenant, given trust to the actual tenant) and use $tenantAuthority = trusted authority, then I always the following error:
Failed the refresh token:
AADSTS65001: The user or administrator has not consented to use the application with ID
If I change $tenantAuthority to the 'source' tenant authority where the user was created in, everything works fine.
Any help would be greatly appreciated.
Update: Code sample
App has two tenants (tenantA en tenantB) and I will use a user from tenantB with tenantA given a trust to this user.
AuthorizationCodeReceived = async context =>
{
TenantContext.TenantId = "someguid";
var tenantId =
TenantContext.TenantId;
// get token cache via func, because the userid is only known at runtime
var getTokenCache = container.Resolve<Func<string, TokenCache>>();
var userId = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.ObjectIdentifier).Value;
var tokenCache = getTokenCache(userId);
var authenticationContext = new AuthenticationContext($"{configuration.Authority}",
tokenCache);
await authenticationContext.AcquireTokenByAuthorizationCodeAsync(
context.Code,
new Uri(context.Request.Uri.GetLeftPart(UriPartial.Authority)),
new ClientCredential(configuration.ClientId, configuration.ClientSecret),
configuration.GraphResourceId);
}
This code works perfectly. Login in with a user from both tenants works perfectly.
But when I need the Graph Service Client or ActiveDirectoryClient, I need to obtain access tokens to been able to address an api for a certain tenant. I retrieve the access tokens like this:
public IGraphServiceClient CreateGraphServiceClient()
{
var client = new GraphServiceClient(
new DelegateAuthenticationProvider(
async requestMessage =>
{
Logger.Debug("Retrieving authentication token to use in Microsoft Graph.");
string token;
var currentUserHomeTenantId = TenantContext.TenantId;
var currentUserObjectId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.ObjectIdentifier).Value;
var authenticationContext =
new AuthenticationContext($"{_configuration.TenantAuthorityPrefix}{currentUserHomeTenantId}",
_tokenCacheFactoryMethod(currentUserObjectId));
var clientCredential = new ClientCredential(_configuration.ClientId, _configuration.ClientSecret);
try
{
token = await GetTokenSilently(authenticationContext, _configuration.GraphResourceId, currentUserObjectId);
}
catch (AdalSilentTokenAcquisitionException e)
{
Logger.Error("Failed to retrieve authentication token silently, trying to refresh the token.", e);
var result = await authenticationContext.AcquireTokenAsync(_configuration.GraphResourceId, clientCredential);
token = result.AccessToken;
}
requestMessage.Headers.Authorization = new AuthenticationHeaderValue(AuthenticationHeaderKeys.Bearer, token);
}));
return client;
}
public IActiveDirectoryClient CreateAdClient()
{
var currentUserHomeTenantId = TenantContext.TenantId;
var currentUserObjectId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.ObjectIdentifier).Value;
var graphServiceUrl = $"{_configuration.AdGraphResourceId}/{currentUserHomeTenantId}";
var tokenCache = _tokenCacheFactoryMethod(currentUserObjectId);
var client = new ActiveDirectoryClient(new Uri(graphServiceUrl),
() => GetTokenSilently(
new AuthenticationContext(
$"{_configuration.TenantAuthorityPrefix}{ClaimsPrincipal.Current.FindFirst(ClaimTypes.TenantId).Value}", tokenCache
),
_configuration.AdGraphResourceId, currentUserObjectId
));
return client;
}
When I do a request with one of the two client SDK's, I got the following error:
Failed the refresh token: AADSTS65001: The user or administrator has not consented to use the application with ID.
Changing the catch method when retrieving the Token did the trick:
if(e.ErrorCode == "failed_to_acquire_token_silently")
{
HttpContext.Current.Response.Redirect(authenticationContext.GetAuthorizationRequestUrlAsync(resourceId, _configuration.ClientId, new Uri(currentUrl),
new UserIdentifier(currentUserId, UserIdentifierType.UniqueId), string.Empty);
}
I don't see that you mention that so: in a B2B collaboration you've to invite user from other tenant first. The steps are like that:
invite and authorize a set of external users by uploading a comma-separated values - CSV file
Invitation will be send to external users.
The invited user will either sign in to an existing work account with Microsoft (managed in Azure AD), or get a new work account in Azure AD.
After signed in, user will be redirected to the app that was shared with them
That works perfectly in my case.
Regarding some problems which I've detect:
Trailing "/" at the end of the active directory resource - try to remove it as this may cause problems. Bellow you will find some code to get authentication headers:
string aadTenant = WebServiceClientConfiguration.Settings.ActiveDirectoryTenant;
string clientAppId = WebServiceClientConfiguration.Settings.ClientAppId;
string clientKey = WebServiceClientConfiguration.Settings.ClientKey;
string aadResource = WebServiceClientConfiguration.Settings.ActiveDirectoryResource;
AuthenticationContext authenticationContext = new AuthenticationContext(aadTenant);
ClientCredential clientCredential = new ClientCredential(clientAppId, clientKey);
UserPasswordCredential upc = new UserPasswordCredential(WebServiceClientConfiguration.Settings.UserName, WebServiceClientConfiguration.Settings.Password);
AuthenticationResult authenticationResult = await authenticationContext.AcquireTokenAsync(aadResource, clientAppId, upc);
return authenticationResult.CreateAuthorizationHeader();
Applications provisioned in Azure AD are not enabled to use the OAuth2 implicit grant by default. You need to explicitly opt in - more details can be found here: Azure AD OAuth2 implicit grant

How to retrive events from google calendar API fro c# .net using API key

I am able to retrieve event from google calendar API using OAuth authentication by code given here, but how to code for getting event information using API key.
API key is for use with PUBLIC data. For an API key to work with a calendar I would assume said calendar would have to be set to public.
var service = new CalendarService(new BaseClientService.Initializer()
{
ApiKey = "XXX",
ApplicationName = "xyz",
});
var events = service.Events.List("en.danish#holiday#group.v.calendar.google.com").Execute();
foreach (var myEvent in events.Items)
{
Console.WriteLine(string.Format("Event: {0} Start: {1} End: {2}", myEvent.Summary, myEvent.Start.DateTime.ToString(), myEvent.End.DateTime.ToString()));
}
I have an example / tutorial of accessing events on public calendars here
I think you should consider looking at a service account instead this will allow you to grant the service account access to your calendar and you wont need to go though the Oauth2 authentication which I suspect is what you don't want to be doing.
string[] scopes = new string[] {
CalendarService.Scope.Calendar, // Manage your calendars
CalendarService.Scope.CalendarReadonly // View your Calendars
};
var certificate = new X509Certificate2(keyFilePath, "notasecret", X509KeyStorageFlags.Exportable);
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(serviceAccountEmail) {
Scopes = scopes
}.FromCertificate(certificate));
I have a tutorial on how to us a service account here

Is there a way to reuse Google.Apis.Auth.OAuth2.UserCredential object to authorize SpreadsheetsService?

I'm trying to use the Google Drive and Spreadsheets APIs from a C# console app. I'd like to authorize both services using user credentials with a FileDataStore so that I don't have to reauth my app every single time it runs. Below is how I'm authorizing my Drive service object:
var userCredential = GoogleWebAuthorizationBroker.AuthorizeAsync
(
new ClientSecrets
{
ClientId = "[clientID]",
ClientSecret = "[clientSecret]"
},
new []
{
"https://www.googleapis.com/auth/drive",
"https://spreadsheets.google.com/feeds"
},
"[userName]",
CancellationToken.None,
new FileDataStore("MyApp.GoogleDrive.Auth.Store")
).Result;
var driveService = new DriveService
(
new BaseClientService.Initializer
{
HttpClientInitializer = userCredential,
ApplicationName = "MyApp",
}
);
For the Spreadsheets service, I'm authorizing as prescribed by this guide, but every time I run my app, I have to open a browser to the given auth URL and manually copy in the access token to get it to work.
Is there a way to auth once, obtain the user credentials as above, and use them with both services? Note, I'm authorizing with both the Drive and the Spreadsheets scope, so I don't think there's a problem with that.
I've tried to make it work like this, but I keep getting 400 Bad Request errors when I attempt to insert rows into my spreadsheet:
var auth = new OAuth2Parameters
{
ClientId = "[clientID]",
ClientSecret = "[clientSecret]",
RedirectUri = "[redirectUri]",
Scope = "https://www.googleapis.com/auth/drive https://spreadsheets.google.com/feeds" ,
AccessToken = userCredential.Token.AccessToken,
RefreshToken = userCredential.Token.RefreshToken,
TokenType = userCredential.Token.TokenType,
};
var requestFactory = new GOAuth2RequestFactory(null, "MyApp", auth);
var spreadsheetsService = new SpreadsheetsService("MyApp")
{
Credentials = new GDataCredentials(userCredential.Token.TokenType + " " + userCredential.Token.AccessToken),
RequestFactory = requestFactory,
};
Is there a way to auth once, obtain the user credentials as above, and use them with both services?
Yes. Provided you have included all scopes and have requested offline access, then you'll get a refresh token which you can store and reuse to get access tokens as needed. Obv you need to consider the security implications.
A 400 bad request doesn't sound like an OAuth issue. I think you have two questions/issues here and it might be worth starting a new thread. Include the http request/response for the 400 in your question.

My Issue with Google Authentication

I have been using the old version of Google Calendar GData API (v1, v2) since November 2011 in my ASP.NET Applications, allowing Users to retrieve and/or create Calendar Events after submitting their usernames and passwords , and this was working perfectly till 17th of November 2014 just before Google decided to shut down this version of API as announced Calendar GData API / Google Calendar Connectors deprecation
Now I am stuck with the new version of Google APIS Calendar (v3) which forces me to use different scenario of Authentication Process instead of the Traditional one. I don't mind at all using this version of Calendar API as it supports all the needed features but now i don't know how to handle multiple users authentication to use their User Client's ID and Secret which are registered per each user Code Console.
So my question is : Is there any way to let user sign in with his/her normal credentials (Either by Username/Password or Google+ Sign UP feature) and bypassing the process of creating API Project, Enabling the needed APIs and creating new User credentials inside the Console through ASP.net code?
Any Sample code made in C# ASP.net is highly appreciated .
EDIT: Here is my code of Authentication I use
public static CalendarService Authenticate()
{
CalendarService service;
GoogleAuthorizationCodeFlow flow;
string json_File = System.Configuration.ConfigurationManager.AppSettings["Authentication_Path"];
string store_path = System.Configuration.ConfigurationManager.AppSettings["FileStore_Path"];
string url = System.Configuration.ConfigurationManager.AppSettings["Authent_URL"];
using (var stream = new FileStream(json_File, FileMode.Open, FileAccess.Read))
{
flow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
DataStore = new FileDataStore(store_path),
ClientSecretsStream = stream,
Scopes = new[] { CalendarService.Scope.Calendar }
});
}
var uri = url;
var result = new AuthorizationCodeWebApp(flow, uri, uri).AuthorizeAsync("TRAININGCALENDAR", CancellationToken.None).Result;
if (result.Credential == null)
{
GoogleCalendar_Bus.Main_Authentication(url, "", "");
}
// The data store contains the user credential, so the user has been already authenticated.
service = new CalendarService(new BaseClientService.Initializer
{
ApplicationName = "Calendar API Sample",
HttpClientInitializer = result.Credential
});
if (result.Credential != null)
{
service = new CalendarService(new BaseClientService.Initializer
{
ApplicationName = "Calendar API Sample",
HttpClientInitializer = result.Credential
});
}
return service;
}
No you need to use Oauth2. When they authenticate you just save the refresh token this will allow you to then get a new access token and you will have access again. You will need to make your own implementation of Idatastore to store these refresh tokens in the database.
The code for creating an implementation of a Idatastore that stores to the Database is to extensive to post here but you can see a basic example here: DatabaseDataStore.cs
Then you can use it like this.
credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
new ClientSecrets { ClientId = _client_id
,ClientSecret = _client_secret }
,scopes
,Environment.UserName
,CancellationToken.None
,new DatabaseDataStore(#"LINDAPC\SQL2012", "LindaTest", "test123", "test", "test")).Result;
Update: Now that I can see your code.
Make sure you have the latested .net client lib. Google.Apis.Calendar.v3 Client Library
your code is using FileDataStore this is what you will need to change. You need to make your own implementation of Idatastore similar to the one I have created DatabaseDatastore.
Your code looks different from how I normally do it.
string[] scopes = new string[] {
CalendarService.Scope.Calendar , // Manage your calendars
CalendarService.Scope.CalendarReadonly // View your Calendars
};
try
{
// here is where we Request the user to give us access, or use the Refresh Token that was previously stored in %AppData%
UserCredential credential = GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets { ClientId = clientId, ClientSecret = clientSecret }
, scopes
, userName
, CancellationToken.None
, new FileDataStore("Daimto.GoogleCalendar.Auth.Store")).Result;
CalendarService service = new CalendarService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "Calendar API Sample",
});
return service;
}
catch (Exception ex)
{
Console.WriteLine(ex.InnerException);
return null;
}
This may be due to the fact that you aren't using the most up to date client lib, you can find a sample console application for Google Calendar here unfortunately it also uses FileDatastore you will have to edit it to use DatabaseDataStore. The authentication sample project can be found here Google-Dotnet-Samples/Authentication/ it shows how you can create your own implementation of Idatastore.
I am still working on the tutorial to go along with that sample project I hope to have it completed soon.

Categories

Resources