I have a customer that is trying to access their calendars from our web application. Everything works for all of our other customers, so I am not sure what is different here except this customer is in Australia and using a non gmail.com email address.
The customer is able to authorize our application and we do get a oauth token for the user. We request calendar access and the customer granted it. When we request a list of all of the calendars, we get the invalid grant message.
Below is the code that we use to access their calendars. The method being called is GetAllWritableCalendars.
public class GoogleCalendarAdapter : ICalendarAdapter {
#region attributes
private readonly ISiteAuthTokenQueryRepository _tokenRepo;
private readonly GoogleCalendarSettings _settings;
private const string APPNAME = "SomeAppName";
private const string ACL_OWNER = "owner";
private const string ACL_WRITER = "writer";
#endregion
#region ctor
public GoogleCalendarAdapter(ISiteAuthTokenQueryRepository tokenRepo,
GoogleCalendarSettings settings) {
_tokenRepo = tokenRepo;
_settings = settings;
}
#endregion
#region methods
private GoogleAuthorizationCodeFlow BuildAuthorizationCodeFlow() {
return new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer() {
ClientSecrets = BuildClientSecrets(),
Scopes = BuildScopeList()
});
}
private CalendarService BuildCalendarService(SiteAuthToken token) {
return new CalendarService(new BaseClientService.Initializer() {
ApplicationName = APPNAME,
HttpClientInitializer = BuildUserCredential(token)
});
}
private ClientSecrets BuildClientSecrets() {
return new ClientSecrets() {
ClientId = _settings.ClientId,
ClientSecret = _settings.ClientSecret
};
}
private string[] BuildScopeList() {
return new [] { CalendarService.Scope.Calendar };
}
private UserCredential BuildUserCredential(SiteAuthToken token) {
TokenResponse responseToken = new TokenResponse() {
AccessToken = token.AccessToken,
RefreshToken = token.RefreshToken
};
return new UserCredential(BuildAuthorizationCodeFlow(), APPNAME, responseToken);
}
public async Task<List<Cal>> GetAllWritableCalendars(Guid siteGuid) {
SiteAuthToken token = await GetToken(siteGuid);
CalendarService svc = BuildCalendarService(token);
IList<CalendarListEntry> calendars = svc.CalendarList
.List()
.Execute()
.Items;
return calendars.Where(c => c.AccessRole.Equals(ACL_OWNER, StringComparison.CurrentCultureIgnoreCase) ||
c.AccessRole.Equals(ACL_WRITER, StringComparison.CurrentCultureIgnoreCase))
.Select(c => new Cal() {
Id = c.Id,
Name = c.Summary
})
.OrderBy(o => o.Name)
.ToList();
}
private async Task<SiteAuthToken> GetToken(Guid siteGuid) {
SiteAuthToken retVal = await _tokenRepo.GetSiteAuthToken(siteGuid);
if (retVal == null) {
throw new ApplicationException($"Could not find a SiteAuthToken for specified site (SiteGuid: {siteGuid})");
}
return retVal;
}
#endregion
The credentials are the authorization from Google to Your Application to use the scopes you have set-up, this is okay to have it in a database if you update it every time you add new scopes to your app.
The Access Token is the authorization from the user to your application to get it's Google Data (calendar in this case). It has a limited lifetime so this is not okay to save in a database.
The Refresh Token is the token that allows your application to get more tokens for a client. It has a limited lifetime as well.
For more info see: Using OAuth 2.0 to Access Google APIs
Every time you change your scopes or add more scopes you have to re-generate the credentials. You have 50 refresh tokens per user account per client, see Token expiration. So having the tokens in a database makes no sense since they are going to get deprecated at some point, if you have 51 clients the 1st token will get deprecated.
Check:
How do you have it set-up on your database
If you renew properly the tokens
If you are using the correct tokens for the users
You can delete all the tokens (NOT the CREDENTIALS) and your current users will only have to go through the consent screen and allow it again, they will not lose the connection.
I asked the question later in a different way. Maybe it was a little more relevant. Perhaps there was a little more information available. What ever the case may be, I discovered how to test things properly.
Look at this question
Related
I'm able to get access token from client side using google file picker. But the access token expires after 3600 seconds. I scratched my head to get the refresh token but unable to do it on C# ASP.NET. Can somebody help with the C# code to understand and retrieve the refresh token ? It will be very helpful. Thank You.
I tried using GoogleWebAuthorizationBroker.AuthorizeAsync, it works in my local machine but doesn't work on the production IIS server. I also tried GoogleAuthorizationCodeFlow but unable to go further in it and I don't know how to use it.
IAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = System.Configuration.ConfigurationManager.AppSettings["GoogleDriveClientID"],
ClientSecret = System.Configuration.ConfigurationManager.AppSettings["GoogleDriveClientSecret"]
},
Scopes = Scopes,
DataStore = null
});
I tried this but I don't know how to move forward after this.
It seems you are trying to generate refresh token from your C# code sample.
You could try following code snippet:
Refresh Token Example:
public class AppFlowMetadata : FlowMetadata
{
private static readonly IAuthorizationCodeFlow flow =
new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = "yourClientId",
ClientSecret = "yourClientSecret"
},
Scopes = new[] { DriveService.Scope.Drive },
DataStore = new FileDataStore("Drive.Api.Auth.Store")
});
public override string GetUserId(Controller controller)
{
// In this sample we use the session to store the user identifiers.
// That's not the best practice, because you should have a logic to identify
// a user. You might want to use "OpenID Connect".
// You can read more about the protocol in the following link:
// https://developers.google.com/accounts/docs/OAuth2Login.
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; }
}
}
Note: If you have any more query on how you get ClientId, ClientSecret and Scope, please refer to this MVC Example.
My setup,
An IdentityServer using MVC Identity to store the Users, created with dotnet new mvc -au Individual and applying the http://docs.identityserver.io/en/release/quickstarts/0_overview.html tutorial, running in localhost 5000.
A client App, but now I'm using postman to do tests.
A WEB API, created with dotnet new webapi, running in localhost 5001.
The IdentityServer resources and clients configuration is the following, notice that I'm using reference tokens:
public static IEnumerable<IdentityResource> GetIdentityResources() {
return new List<IdentityResource>{ new IdentityResources.OpenId() };
}
public static IEnumerable<ApiResource> GetApiResources() {
return new List<ApiResource>{
new ApiResource("api_resource", "API Resource") {
Description= "API Resource Access",
ApiSecrets= new List<Secret> { new Secret("apiSecret".Sha256()) },
}
};
}
public static IEnumerable<Client> GetClients() {
return new List<Client>{
new Client {
ClientId= "angular-client",
ClientSecrets= { new Secret("secret".Sha256()) },
AllowedGrantTypes= GrantTypes.ResourceOwnerPassword,
AllowOfflineAccess= true,
AccessTokenType = AccessTokenType.Reference,
AlwaysIncludeUserClaimsInIdToken= true,
AllowedScopes= { "api_resource" }
}
}
The password and user is send with postman and the token received is send to the WEB API also with postman, something like call localhost:5001/v1/test with the token pasted in option bearer token.
In the API Startup, in ConfigureServices I'm adding the lines below
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority= "http://localhost:5000";
options.ApiName= "api_resource";
options.ApiSecret = "apiSecret";
});
And I'm getting the Id of the user inside the controller as follows:
public async Task<IActionResult> Get(int id) {
var discoveryClient = new DiscoveryClient("http://localhost:5000");
var doc = await discoveryClient.GetAsync();
var introspectionClient = new IntrospectionClient(
doc.IntrospectionEndpoint,
"api_resource",
"apiSecret");
var token= await HttpContext.GetTokenAsync("access_token");
var response = await introspectionClient.SendAsync(
new IntrospectionRequest { Token = token });
var userId = response.Claims.Single(c => c.Type == "sub").Value;
}
The question itself is, am I using the right path to get the Id from the reference token?, because now It works but I don't want to miss anything, specially thinking that is a security concern.
I'm asking also because I have seen anothers using
string userId = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value;
that is more straightforward but doesn't seems to fit with reference tokens.
Thanks in advance.
Inside a controller action that is protected with an [Authorize] attribute you can simply get claims directly from the ClaimsPrinciple, without having to go through a manual discovery client. The claims principle is handily aliased simply with User inside your controllers.
I'm asking also because I have seen anothers using
string userId = User.Claims.FirstOrDefault(c => c.Type ==
ClaimTypes.NameIdentifier).Value;
that is more straightforward but doesn't seems to fit with reference
tokens.
It works just fine with reference tokens. You should have no problems accessing the sub claim.
EDIT:
As I mentioned in a comment below, I tend to use the standard JwtClaimTypes and create some extension methods on the ClaimsPrinciple, such as:
public static string GetSub(this ClaimsPrincipal principal)
{
return principal?.FindFirst(x => x.Type.Equals(JwtClaimTypes.Subject))?.Value;
}
or
public static string GetEmail(this ClaimsPrincipal principal)
{
return principal?.FindFirst(x => x.Type.Equals(JwtClaimTypes.Email))?.Value;
}
... so that within my protected actions I can simply use User.GetEmail() to get hold of claim values.
It's worth stating the obvious, that any method for retrieving claim values will only work if the claims actually exist. i.e. asking for the ZoneInfo claim will not work unless that claim was requested as part of the token request in the first place.
I am using Google APIs for OAuth2 version 1.9 and trying to send AccessType as offline and ApprovalPrompt as force everytime so that I get a refresh token. I know there are many questions on this topic here in various api versions and languages. However, none of the solutions works with the new google library.
I am using the following to get the flow:
private IAuthorizationCodeFlow GetAuthorizationCodeFlow()
{
var flow = new GoogleAuthorizationCodeFlow(
new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId =
"***",
ClientSecret = "***"
},
Scopes = new string[]
{
CalendarService.Scope.Calendar,
PlusService.Scope.UserinfoProfile,
PlusService.Scope.UserinfoEmail,
PlusService.Scope.PlusLogin,
"https://www.googleapis.com/auth/contacts.readonly"
}
});
return flow;
}
and then using the following code to get the token:
var token = flow.ExchangeCodeForTokenAsync("me", code,
uri.Substring(0, uri.IndexOf("?")), CancellationToken.None).Result;
This is where I need the refresh token every time (not just the first time) so I want to set the AccessType and ApprovalPrompt.
I also had the exact same question. Credit goes to this post.
I will paste my code as I had to make a small change to get it working.
(to use GoogleAuthorizationCodeFlow.Initializer in the constructor rather than AuthorizationCodeFlow.Initializer)
Implement your own GoogleAuthorizationCodeFlow class and set the "offline" access in the AccessType property. This will give you the refreshtoken.
public class OfflineAccessGoogleAuthorizationCodeFlow : GoogleAuthorizationCodeFlow
{
public OfflineAccessGoogleAuthorizationCodeFlow(GoogleAuthorizationCodeFlow.Initializer initializer) : base(initializer) { }
public override AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(string redirectUri)
{
return new GoogleAuthorizationCodeRequestUrl(new Uri(AuthorizationServerUrl))
{
ClientId = ClientSecrets.ClientId,
Scope = string.Join(" ", Scopes),
RedirectUri = redirectUri,
AccessType = "offline",
ApprovalPrompt = "force"
};
}
};
It's unfortunate that we can't set the "access_type" from GoogleAuthorizationCodeFlow Initializer.
Also, I use following code to retrieve the refresh token,
Google.Apis.Auth.OAuth2.Responses.TokenResponse token = new Google.Apis.Auth.OAuth2.Responses.TokenResponse();
//// Checks if the token is out of date and refresh the access token using the refresh token.
if (result.Credential.Token.IsExpired(SystemClock.Default))
{
//If the token is expired recreate the token
token = await result.Credential.Flow.RefreshTokenAsync(userid.ToString(), result.Credential.Token.RefreshToken, CancellationToken.None);
//Get the authorization details Results
result = await new AuthorizationCodeMvcApp(this, new AppFlowMetadata()).AuthorizeAsync(cancellationToken);
}
I need to be able to use a refresh token to be able to re-authenticate a token after the access token has expired. How can I do this using the C# v3 API? I've looked at the UserCredential class and AuthorizationCodeFlow class and nothing is jumping out at me.
I'm using the following code to authenticate it originally.
var result = await new AuthorizationCodeMvcApp(this, new AppFlowMetadata()).
AuthorizeAsync(CancellationToken.None);
if (result.Credential != null)
{
var service = new YouTubeService(new BaseClientService.Initializer
{
HttpClientInitializer = result.Credential,
ApplicationName = "YouTube Upload Tool"
});
}
And this is my AppFlowMetadata class.
public class AppFlowMetadata : FlowMetadata
{
private static readonly IAuthorizationCodeFlow flow =
new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = "ID",
ClientSecret = "SECRET",
},
Scopes = new[] { YouTubeService.Scope.YoutubeUpload },
DataStore = new EFDataStore(-1) // A data store I implemented using Entity Framework 6.
});
public override string GetUserId(Controller controller)
{
return "test";
}
public override IAuthorizationCodeFlow Flow
{
get { return flow; }
}
}
If anyone can suggest anything, I would greatly appreciate it. Thank you.
While this is not an answer, this is how I got around it. I had to create the GET request for authorisation (redirect your user to the url you get back and set your Controller Action to receive the callback specified in your Google Developer Console) and the PUT request for the Token (which I then stored using EF6) manually. I used System.Net.Http.HttpClient to make these requests, which was quite straight forward. See this link for all the details I needed to get this working.
It was the only way I could set the access_type to "offline". If the .NET API does this, I'm still curious to find out how.
With the token data stored, I now use the API to validate and refresh the token when I need to. I actually did this in a server side console application rather than a MVC app (hence the EF token persistence).
UserCredential credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
new ClientSecrets
{
ClientId = "ID",
ClientSecret = "Secret"
},
new[] { YouTubeService.Scope.YoutubeUpload },
"12345",
CancellationToken.None,
new EFDataStore(-1) // My own implementation of IDataStore
);
// This bit checks if the token is out of date,
// and refreshes the access token using the refresh token.
if(credential.Token.IsExpired(SystemClock.Default))
{
if (!await credential.RefreshTokenAsync(CancellationToken.None))
{
Console.WriteLine("No valid refresh token.");
}
}
var service = new YouTubeService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "MY App"
});
I hope this helps others.
Perhaps I am the only one that thinks Google's API documentation is awful but I've spent more time on this simple task than I wanted.
Currently my project is using a GDATA implementation to connect with the Google Calendar API v2. I followed this guide: http://www.codeproject.com/Articles/565032/Google-Calendar-Integration-in-ASP-NET-Create-ed
But I noticed that Google is deprecating version 2 of their API this fall. I am trying to figure out how I can connect to their version 3 API which appears to be using OAuth2.
After reading their documentation and searching the internet >:( - The problem I keep running into is EVERY sample, tutorial or youtube video I've come across that shows how to implement this involve the Google consent screen where the user clicks "Accept".
I've tried doing the following but honestly not sure if it's even the right direction?
// Register the authenticator. The Client ID and secret have to be copied from the API Access
// tab on the Google APIs Console.
var provider = new NativeApplicationClient(GoogleAuthenticationServer.Description);
provider.ClientIdentifier = "MY_CLIENT_ID";
provider.ClientSecret = "MY_CLIENT_SECRET";
// Create the service. This will automatically call the previously registered authenticator.
var service = new CalendarService();
My application doesn't need the user's account/consent (OAuth), I need to connect like I am currently in my code-behind.
So the question is how do I "upgrade" my current implementation to v3? Do I use OAuth, Service Account? I've found plenty of examples showing the v3 usages for how to retrieve events and insert them... but they all authenticate with a user consent screen on the front end.
Here is my current GData implementation...
public class GoogleGateway : IGoogleGateway
{
private readonly IRepository<UserSetting> _settingsRepository;
private Service _googleService;
private CalendarService _googleCalendar;
private Uri _calendarUri;
public GoogleGateway(IRepository<UserSetting> settingsRepository)
{
_settingsRepository = settingsRepository;
}
public IEnumerable<EventEntry> GetAllEvents(DateTime? startDate)
{
if (!Connect()) return new List<EventEntry>();
// Create the query object:
EventQuery query = new EventQuery();
query.Uri = _calendarUri;
if (startDate != null)
query.StartTime = startDate.Value;
// Tell the service to query:
EventFeed calFeed = _googleCalendar.Query(query);
return calFeed.Entries.Cast<EventEntry>();
}
public bool Connect()
{
var calSettings = _settingsRepository.Get().Where(x => x.Setting == "Calendar");
if (calSettings.Any())
{
var username = calSettings.First(x => x.Meta == "GoogleUsername").Value;
var password = calSettings.First(x => x.Meta == "GooglePassword").Value;
var calendarUri = new Uri(calSettings.First(x => x.Meta == "CalendarFeed").Value);
var applicationName = calSettings.First(x => x.Meta == "ApplicationName").Value;
_calendarUri = calendarUri;
//FeedQuery feedQuery = new FeedQuery();
_googleService = new Service("cl", applicationName);
_googleCalendar = new CalendarService(applicationName);
// Set your credentials:
_googleService.setUserCredentials(username, password);
_googleCalendar.setUserCredentials(username, password);
return true;
}
return false;
}
public void AddEvent(string title, string contents, string location, DateTime startTime, DateTime endTime)
{
if (!Connect()) return;
EventEntry.EVENT_CATEGORY = new AtomCategory("Appointments");
EventEntry entry = new EventEntry
{
Title = { Text = title },
Content = { Content = contents },
};
// Set the title and content of the entry.
// Set a location for the event.
Where eventLocation = new Where();
eventLocation.ValueString = location;
entry.Locations.Add(eventLocation);
When eventTime = new When(startTime, endTime);
entry.Times.Add(eventTime);
Uri postUri = new Uri("http://www.google.com/calendar/feeds/default/private/full");
// Send the request and receive the response:
AtomEntry insertedEntry = _googleCalendar.Insert(postUri, entry);
}
public void DeleteEvent(string eventId)
{
if (!Connect()) return;
var events = GetAllEvents(null);
var appointment = events.First(x => x.EventId == eventId);
_googleService.Delete(appointment);
}
}
I'm growing desperate at this point, any help would be very appreciated. Include your twitter handle in your answer and I'll buy you a coffee!
UPDATED
I currently have the following, but I is still not authenticating... :(
static CalendarService BuildService()
{
String serviceAccountEmail = "xxxxxxxxxxxxx-31xxxxxxxxxxxxxxxxxxx#developer.gserviceaccount.com";
var certPath = HttpContext.Current.Server.MapPath("/xxxxxxxxxxxx.p12");
var certificate = new X509Certificate2(certPath, "notasecret", X509KeyStorageFlags.Exportable);
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
Scopes = new[] { CalendarService.Scope.Calendar }
}.FromCertificate(certificate));
// Create the service.
var service = new CalendarService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential, <<<<<< DOES NOT RESOLVE!
ApplicationName = "MyApplication",
});
var test = service.Calendars.Get("xxxxxxxxxxxxxxxxxx#group.calendar.google.com");
return service;
}
The problem is that you are storing credentials in plaintext. In Oauth2 the users won't give you their credentials (thus access to everything) but instead they enable your app to access the data of a specific type / scope.
It's not clear from your description whether you only ever access one calendar fully in your control or you have multiple users. In the first case the answer would be use service account (https://developers.google.com/accounts/docs/OAuth2ServiceAccount). In the second case if you are a calendar app with many users, you will need to go down the user consent road and you should read on :)
For offline access you can specify that the access_type should be offline when retrieving the credentials for the first time. Together with the access token you'll also get a refresh token, which you can use to re-authenticate at any later time without any more user clicks (https://developers.google.com/accounts/docs/OAuth2WebServer#refresh). However, at least one consent screen it is.