Have a really strange issue going on. Trying to implement Google's OAuth 2.0 in a ASP.NET (non-MVC) scenario to Google Calendar API. I see the token response in the storage area, no errors encountered.
Here's the code:
public CalendarService Credential(string sUserID)
{
CalendarService service = new CalendarService();
var folder = #"C:\TEMP\GoogleStorage";
UserCredential credential = null;
string clientId = "{client id is redacted}";
string clientSecret = "{client secret is redacted}";
string[] scopes = new string[] {CalendarService.Scope.Calendar};
credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
new ClientSecrets
{
ClientId = clientId,
ClientSecret = clientSecret
},
scopes,
sUserID,
CancellationToken.None,
new FileDataStore(folder)
).Result;
}
service = new CalendarService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "Interpose Concept"
});
return service;
}
Here's the series of events:
I hit the above code and get a successful credential and a successful service returned. Google Account A using Chrome gets prompted to allow application to access the calendar. A token in stored at c:\temp\GoogleStorage
I am able to use the returned service to query User A's calendar.
Google Account B (simulating a different user hitting the same web server) using Firefox goes through the same code. A different UserID is passed into Credential based on this new session. Again, there's a successful credential to Google, this time userID is different, but this time no requiring to allow access to their Calendar. I am able to successfully return the service, but the service is for Google Account A's calendar.
I am certain I am missing something along the lines of being able to segregate the different Google Accounts, but I don't see how to separate the different users hitting this web server other than the user parameter in the AuthorAsync call I make. I further don't understand why it would retain the first account's credential.
What's blowing my mind is this seems to be working (first request is good, no errors, can query the calendar), but all subsequent requests seem to be tied to the first credential request that works.
What am I not understanding?
Related
Having spent hours looking for an answer on how to access the Gmail API with the use of a service account and saw that I can't, unless I'm using a GSuite account that it's provided with domain-wide authorization, I came here to ask you if there's a way to actually create labels using the said API and a private account. I'm using Visual Studio 2019 and C#. In the "developers.google.com" there's a tool called "Try this API" and I can create a label using my OAuth 2.0 just fine, and the .NET Quickstart found here also works in listing my labels. But why can't it let me create labels? I have enabled all of the scopes possible for this to work.
This is the error I am getting:
"Google.GoogleApiException: 'Google.Apis.Requests.RequestError
Request had insufficient authentication scopes. [403]
Errors [
Message[Insufficient Permission] Location[ - ] Reason[insufficientPermissions] Domain[global]" enter image description here
The method Lables.create requires permissions in order to create labels on the users account. The user must have consented to that permission.
the error message
Google.Apis.Requests.RequestError Request had insufficient authentication scopes.
Is telling you that the user has not consented to the proper scope. The user must have consented to one of the following scopes
If you followed the quick start then you probably only included GmailService.Scope.GmailReadonly. You will need to change the scope and request authorization of the user again. Note that the tutorial you are following is not for service account authencation but rather for Oauth2 authentication.
service account
string ApplicationName = "Gmail API .NET Quickstart";
const string serviceAccount = "xxxxx-smtp#xxxxx-api.iam.gserviceaccount.com";
var certificate = new X509Certificate2(#"c:\xxxxx-api-ed4859a67674.p12", "notasecret", X509KeyStorageFlags.Exportable);
var gsuiteUser = "xxxxx#xxxx.com";
var serviceAccountCredentialInitializer = new ServiceAccountCredential.Initializer(serviceAccount)
{
User = gsuiteUser,
Scopes = new[] { GmailService.Scope.GmailSend, GmailService.Scope.GmailLabels }
}.FromCertificate(certificate);
var credential = new ServiceAccountCredential(serviceAccountCredentialInitializer);
if (!credential.RequestAccessTokenAsync(CancellationToken.None).Result)
throw new InvalidOperationException("Access token failed.");
var service = new GmailService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = ApplicationName,
});
We passed the verification to start using calendar.events.readonly scope on breess.com service.
We created the service account with wide delegation
In G Suite domain’s admin console added scope https://www.googleapis.com/auth/calendar.events.readonly
The user account xxxxx#xxx.com granted access to his calendar
Everything looks configured. The code below is executed on the host and returns the 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.", Uri:""
Have we missed something? Could you please guide us to resolve the issue?
Extra note:
var credential = GoogleCredential
.FromFile("breess-7798c393d5b0.json")
.CreateScoped(new[] { CalendarService.Scope.CalendarEventsReadonly })
.createwithuser("xxx#xxx.xxx");
var calendarService = new CalendarService(new BaseClientService.Initializer { HttpClientInitializer = credential, ApplicationName = "BREESS" });
var request = calendarService.Events.List(calendarId);
var events = await request.ExecuteAsync();
Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested.
There are three ways of authorizing Oauth with google. They each have their own type of credential file and code needed to use them.
Installed application
Web application
Service account application
API key for public access only
When you create the project on Google developer console you need to be sure that you have created service account credentials. Then you need to be sure that you are using the code that was intended for use with service account credentials
var scopes = new[] { CalendarService.Scope.CalendarEventsReadonly }
GoogleCredential credential;
using (var stream = new FileStream(serviceAccountCredentialFilePath, FileMode.Open, FileAccess.Read))
{
credential = GoogleCredential.FromStream(stream)
.CreateScoped(scopes);
}
credential.User = "domainuser#yourdomain.com";
// Create the Calendar service.
return new CalendarService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "Calendar Service account Authentication Sample",
}
);
I dont see anything major wrong with your code so i suspect the issue is that you do not have the correct type of credential file.
I am trying to download a user's mailbox using the Email Audit API. I am getting a 403 Forbidden response to this code (the error occurs on the last line, the call to the UploadPublicKey method):
var certificate = new X509Certificate2(System.Web.HttpRuntime.AppDomainAppPath + "key.p12", "notasecret", X509KeyStorageFlags.Exportable);
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
Scopes = new[] { "https://apps-apis.google.com/a/feeds/compliance/audit/" }
}.FromCertificate(certificate));
credential.RequestAccessTokenAsync(System.Threading.CancellationToken.None).Wait();
DebugLabel.Text = credential.Token.AccessToken;
var requestFactory = new GDataRequestFactory("My App User Agent");
requestFactory.CustomHeaders.Add(string.Format("Authorization: Bearer {0}", credential.Token.AccessToken));
AuditService aserv = new AuditService(strOurDomain, "GoogleMailAudit");
aserv.RequestFactory = requestFactory;
aserv.UploadPublicKey(strPublicKey);
I have created the service account in the Developers Console and granted the Client ID access to https://apps-apis.google.com/a/feeds/compliance/audit/ in the Admin console.
Seems to me like the account should have all the permissions it needs, yet it doesn't. Any idea what I am missing?
OK, so I gave up on trying to make it work with a service account even though that is what Google's documentation would lead you to believe is the correct way to do it. After emailing Google support, I learned I could just use OAuth2 for the super user account that created the application on the developer's console.
So then I worked on getting an access token for offline access (a refresh token) by following the process outlined here:
Youtube API single-user scenario with OAuth (uploading videos)
and then taking that refresh token and using it with this code:
public static GOAuth2RequestFactory RefreshAuthenticate(){
OAuth2Parameters parameters = new OAuth2Parameters(){
RefreshToken = "<YourRefreshToken>",
AccessToken = "<AnyOfYourPreviousAccessTokens>",
ClientId = "<YourClientID>",
ClientSecret = "<YourClientSecret>",
Scope = "https://apps-apis.google.com/a/feeds/compliance/audit/",
AccessType = "offline",
TokenType = "refresh"
};
OAuthUtil.RefreshAccessToken(parameters);
return new GOAuth2RequestFactory(null, "<YourApplicationName>", parameters);
}
which is code from here https://stackoverflow.com/a/23528629/5215904 (Except I changed the second to last line... for whatever reason the code shared did not work until I made that change).
So there I was finally able to get myself an access token that would allow me access to the Email Audit API. From there everything was a breeze once I stopped trying to mess around with a service account.
I am trying to use the Google Calendar API in my non-MVC .NET Web Application. (This appears to be an important distinction.)
I’ve tried to use code from this example at Google and this example at Daimto along with some helpful hints from a number of related posts here.
I have written the following method:
public void GetUserCredential( String userName )
{
String clientId = ConfigurationManager.AppSettings[ "Google.ClientId" ]; //From Google Developer console https://console.developers.google.com
String clientSecret = ConfigurationManager.AppSettings[ "Google.ClientSecret" ]; //From Google Developer console https://console.developers.google.com
String[] scopes = new string[] {
Google.Apis.Calendar.v3.CalendarService.Scope.Calendar
};
// 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( "c:\\temp" ) ).Result;
// TODO: Replace FileDataStore with DatabaseDataStore
}
Problem is, when Google’s OAuth2 page is called, redirect_uri keeps getting set to http://localhost:<some-random-port>/authorize. I have no idea how to set this to something else, as in the following example URL generated by AuthorizeAsync:
https://accounts.google.com/o/oauth2/auth?access_type=offline
&response_type=code
&client_id=********.apps.googleusercontent.com
&redirect_uri=http:%2F%2Flocalhost:40839%2Fauthorize%2F
&scope=https:%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar
Google responds with a redirect_uri_mismatch error page with the message:
“The redirect URI in the request: http://localhost:XXXXX/authorize/ did not match a registered redirect URI”
I can only register so many Redirect URI’s in my Google Developer’s Console Credentials page. I’m not inclined to register 65535 ports, and I want to use a page other than /authorize on my site. Specifically, I want to use, during development, http://localhost:888/Pages/GoogleApiRedirect but have no clue as to where I would set this, beyond what I've done in the Developer’s Console.
How do I explicitly set the value of redirect_uri? I am also open to a response in the form “This approach is completely wrong.”
EDIT:
After playing with this over the past day, I've discovered that by using the Client ID/Client Secret for the Native Application rather than the Web Application, I can at least get to Google's web authorization page without it complaining about a redirect_uri_mismatch. This is still unacceptable, because it still returns to http://localhost:<some-random-port>/authorize, which is outside the control of my web application.
You can use this code: (original idea from http://coderissues.com/questions/27512300/how-to-append-login-hint-usergmail-com-to-googlewebauthorizationbroker)
dsAuthorizationBroker.RedirectUri = "my localhost redirect uri";
UserCredential credential = await dsAuthorizationBroker.AuthorizeAsync(...
dsAuthorizationBroker.cs
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Auth.OAuth2.Flows;
using Google.Apis.Auth.OAuth2.Requests;
using Google.Apis.Util.Store;
namespace OAuth2
{
public class dsAuthorizationBroker : GoogleWebAuthorizationBroker
{
public static string RedirectUri;
public new static async Task<UserCredential> AuthorizeAsync(
ClientSecrets clientSecrets,
IEnumerable<string> scopes,
string user,
CancellationToken taskCancellationToken,
IDataStore dataStore = null)
{
var initializer = new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = clientSecrets,
};
return await AuthorizeAsyncCore(initializer, scopes, user,
taskCancellationToken, dataStore).ConfigureAwait(false);
}
private static async Task<UserCredential> AuthorizeAsyncCore(
GoogleAuthorizationCodeFlow.Initializer initializer,
IEnumerable<string> scopes,
string user,
CancellationToken taskCancellationToken,
IDataStore dataStore)
{
initializer.Scopes = scopes;
initializer.DataStore = dataStore ?? new FileDataStore(Folder);
var flow = new dsAuthorizationCodeFlow(initializer);
return await new AuthorizationCodeInstalledApp(flow,
new LocalServerCodeReceiver())
.AuthorizeAsync(user, taskCancellationToken).ConfigureAwait(false);
}
}
public class dsAuthorizationCodeFlow : GoogleAuthorizationCodeFlow
{
public dsAuthorizationCodeFlow(Initializer initializer)
: base(initializer) { }
public override AuthorizationCodeRequestUrl
CreateAuthorizationCodeRequest(string redirectUri)
{
return base.CreateAuthorizationCodeRequest(dsAuthorizationBroker.RedirectUri);
}
}
}
If you are trying to use GoogleWebAuthorizationBroker.AuthorizeAsync in a .NET application NON-web server application i.e. C# Console App command line program, it's crucial when creating the Google OAuth profile (https://console.developers.google.com/apis) in the credentials to select the following.
DESKTOP APP - This also known now as an installed application
It's hidden under "Help me choose" new credential. You must create a new credential which shows Desktop app and not WebApp.
CLICK "HELP ME CHOOSE"
CHOOSE YOUR INTENDED API LIBRARY
ie (Google Calendar API, YouTube) Select "User Data"
STEP 3 - OAUTH CLIENT ID - Desktop App!
Yeah - NO AUTHORIZATION REQUIRED FILEDS"
ie Javascript & Redirect Now you have a profile without the Web Application redirect authorization
Use the "Download JSON" and save it to your application
Reference in the code below. When you look inside this file, you will notice a different set of parameters as well to tell the broker this is an application. In this example, I am accessing the scope Calendar API. Just change the scope to whatever API you are trying to access.**
string[] Scopes = { CalendarService.Scope.Calendar }; //requires full scope to get ACL list..
string ApplicationName = "Name Of Your Application In Authorization Screen";
//just reference the namespaces in your using block
using (var stream = new FileStream("other_client_id.json", FileMode.Open, FileAccess.Read))
{
// The file token.json stores the user's access and refresh tokens, and is created
// automatically when the authorization flow completes for the first time.
string credPath = "other_token.json";
credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
GoogleClientSecrets.Load(stream).Secrets,
Scopes,
"user",
CancellationToken.None,
new FileDataStore(credPath, true)).Result;
}
// Create Google Calendar API service.
var service = new CalendarService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = ApplicationName,
});
//Then your ready to grab data from here using the methods mentioned in Google Calendar API docs
To run this in Docker
When you deploy this to docker you will get an exception that the operating system does not support running the process. Basically, its trying to open a browser on the Metal Box. which is not possible with docker.
To solve this. Modify the code to use full absolute path like this
var inputFolderAbsolute = Path.Combine(AppContext.BaseDirectory, "Auth.Store");
...
new FileDataStore(inputFolderAbsolute, true)
Run this application as a console app on your local machine so the browser opens.
Select the account you want to work with
In the bin folder, a new folder and file will be created.
Copy that folder to the root path
Set the file to copy if newer
Deploy to docker
Because the refresh token is saved for the account you selected it will get a new access token and work.
NB: It is possible the refresh token expires to whatever reason. You will have to repeat the steps above
selecting "other" while creating oAuth Client ID helped me resolve the redirection issue for me. (Having the "Web Application" option tries to redirect to some url with random port, which is very annoying)
Now my Gmail API works like a charm :)
If you are struggling to build a console app that would be able to authenticate to Gmail to send mail, here is what worked. LocalServerCodeReceiver was the key to listening to access token returned from the browser. This code also would obtain a refresh token if needed, and will cache the token on the file system for future use.
var clientSecrets = new ClientSecrets
{
ClientId = YOUR_CLIENTID,
ClientSecret = YOUR_CLIENT_SECRET
};
var codeFlow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
DataStore = new FileDataStore(AppDomain.CurrentDomain.BaseDirectory),
Scopes = new[] { GmailService.Scope.MailGoogleCom },
ClientSecrets = clientSecrets
});
var codeReceiver = new LocalServerCodeReceiver();
var authCode = new AuthorizationCodeInstalledApp(codeFlow, codeReceiver);
var credential = await authCode.AuthorizeAsync(EmailUser, CancellationToken.None);
if (authCode.ShouldRequestAuthorizationCode(credential.Token))
{
await credential.RefreshTokenAsync(CancellationToken.None);
}
Note:
in my experience, Firefox didn't handle the browser redirect correctly, so I had to switch my default Windows browser to Chrome in order for this to work
even though this code is part of a console app, it is not entirely set up to run unattended; unless the token is cached on the filesystem from prior runs, you would need to manually go through the authentication flow in the browser popup
the OAuth client ID configuration in the Developers console should be set to "Desktop"
I want to authenticate user with Google login, I am getting the authorization code from frontend, and I want to trade it for access token with Google.
This is my code:
[Route("google")]
public object Google(AuthModel model) {
IAuthorizationCodeFlow flow =
new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer {
ClientSecrets = new ClientSecrets {
ClientId = model.ClientId,
ClientSecret = Constants.Constants.GOOGLE_SECRET
},
Scopes = new[] { "https://www.googleapis.com/auth/plus.login" }
});
var token = flow.ExchangeCodeForTokenAsync("", model.Code, "postmessage",
CancellationToken.None).Result;
return token.AccessToken;
}
When calling method flow.ExchangeCodeForTokenAsync I am getting error
{"Error:\"redirect_uri_mismatch\", Description:\"\", Uri:\"\""}
I do not understand, where I can define a redirect URL? I have spend multiple hours of googleing the issue and answers all say that I should define some URLs in my google application account, where I have at the moment:
Redirect URIs
http://localhost:53906
http://localhost:53906/authcallback
For me, the only thing that worked was putting same url parameter for TokenResponse as it was for Credentials. Nothing else worked - I've tried 'postmessage' as url, then urls to some other actions on my site, even without url. Nothing worked.
When I've typed same url as it was for credentials, it worked like a charm.
Code:
ClientSecrets secrets = new ClientSecrets
{
ClientId = "***",
ClientSecret = "***"
};
IEnumerable<string> scopes = new[] { PlusService.Scope.UserinfoEmail, PlusService.Scope.UserinfoProfile };
IAuthorizationCodeFlow flow =
new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = secrets,
Scopes = scopes
});
TokenResponse token = flow.ExchangeCodeForTokenAsync("", code,
"https://localhost:44388/TestLogin/Redirect", //This is the same url that I have for credentials
CancellationToken.None).Result;
The client library detects on its own where you are sending your request from. So you don't need to apply it. You tell Google which redirect URIs you will be using from the Google Developer console. It appears you have done that.
Your issue is probably that Visual studio has a habit of randomly changing the port number. You can either fix this in your project settings.
OR
Go to Google Developer Console. Create a Client ID for native application use this client id while you are testing on localhost. Once you release to production website change back to a Client ID for web application
The reason this works is Client ID for native application allows connection from all locations so it wont matter if Visual studio changes your port.