Google Drive API invalid_grant after removing access - c#

I have been writing a desktop application that uses the Google Drive API v2. I have the following code:
var credential = GoogleWebAuthorizationBroker.AuthorizeAsync
(
new ClientSecrets { ClientId = ClientID, ClientSecret = ClientSecret },
new[] { DriveService.Scope.Drive }, "user", CancellationToken.None
)
.Result;
this.Service = new DriveService( new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "My Test App",
} );
var request = this.Service.Files.List();
request.Q = "title = 'foo' and trashed = false";
var result = request.Execute();
The first time I ran this code it opened a browser and asked me to grant permissions to the App, which I did. Everything worked successfully until I realized I was using the wrong Google account. At that point I logged into the wrong Google account and revoked access to my App. Now, whenever I run the same code it throws an exception:
Error:"invalid_grant", Description:"", Uri:""
When I examine the service and request objects, it looks like the oauth_token isn't getting created any more.
I know what I did to mess things up, but I can't figure out how to correct it so I can use a different Google account for testing. What do I need to do?

The stored credentials are persisted using StorageDataStore. You can either call the DeleteAsync() method or manually find and delete the file.

Related

SharePointOnline CSOM 401 Unauthorized Using Provided Access Token

I am building a feature that automates the retrieval of documents and other SharePoint files from a Web API, but I'm having a difficult time getting authorized to perform even basic read operations. I am testing this in a .NET 6 console application using the Microsoft.SharePointOnline.CSOM NuGet package.
I have registered an application in Azure Active Directory and given it the Sites.Read.All permission. I've taken the ClientID, ClientSecret and TenantID as reported by that registered application and I'm using those in my console application. I can retrieve an access token without issue, and decoding that JWT shows that it comes with Sites.Read.All permission. But regardless of what I try, ClientContext.ExecuteQueryAsync() consistently throws an exception complaining that the remote server responded with a 401.
Here is the code that I'm testing this with:
var clientId = "myClientId";
var clientSecret = "myClientSecret";
var tenantId = "myTenantId";
var authority = "https://login.microsoftonline.com/" + tenantId;
var siteUrl = "https://myorg.sharepoint.com";
var app = new ConfidentialClientApplicationBuilder
.Create()
.WithClientSecret(clientSecret)
.WithAuthority(authority)
.WithTenantId(tenantId)
.Build();
var paramBuilder = app.AcquireTokenForClient(new[] { siteUrl + "/.default" });
var authResult = await paramBuilder.ExecuteAsync();
// authResult has successfully retrieved an access token at this point
var context = new ClientContext(siteUrl);
context.ExecutingWebRequest += (_, e) =>
{
e.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + authResult.AccessToken;
}
context.Load(context.Web);
await context.ExecuteQueryAsync(); // 401 is thrown here
var title = context.Web.Title;
I have tried several different ways of getting around this to no avail:
I have gone to the Admin center of my SharePoint site and given the app FullControl permissions, as well as giving the app those permissions in Azure AD. This doesn't seem to have changed anything, I still get the same 401.
I have registered an entirely new app directly from my SharePoint sub-site admin center and given it FullControl permissions. I used the new client ID and client secret that were generated, and I was able to get back an access token. No luck, still get the 401 calling ClientContext.ExecuteQueryAsync()
I have tried changing my siteUrl to a SharePoint site-specific URL (e.g. https://myorg.sharepoint.com/sites/mySite), but once I do that I am no longer able to retrieve an access token. I instead get an Msal exception thrown, AADSTS500011, which reads:
"The resource principal named https://myorg.sharepoint.com/sites/mysite was not found in the tenant named (my tenant). This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You might have sent your authentication request to the wrong tenant.
I have also tried using the base siteUrl to retrieve the token, then giving the site-specific URL to ClientContext. I get the same 401 result.
I have tried several different authorities in case the token I'm being provided is invalid. I've tried using the V1 token URL, the V2 token URL, no token-specific URL (only the default authority address + tenant ID). All of these return an access token, but none of them avoid the 401.
A MS documentation article suggests appending an additional "/" to the requested .default scope in instances where a 401 is being returned (e.g. https://myorg.sharepoint.com/sites/mysite//.default). This doesn't seem to have changed anything.
My application seems to have the permissions it needs to do this basic read operation, but I am continually rebuffed. I am using the ClientID, ClientSecret and Tenant ID as copied directly from the AAD application page. The code I'm using above is recommended by Microsoft to use the new SharePointOnline.CSOM package. What am I missing here?
Constructor of ClientContext requires site url including site name.
var clientId = "myClientId";
var clientSecret = "myClientSecret";
var tenantId = "myTenantId";
var authority = "https://login.microsoftonline.com/" + tenantId;
var siteUrl = "https://myorg.sharepoint.com";
var siteName = "MySiteName";
var app = new ConfidentialClientApplicationBuilder
.Create()
.WithClientSecret(clientSecret)
.WithAuthority(authority)
.WithTenantId(tenantId)
.Build();
var paramBuilder = app.AcquireTokenForClient(new[] { siteUrl + "/.default" });
var authResult = await paramBuilder.ExecuteAsync();
// authResult has successfully retrieved an access token at this point
var webFullUrl = $"{siteUrl}/sites/{siteName}";
var context = new ClientContext(webFullUrl);
If the site has some prefix
var webFullUrl = $"{siteUrl}/sites/{sitePrefix}/{siteName}";
I wound up "solving" this problem by using the PnP.Framework NuGet package instead of Microsoft.SharePointOnline.CSOM. I changed nothing else about my app registration or its designated permissions, and PnP.Framework was able to handle it without issue (and with fewer arguments). It seems to know something that SharePointOnline.CSOM doesn't considering that the following simple console app works:
using System;
using PnP.Framework
const string clientId = "myClientId";
const string clientSecret = "myClientSecret";
const string siteUrl = "https://myorg.sharepoint.com/sites/mysite";
using var clientContext = new AuthenticationManager()
.GetACSAppOnlyContext(siteUrl, clientId, clientSecret);
cc.Load(cc.Web);
await cc.ExecuteQueryAsync(); // no longer throws a 401
Console.WriteLine(cc.Web.Title); // prints my site's title
I tried to use the newer PnP.Core SDK, but I couldn't find any documentation or examples on how to get that package working with an app-only client secret authenticated context. PnP.Framework's API is the cleanest and most reliable that I've found as of yet.

Workspace Domain-wide Delegation with service account not working for one project, works for others

We have different projects on GCP we use them to access different Google APIs. Most of them for internal use only.
In this particular case, we have 2 projects, both use Service Account and both are allowed on Workspace Domain-wide Delegation on the same scopes. They are almost clones of each other.
I execute a simple request with the same code (Spreadsheet.Get()) with project 1 credentials it works. I execute the same request with project 2 credentials it doesn't work.
Since Workspace Domain-wide Delegation it's activated the spreadsheet its shared to my email and I connect to the API with my email too (works with project 1 so this is not the problem) (impersonating a user)
The only difference it's that one project has OAuth Consent Screen on external (only 100 users cause we use it internally only, anyways..) and the other one it's internal but this has nothing to do with this right?
Where the problem could come from? Do I need to recreate the project that doesn't work?
Here is the error message :
Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested
Edit to answer the comments but this code works depending on the service account we use
Generating the credentials:
internal static ServiceCredential GetApiCredentialsFromJson(string jsonCredentialsPath, string mailToMimic)
{
string jsonCertificate = File.ReadAllText(jsonCredentialsPath);
string privateKey = Regex.Match(jsonCertificate, #"(?<=""private_key"": "")(.*)(?="")").Value.Replace(#"\n", "");
string accountEmail = Regex.Match(jsonCertificate, #"(?<=""client_email"": "")(.*)(?="")").Value;
ServiceAccountCredential.Initializer credentials = new ServiceAccountCredential.Initializer(accountEmail)
{
Scopes = _scopes,
User = mailToMimic
}.FromPrivateKey(privateKey);
return new ServiceAccountCredential(credentials);
}
Using the credentials:
internal GoogleSheetService(ServiceCredential credentials)
{
SheetsService = new SheetsService(new BaseClientService.Initializer()
{
HttpClientInitializer = credentials
});
SheetsService.HttpClient.Timeout = TimeSpan.FromSeconds(100);
}
Client ID is allowed on the Drive, Ads and Spreadsheets scopes on the Workspace console.
The answer was simple, but we had to figure it out by ourselves.
The scopes you add in your app when you initialize the client need to be exactly the same scopes you added in the Google Admin wide-delegation page. Even if your app or part of your app don't need them all.
C# example:
private static readonly string[] _scopes = { DriveService.Scope.Drive, SheetsService.Scope.Spreadsheets, SlidesService.Scope.Presentations };
ServiceAccountCredential.Initializer credentials = new ServiceAccountCredential.Initializer(accountEmail)
{
Scopes = _scopes,
User = mailToMimic
}.FromPrivateKey(privateKey);
return new ServiceAccountCredential(credentials);
Here my app only needs SheetsService.Scope.Spreadsheets but I had to add DriveService.Scope.Drive and SlidesService.Scope.Presentations because the same client its used for other apps that need them.

Google Drive Api in MVC C#

I'm doing project in MVC C#, and working Google Drive Api.
I was on the site daimto.com and I took the samples and put in my MVC application.
In site https://console.developers.google.com/apis/ and create two credencias:
The license for name Cliente Padrão works locally, the license Site SESMT should run on the server, but to access the page that has the release access, the site is thinking and thinking goes nowhere, it seems caught . My license to use on the server is as follows:
I've looked examples on the internet, but it's all for local application and nothing for Web application. I'm thinking that something is missing.
Question
My page is thinking and thinking goes nowhere and does not show the authorization screen. Funny that in my local environment works
My Code
Autorização
public static DriveService AuthenticateOauth(string clientId, string clientSecret, string userName)
{
string[] scopes = new string[] { DriveService.Scope.Drive, // view and manage your files and documents
DriveService.Scope.DriveAppdata, // view and manage its own configuration data
DriveService.Scope.DriveAppsReadonly, // view your drive apps
DriveService.Scope.DriveFile, // view and manage files created by this app
DriveService.Scope.DriveMetadataReadonly, // view metadata for files
DriveService.Scope.DriveReadonly, // view files and documents on your drive
DriveService.Scope.DriveScripts }; // modify your app scripts
var folder = System.Web.HttpContext.Current.Server.MapPath("/App_Data/GoogleDriveApi");
UserCredential credential = GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets { ClientId = clientId, ClientSecret = clientSecret }
, scopes
, userName
, CancellationToken.None
, new FileDataStore(folder)).Result;
DriveService service = new DriveService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "site-sesmt"
});
return service;
}
Controller
public ActionResult Index()
{
string CLIENT_ID = "***.apps.googleusercontent.com";
string CLIENT_SECRET = "***";
DriveService service = GoogleApi.AuthenticateOauth(CLIENT_ID, CLIENT_SECRET, "portalsesmt");
return View();
}
I've encountered a similar problem a few weeks back when I was doing the exact same thing. If you are using Oauth 2.0, you have to enable the Google+ API in Google API Console->Dashboard->Enable API->Google+ API.
At least this solved the problem for me for some reason. I think google changed their APIs or something. Try that out and let me know if it worked.

Gmail API: forcing the authorization to reprompt the user

The credentials authorizing access to gmailAPI seem to be cached somehow. When I change the client ID and secret, and email address, nothing changes. Also, when I change the scope, nothing changes. I am trying to force the credentials to refresh. Is there a way to force the credentials to reprompt the user?
Is there a file I can find and delete?
I am using c# and the gmail api package from nuget. The code for credentialing is:
_emailAddress = Settings.EmailAddress;
string clientSecret = Settings.ClientSecret;
string clientId = Settings.ClientId;
ClientSecrets clientSecrets = new ClientSecrets {ClientId = clientId, ClientSecret = clientSecret};
UserCredential credential;
credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
clientSecrets,
Scopes,
"user",
CancellationToken.None).Result;
_service = new GmailService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "Draft Sender", //applicationName,
});
Also, once I credential, my scope is set: cannot change scope until I delete and renew the secret and clientID. Also, it takes two run-throughs in order to generate the credentials, the first resulting in an error, but the second resulting in a prompt to authorize.
I'm pretty sure that this is happening because the credentials are cached somewhere, somehow, even though I haven't specified any storage...but I don't know for sure. Either way, the issue seems to depend on code inside the gmail api package, but I am having trouble finding a solution for what I am dealing with.
Ultimately, I need to deploy a functionality to an ASP website that will access the gmail account as a repository for emails, so I need to have this authorization thing figured out so that the server will have access to the email account upon deployment (or easily after). It is not really necessary (but desired) that it happen without the intermediate error.
Is there a solution (easy or hard) to control this caching process and handle the permissions?
If you don't specify where you are storing the response token, it's stored on:
C:\Users\'Your user'\AppData\Roaming\Google.Apis.Auth
If you want to specify the directory look at this:
(note: you just need to create the directory App_Data in your project)
string path = HostingEnvironment.MapPath("~/App_Data");
credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
new ClientSecrets
{
ClientId = "Client_ID",
ClientSecret = "Secret_Client"
},
Scopes,
"me",
CancellationToken.None,
new FileDataStore(path)
).Result;

Transfer ownership of Google Drive documents

We develop application in C# which need to transfer ownership of all Google Drive documents related to the curtain domain to a single certain user without permission of original owner. We are using trial version of Google Apps business account.
In principal, we need to do this: http://screencast.com/t/effVWLxL0Mr4 but in C# code.
Accourding to the documentation, it is implemented in OAuth2 as superadmin functionality. https://support.google.com/a/answer/1247799?hl=en (1).
But document was deprecated and more over we did not find any API call to do that.
Using account of project creator, it is appeared, that he can not access to all files and can not see files is not shared with him.
In Google Admin Console in manage API client access we added access rights to him to access files without permission to files without permission. Link: screencast.com/t/zU9cc6Psyb. We added routes access routes there according to that document link: trovepromo-tf.trove-stg.com/0m1-sds/support.google.com/a/answer/162106?hl=en and tried again.
It did not work out...
Also, We found out that we need to use service account to have access to all data of all users of the domain, therefore we generated API keys for service account link: screencast.com/t/rNKuz6zchwV in the created project and got authenticated in the application using the following code:
var certificate = new X509Certificate2(#"C:\Temp\key.p12", "notasecret", X509KeyStorageFlags.Exportable);
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer("Service account email")
{
User= "admin#domain.com",
Scopes = new[] { DriveService.Scope.Drive }
}.FromCertificate(certificate));
but when we try to get list of folders, we get error :"access_denied", Description:"Requested client not authorized.", Uri:""
Please help us to transfer ownership of one user to another by service account!
Update from 13-08-2014:
Dear, It seems I have problem with user impersonalization.
1) When I use api to connect on behalf of user. During the authentication it redirects to browser and ask permisstion. After that all is completely fine, I can manimulate with folders except one one thing: I can not transfer ownership to him
2) When I use service account without impersonalization, authentication looks like the following:
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(ServiceAccountId)
{
Scopes = new[] { DriveService.Scope.Drive,
DriveService.Scope.DriveAppdata,
DriveService.Scope.DriveFile,
DriveService.Scope.DriveScripts,
DirectoryService.Scope.AdminDirectoryUser,
DirectoryService.Scope.AdminDirectoryGroup,
DirectoryService.Scope.AdminDirectoryOrgunit,
DirectoryService.Scope.AdminDirectoryUserReadonly
},
}.FromCertificate(certificate));
Then I can access to all files shared to service account but (again) I can not transfer the rights.
3) Then I try impersonalize Service account by adding sypeadministrator email account to the user User = myaddress#mydomain.com
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(ServiceAccountId)
{
Scopes = new[] { DriveService.Scope.Drive,
DriveService.Scope.DriveAppdata,
DriveService.Scope.DriveFile,
DriveService.Scope.DriveScripts,
DirectoryService.Scope.AdminDirectoryUser,
DirectoryService.Scope.AdminDirectoryGroup,
DirectoryService.Scope.AdminDirectoryOrgunit,
DirectoryService.Scope.AdminDirectoryUserReadonly
},
User = AdminEmail,
}.FromCertificate(certificate));
Then I have Error:"access_denied", Description:"Requested client not authorized.", Uri:""
How can I impersonalize service account correctly?
Updated 13-08-2014
We found out that basic api for authentication is here: https://developers.google.com/accounts/docs/OAuth2ServiceAccount#creatingjwt
Normally, all what I showed before is an .net implementation of the protocol.
How can we please do impersonalization of of user in .net code. We did not find any working .net implementation of it.
I finally found an answer.
I could impersonalize an account by using the following construction:
Thanks to all, who tried to help me!
var initializer = new ServiceAccountCredential.Initializer(ServiceAccountId)
{
Scopes = scope,
User = AdminEmail1
};
var credential = new ServiceAccountCredential(initializer.FromCertificate(certificate));
var driveService = new DriveService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = ApplicationName
});

Categories