Integrating with Google Admin SDK in C# - c#

I'm currently trying to integrate with the Google Admin SDK via C# so we can manage users via our own system. However, when running the project I get the error: Unauthorized Client.
Things I have already done via a super admin account:
Setup Service Account
Enabled GSuite domain-wide delegation on service Account
Enabled API Access
Added the Service Accounts client ID to API Client Access with the scope (https://www.googleapis.com/auth/admin.directory.user)
Here's the code that i'm using.
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(_googleServiceSettings.Client_Email)
{
ProjectId = _googleServiceSettings.Project_Id,
User = "superadmin#google.com",
Scopes = new[] { DirectoryService.Scope.AdminDirectoryUser }
}.FromPrivateKey(_googleServiceSettings.Private_Key));
var service = new DirectoryService(new BaseClientService.Initializer
{
HttpClientInitializer = credential,
ApplicationName = "Test API"
});
var request = service.Users.Get("user#google.com");
var result = await request.ExecuteAsync();
The full error i'm getting is
An unhandled exception has occurred while executing the request.
Google.Apis.Auth.OAuth2.Responses.TokenResponseException: Error:"unauthorized_client", Description:"Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested.", Uri:""

Example code that will print some information about a user.
The important item is the class Google.Apis.Admin.Directory.directory_v1.Data.User
Documentation link.
Your error is caused by not creating the credentials correctly. Usually, an issue with scopes when creating the credentials. I am assuming that you have Domain-Wide Delegation setup correctly for the service account.
I am also assuming that the user that you are impersonating is a G Suite Super Admin. If not, you will see a 403 error for service.Users.Get().
The file service_account.json is a normal JSON file that you downloaded from the Google Console (or created with gcloud).
The user user1#example.com is the email address for the G Suite user for which information will be displayed.
The user admin#example.com is the G Suite Super Admin.
using Google.Apis.Auth.OAuth2;
using Google.Apis.Admin.Directory.directory_v1;
using Google.Apis.Admin.Directory.directory_v1.Data;
using Google.Apis.Services;
using System;
using System.IO;
// dotnet add package Google.Apis.Admin.Directory.directory_v1
// Tested with version 1.39.0.1505
// Google.Apis.Admin.Directory.directory_v1.Data.User
// https://developers.google.com/resources/api-libraries/documentation/admin/directory_v1/csharp/latest/classGoogle_1_1Apis_1_1Admin_1_1Directory_1_1directory__v1_1_1Data_1_1User.html
namespace Example
{
class Program
{
static void Main(string[] args)
{
// Service Account with Domain-Wide delegation
var sa_file = "service_account.json";
// G Suite User to impersonate
var user_email = "admin#example.com";
// G Suite User to get information about
var gs_email = "user1#example.com";
// Scopes
var scopes = "https://www.googleapis.com/auth/admin.directory.user";
var credential = GoogleCredential.FromFile(sa_file)
.CreateScoped(scopes)
.CreateWithUser(user_email);
// Create Directory API service.
var service = new DirectoryService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential
});
try {
var request = service.Users.Get(gs_email);
var result = request.Execute();
Console.WriteLine("Full Name: {0}", result.Name.FullName);
Console.WriteLine("Email: {0}", result.PrimaryEmail);
Console.WriteLine("ID: {0}", result.Id);
Console.WriteLine("Is Admin: {0}", result.IsAdmin);
} catch {
Console.WriteLine("User not found.");
}
}
}
}

If you want to use the service account you can authenticate with below code.
String serviceAccountEmail = "yourserviceaccountmail";
public GmailService GetService(string user_email_address)
{
var certificate = new X509Certificate2(#"yourkeyfile.p12",
"notasecret", X509KeyStorageFlags.Exportable);
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
User = user_email_address,
Scopes = new[] { GmailService.Scope.MailGoogleCom }
}.FromCertificate(certificate));
GmailService service = new GmailService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = AppName,
});
return service;
}
You can list users using this service. Its work for me.
And you can list userlist with below code. ( with DirectoryService)
public Users GetDirService()//UserList with DirectoryService
{
string Admin_Email = "yoursuperadminemail";
string domain = "yourdomain.com";
try
{
var certificate = new X509Certificate2(#"yourkeyfile.p12", "notasecret", X509KeyStorageFlags.Exportable);
ServiceAccountCredential credentialUsers = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
Scopes = new[] { DirectoryService.Scope.AdminDirectoryUser },
User = Admin_Email,
}.FromCertificate(certificate));
var serviceUsers = new DirectoryService(new BaseClientService.Initializer()
{
HttpClientInitializer = credentialUsers,
ApplicationName = AppName,
});
var listReq = serviceUsers.Users.List();
listReq.Domain = domain;
Users users = listReq.Execute();
return users;
}
catch (Exception ex)
{
MessageBox.Show("your mail address must be super admin authorized.", "Warning", MessageBoxButton.OK, MessageBoxImage.Warning);
return null;
}
}

Ok I have solved the issue.
Adding the following scope via the security settings within the Google Portal has solved the issue. This is strange as their own example doesn't require this scope to be added ad their documentation doesn't say it's required for this method.
https://www.googleapis.com/auth/admin.directory.group

Related

Error while trying to send email from google workspace from asp.net core web api app using Google.Apis.Gmail

I'm trying to send email from my google workspace account using this code:
using (var stream =
new FileStream("credentials.json", FileMode.Open, FileAccess.Read))
{
var credential = ServiceAccountCredential.FromServiceAccountData(stream);
service = new GmailService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = ApplicationName,
});
var email = MimeMessage.CreateFromMailMessage(new System.Net.Mail.MailMessage("EMAILADDRESS", destinationEmailAddress, "Verification code", $"Your verification code is {messageBody}"));
Message message = new Message();
byte[] blob;
using (var memory = new MemoryStream())
{
email.WriteTo(memory);
blob = memory.ToArray();
}
message.Raw = await credential.SignBlobAsync(blob);
await service.Users.Messages.Send(message, "me").ExecuteAsync();
}
but I get the following exception:
Google.Apis.Requests.RequestError\nPrecondition check failed. [400]\nErrors [\n\tMessage[Precondition check failed.] Location[ - ] Reason[failedPrecondition] Domain[global]\n]\n
what am I doing wrong? and is there a straight forward guide to do this right?
The issue you are having is that you are using Gmail with a service account and you have not properly configured domain wide delegation to a user on your workspace account. Follow this guide to configure your workspace account with the service account. Perform Google Workspace Domain-Wide Delegation of Authority
The following code should show you how to authorize it. Note the CreateWithUser method this sets up the user you wish to delegate the service account as.
class Program
{
private static readonly string[] Scopes = {GmailService.Scope.GmailSend};
private static readonly string PathToServiceAccountKeyFile =
#"C:\YouTube\workspaceserviceaccount-e4823a933ae3.json";
private static readonly string workspaceAdmin = "xxxx#daimto.com";
private static readonly string sendEmailTo = "xxxx#gmail.com";
static async Task Main(string[] args)
{
Console.WriteLine("Hello World!");
var credential = LoadGoogleCredentials();
var service = CreateDirectoryService(credential);
var mailMessage = new System.Net.Mail.MailMessage
{
From = new System.Net.Mail.MailAddress(workspaceAdmin),
ReplyToList = {workspaceAdmin},
To = {sendEmailTo},
Subject = "Welcome",
Body = "welcome new workspace user",
};
var mimeMessage = MimeMessage.CreateFromMailMessage(mailMessage);
var gmailMessage = new Message
{
Raw = Encode(mimeMessage)
};
var request = await service.Users.Messages.Send(gmailMessage, workspaceAdmin).ExecuteAsync();
Console.ReadLine();
}
public static string Encode(MimeMessage mimeMessage)
{
using (MemoryStream ms = new MemoryStream())
{
mimeMessage.WriteTo(ms);
return Convert.ToBase64String(ms.GetBuffer())
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
}
}
private static GmailService CreateDirectoryService(GoogleCredential credential)
{
return new(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "Daimto Testing Workspace with service account"
}
);
}
private static GoogleCredential LoadGoogleCredentials()
{
return GoogleCredential.FromFile(PathToServiceAccountKeyFile)
.CreateScoped(Scopes)
.CreateWithUser(workspaceAdmin);
}
}
Note: this code is the same for all types of applications even though my test code is a console app the methods used will work for your asp .net core app as well
from comments
Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested
this error message means that your servcie account is not authorized with the proper scope. the gmail send method requires a scope that allows access to sending emails GmailService.Scope.GmailSend for instance as i have use din my code remember to add it in workspace as well.
Full tutorial Gmail api with google workspace and .net

Getting permission error with service account for Google My Business (GMB), unsure if permissions or code is incorrect

OK so my problem is that I am trying to query GMB, I am just trying to get a response back so I'm trying to run a get call. However, I am getting "The caller does not have permission [403]". I am unsure why this would be so maybe I am doing something incorrectly.
I created a Service Account with "Owner" privileges, took the .p12 key from it and that's what I'm using in the code as well using that account's email address in the code for serviceAccountEmail. I am using a Service Account bc I just want to use GMB's location API Location Data so I don't need to connect to any users. Any help would be great! Thanks!
class Program
{
static void Main(string[] args)
{
Console.WriteLine("====================");
try
{
new Program().Run().Wait();
}
catch (AggregateException ex)
{
foreach (var e in ex.InnerExceptions)
{
Console.WriteLine("ERROR: " + e.Message);
}
}
Console.WriteLine("Press any key to continue...");
Console.ReadKey();
}
private async Task Run()
{
Console.WriteLine("GMB API - Location Data");
Console.WriteLine("==========================");
String serviceAccountEmail = "test#test-api.iam.gserviceaccount.com";
var certificate = new System.Security.Cryptography.X509Certificates.X509Certificate2(#"key.p12", "notasecret", X509KeyStorageFlags.Exportable);
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
Scopes = new[] { "https://www.googleapis.com/auth/plus.business.manage" }
}.FromCertificate(certificate));
// Create the service.
var service = new MyBusinessService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
});
var accountsListRequest = service.Accounts.List();
ListAccountsResponse accountsResult = accountsListRequest.Execute();
var account = service.Accounts.Get("accounts/12345").Execute();
Console.WriteLine("Press any key to continue...");
Console.ReadKey();
}
}
}
Serice accounts need to be preauthorized. Take the service account email address go to GMB share the account with the service account using the email address like you would share it with any other user.
The service account will then have access to the GMB in question
This is assuming that you are sure GMB even supports service accounts which I wasn't aware that it did
seeing as the documentation only appears to mention oauth2 i suspect it still doesn't and you should switch to using oauth2
https://developers.google.com/my-business/content/basic-setup

Can anyone confirm if ServiceAccount can be used to download Emails from any regular gmail account?

As per different threads here on stackoverflow, it seems that I can create a service account for any regular gmail account. But cannot assign permissions from the domain admin panel.
Can anyone confirm if ServiceAccount can be used to download Emails from any regular gmail account?
If I use the following code:
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
User = userEmail,
Scopes = new string[] { Gmail.v1.GmailService.Scope.GmailReadonly }
}.FromCertificate(certificate)
);
then I get the following error:
{"Error:\"unauthorized_client\", Description:\"Unauthorized client or scope in request.\", Uri:\"\""}
And If I remove the User object from the ServiceAccountCredential constructor then the error is gone. But have the following error when I use any google service:
{"Google.Apis.Requests.RequestError\r\nBad Request [400]\r\nErrors [\r\n\tMessage[Bad Request] Location[ - ] Reason[failedPrecondition] Domain[global]\r\n]\r\n"}
I did set the 'Viewer' role for the service account from the developer console and enabled the google APIs.
Is this anyway related to domain account or permissions for the domain account?
Full Code is as following:
String serviceAccountEmail = "gmailscraperserviceaccount2#gmailscraper-1221.iam.gserviceaccount.com";
var certificate = new X509Certificate2("key2.p12", "notasecret",
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable);
string userEmail = "abc#gmail.com";
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
Scopes = new string[] { Gmail.v1.GmailService.Scope.GmailReadonly }
}.FromCertificate(certificate)
);
if (credential.RequestAccessTokenAsync(CancellationToken.None).Result)
{
Google.Apis.Auth.OAuth2.Responses.TokenResponse toke = credential.Token;
GmailService gs = new GmailService(
new Google.Apis.Services.BaseClientService.Initializer()
{
ApplicationName = "TestApp",
HttpClientInitializer = credential
}
);
UsersResource.MessagesResource.ListRequest allMessageRequest = gs.Users.Messages.List(userEmail);
Google.Apis.Gmail.v1.Data.ListMessagesResponse response = allMessageRequest.Execute();

Can we access GMAIL API using Service Account?

I have a desktop application to read mail using GMAIL API over REST Interface. I want to use service account so that we can download the mails using domain setting and user interaction is null. I am successfully able to create Gmail Service instance but when I try to access any Gmail API method like fetching mail list or any other I get an exception saying
Google.Apis.Auth.OAuth2.Responses.TokenResponseException:
Error:"access_denied", Description:"Requested client not
authorized."
I am done with all the setting at developer console and added scopes to my gapps domain.
Does Gmail API support service account? Using the same setting and service account I am able to get list of all files in Google drive using Drive service and API.
I use the following C# code for accessing Gmail from Service Account
String serviceAccountEmail =
"999999999-9nqenknknknpmdvif7onn2kvusnqct2c#developer.gserviceaccount.com";
var certificate = new X509Certificate2(
AppDomain.CurrentDomain.BaseDirectory +
"certs//fe433c710f4980a8cc3dda83e54cf7c3bb242a46-privatekey.p12",
"notasecret",
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable);
string userEmail = "user#domainhere.com.au";
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
User = userEmail,
Scopes = new[] { "https://mail.google.com/" }
}.FromCertificate(certificate)
);
if (credential.RequestAccessTokenAsync(CancellationToken.None).Result)
{
GmailService gs = new GmailService(
new Google.Apis.Services.BaseClientService.Initializer()
{
ApplicationName = "iLink",
HttpClientInitializer = credential
}
);
UsersResource.MessagesResource.GetRequest gr =
gs.Users.Messages.Get(userEmail, msgId);
gr.Format = UsersResource.MessagesResource.GetRequest.FormatEnum.Raw;
Message m = gr.Execute();
if (gr.Format == UsersResource.MessagesResource.GetRequest.FormatEnum.Raw)
{
byte[] decodedByte = FromBase64ForUrlString(m.Raw);
string base64Encoded = Convert.ToString(decodedByte);
MailMessage msg = new MailMessage();
msg.LoadMessage(decodedByte);
}
}
Here is a little bit of python 3.7:
from google.oauth2 import service_account
from googleapiclient.discovery import build
def setup_credentials():
key_path = 'gmailsignatureproject-zzz.json'
API_scopes =['https://www.googleapis.com/auth/gmail.settings.basic',
'https://www.googleapis.com/auth/gmail.settings.sharing']
credentials = service_account.Credentials.from_service_account_file(key_path,scopes=API_scopes)
return credentials
def test_setup_credentials():
credentials = setup_credentials()
assert credentials
def test_fetch_user_info():
credentials = setup_credentials()
credentials_delegated = credentials.with_subject("tim#vci.com.au")
gmail_service = build("gmail","v1",credentials=credentials_delegated)
addresses = gmail_service.users().settings().sendAs().list(userId='me').execute()
assert gmail_service
If you want to "read mail" you'll need the newer Gmail API (not the older admin settings API that 'lost in binary' pointed out). Yes you can do this with oauth2 and the newer Gmail API, you need to whitelist the developer in Cpanel and create a key you can sign your requests with--it take a little bit to setup:
https://developers.google.com/accounts/docs/OAuth2ServiceAccount#formingclaimset
For C# Gmail API v1, you can use the following code to get the gmail service. Use gmail service to read emails. Once you create the service account in Google Console site, download the key file in json format. Assuming the file name is
"service.json".
public static GoogleCredential GetCredenetial(string serviceAccountCredentialJsonFilePath)
{
GoogleCredential credential;
using (var stream = new FileStream(serviceAccountCredentialJsonFilePath, FileMode.Open, FileAccess.Read))
{
credential = GoogleCredential.FromStream(stream)
.CreateScoped(new[] {GmailService.Scope.GmailReadonly})
.CreateWithUser(**impersonateEmail#email.com**);
}
return credential;
}
public static GmailService GetGmailService(GoogleCredential credential)
{
return new GmailService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "Automation App",
});
}
// how to use
public static void main()
{
var credential = GetCredenetial("service.json");
var gmailService = GetGmailService(credential);
// you can use gmail service to retrieve emails.
var mMailListRequest = gmailService.Users.Messages.List("me");
mMailListRequest.LabelIds = "INBOX";
var mailListResponse = mMailListRequest.Execute();
}
Yes you can... check the delegation settings...
https://developers.google.com/admin-sdk/directory/v1/guides/delegation#delegate_domain-wide_authority_to_your_service_account
Edit: Use the link Eric DeFriez shared.
You can access any user#YOUR_DOMAIN.COM mails/labels/threads etc. with the new Gmail API:
https://developers.google.com/gmail/api/
via service account with impersonation (service account is accessing api as if it was specific user from your domain).
See details here: https://developers.google.com/identity/protocols/OAuth2ServiceAccount
Here is relevant code in Dartlang:
import 'package:googleapis_auth/auth_io.dart' as auth;
import 'package:googleapis/gmail/v1.dart' as gmail;
import 'package:http/http.dart' as http;
///credentials created with service_account here https://console.developers.google.com/apis/credentials/?project=YOUR_PROJECT_ID
final String creds = r'''
{
"private_key_id": "FILL_private_key_id",
"private_key": "FILL_private_key",
"client_email": "FILL_service_account_email",
"client_id": "FILL_client_id",
"type": "service_account"
}''';
Future<http.Client> createImpersonatedClient(String impersonatedUserEmail, List scopes) async {
var impersonatedCredentials = new auth.ServiceAccountCredentials.fromJson(creds,impersonatedUser: impersonatedUserEmail);
return auth.clientViaServiceAccount(impersonatedCredentials , scopes);
}
getUserEmails(String userEmail) async { //userEmail from YOUR_DOMAIN.COM
var client = await createImpersonatedClient(userEmail, [gmail.GmailApi.MailGoogleComScope]);
var gmailApi = new gmail.GmailApi(client);
return gmailApi.users.messages.list(userEmail, maxResults: 5);
}

Requested client not authorized

I am trying to get google users from my domain using google service account.
But it throws error
Error:"access_denied", Description:"Requested client not authorized.", Uri:""
My code
X509Certificate2 certificate = new X509Certificate2(key_path,
"notasecret", X509KeyStorageFlags.Exportable);
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer("publickey.gserviceaccount.com")
{ Scopes = scopes,
User = "admin#domain.com"
}.FromCertificate(certificate));
var service = new DirectoryService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "appname",
});
service.Users.List().Domain = "domain.com";
Users results = service.Users.List().Execute();
Thanks in advance
The service account email address needs to have access the domain. Take the email and add it as a user just enough access that it can read should be good.
Also did you change this for posting?
"publickey.gserviceaccount.com"
A service account email looks more like this:
539621478854-imkdv94bgujcom228h3ea33kmkoefhil#developer.gserviceaccount.com
You need to give your service-account/API project access to your domain first.
Steps detailed in the docs here:
https://developers.google.com/admin-sdk/directory/v1/guides/delegation#delegate_domain-wide_authority_to_your_service_account
You need to specify the correct scopes you need in step 6 of those instructions, which would be https://www.googleapis.com/auth/admin.directory.user.readonly to access the list of users.
In addition for the Directory API to work you need to enable API access in the domain settings: https://developers.google.com/admin-sdk/directory/v1/guides/prerequisites#set_up_api
I was finally able to get this working. Here is the code I have
var grpReq = service.Groups.List();
grpReq.Domain = "mydomain.com";
Groups groups = grpReq.Execute();
IList<Group> gps = groups.GroupsValue;
var memReq=service.Members.List(groups.GroupsValue[0].Id);
Members members = memReq.Execute();
I am still not sure why creating a var object and then Execute() got this to work but the earlier code didn't work.
I still have the problem of the consent screen showing up for all users. I have the following code. I think the way I get the logged in user's email is incorrect. Any ideas?
string mymail = googleauth.GetUsersEmail(ExchangeCodeWithAccessAndRefreshToken().Access_Token);
string path = "d:\\c6b82065f26fbb0-privatekey.p12";
X509Certificate2 certificate = new X509Certificate2(
path,
"notasecret", X509KeyStorageFlags.Exportable);
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer("876131792-v824u6drpss#developer.gserviceaccount.com")
{
User = mymail,
Scopes = new[] { PlusService.Scope.UserinfoEmail, PlusService.Scope.UserinfoProfile, PlusService.Scope.PlusMe }
}.FromCertificate(certificate));
PlusService plus = new PlusService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "myapp"
});
Person profile = plus.People.Get("me").Execute();
string email = profile.Emails[0].Value;

Categories