Login Required 401 using Google ServiceAccountCredential using Google Admin Directory API - google-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)

Related

How to implement Exchange online OAuth2.0 for unmanaged EWS API?

For managed EWS code, I have used to OAuth 2.0 to get token and it worked.
For unmanaged EWS, it is failing to connect to Exchange as an unauthorized error.
Below is the code to access unmanaged EWS.
How to make below code work with OAuth token instead of passing credentials as below?.
Binding = new ExchangeServiceBinding
{
Url = ServerUrl,
Credentials = new OAuthCredentials(token),
RequestServerVersionValue = new RequestServerVersion { Version = ExchangeVersionType.Exchange2007_SP1 },
ExchangeImpersonation = null
};
Above is not working as credential is asking of type ICredentials and it is not accepting token. Please help me.
Below is the code how I direct access managed EWS.
var authResult = await pca.AcquireTokenByUsernamePassword(ewsScopes, credential.UserName, credential.SecurePassword).ExecuteAsync();
configure the ExchangeService with the access token
ExchangeService = new ExchangeService();
ExchangeService.Url = new Uri(ServerUrl);
ExchangeService.Credentials = new OAuthCredentials(authResult.AccessToken);
One method i use (as I've never worked out how to override the WSDL classes) is if you modify the Reference.cs file that gets generated in the web references directory you can modify the GetWebResponse command (In this case the token is being passed via the credentials object password property but there a number of different approaches you can take here) eg
private String AnchorMailbox;
private bool oAuth;
protected override System.Net.WebResponse GetWebResponse(System.Net.WebRequest req)
{
if (xAnchorMailbox != null)
{
if (xAnchorMailbox != "")
{
req.Headers.Add("X-AnchorMailbox", AnchorMailbox);
}
}
if(req.Credentials is System.Net.NetworkCredential)
{
if(oAuth){
req.Headers.Add("Authorization", ("Bearer " + ((System.Net.NetworkCredential)req.Credentials).Password));
}
}
System.Net.HttpWebResponse
rep = (System.Net.HttpWebResponse)base.GetWebResponse(req);
return rep;
}

.NET Google DRIVE API client library - Client is unauthorized to retrieve access tokens using this method

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

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.

Reusing Google API credentials in GData API

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;

Google apps Directory API (1.6 and above) DotNetOpenAuth not resolving

This code is changing fast and hard to get a handle on what works and what doesn't...
I was looking at this post: Have you used Google's Directory API?
Which is using the 1.4 library.
I installed the 1.6 API through nuget. However, the NativeApplicationClient and IAuthorizationState cannot be resolved. I was under the impression that I no longer needed the DotNetOpenAuth nuget package or the Google.Apis.Authentication package (which is where I believe they are resolved.
This is the complete and modified code I am playing with: (if you have a better example of creating users using the new API I'd like to see that!)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Diagnostics;
using Google;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Auth;
using Google.Apis.Download;
using Google.Apis.Logging;
using Google.Apis.Services;
using Google.Apis.Upload;
using Google.Apis.Admin.Directory;
using Google.Apis.Admin.Directory.directory_v1.Data;
namespace GoogleAddUser
{
class Program
{
static void Main(string[] args)
{
// Display the header and initialize the sample.
//CommandLine.EnableExceptionHandling();
Console.WriteLine("Create users in a google apps domain!");
Console.WriteLine("by Jonas Bergstedt 2013");
// Get the user data and store in user object
Console.Write("Email: ");
string userId = Console.ReadLine();
Console.Write("Givenname: ");
string GivenName = Console.ReadLine();
Console.Write("Familyname: ");
string FamilyName = Console.ReadLine();
Console.Write("Password: ");
string Password = Console.ReadLine();
User newuserbody = new User();
UserName newusername = new UserName();
newuserbody.PrimaryEmail = userId;
newusername.GivenName = GivenName;
newusername.FamilyName = FamilyName;
newuserbody.Name = newusername;
newuserbody.Password = Password;
// Register the authenticator.
var provider = new NativeApplicationClient(GoogleAuthenticationServer.Description)
{
ClientIdentifier = "<your clientId from Google APIs Console>",
ClientSecret = "<your clientsecret from Google APIs Console>",
};
var auth = new OAuth2Authenticator<NativeApplicationClient>(provider, GetAuthorization);
// Create the service.
var service = new DirectoryService(new BaseClientService.Initializer()
{
Authenticator = auth,
ApplicationName = "Create User",
ApiKey = "<your API Key from Google APIs console> (not sure if needed)"
});
User results = service.Users.Insert(newuserbody).Execute();
Console.WriteLine("User :" + results.PrimaryEmail + " is created");
Console.WriteLine("Press any key to continue!");
Console.ReadKey();
}
private static IAuthorizationState GetAuthorization(NativeApplicationClient arg)
{
// Get the auth URL:
IAuthorizationState state = new AuthorizationState(new[] { DirectoryService.Scopes.AdminDirectoryUser.GetStringValue() });
state.Callback = new Uri(NativeApplicationClient.OutOfBandCallbackUrl);
Uri authUri = arg.RequestUserAuthorization(state);
// Request authorization from the user (by opening a browser window):
Process.Start(authUri.ToString());
Console.WriteLine();
Console.Write("Authorization Code: ");
string authCode = Console.ReadLine();
// Retrieve the access token by using the authorization code:
return arg.ProcessUserAuthorization(authCode, state);
}
}
}
From release 1.6.0-beta we presented a new Google.Apis.Auth NuGet package (Google.Apis.Authentication which uses DNOA is obsolete!). You already installed that package, because all the new APIs have a reference to it. Take a look in our OAuth2 wiki page for more details about how to use the new OAuth2 flows (it's not magic anymore, now the flows actually make sense!)
I recommend you subscribing to our announcement blog and optionally to my personal blog to get more information about the client library. In our announcement blog we described the reason the new OAuth2 pacakge.
Hope it is helpful.

Resources