I'm trying to retrieve the reviews from our business account.
For this I'm using the Google.Apis.Auth.OAuth2.Mvc library for .net https://www.nuget.org/packages/Google.Apis.Auth.MVC/ and following this example https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth#web-applications-asp.net-mvc.
The library is supposed to use the refresh token automatically but for some reason after 1 hour when the access token expires we lost access to the reviews.
Here is my implementation:
public class AppFlowMetadata : FlowMetadata
{
private static readonly IAuthorizationCodeFlow flow =
new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = "xxxxxxxxxxxxxxxxxxxxxxxxx",
ClientSecret = "xxxxxxxxxxxxxxxxxxxxx"
},
Scopes = new string[] { "https://www.googleapis.com/auth/plus.business.manage" },
DataStore = new FileDataStore(System.Web.HttpContext.Current.Server.MapPath("/App_Data/MyGoogleStorage"), true)
//DataStore = new FileDataStore(System.Web.HttpContext.Current.Server.MapPath("/App_Data/Drive.Api.Auth.Store"))
});
public override string GetUserId(Controller controller)
{
return "our email address";
}
public override IAuthorizationCodeFlow Flow
{
get { return flow; }
}
}
public async Task<ActionResult> IndexAsync(CancellationToken cancellationToken)
{
var result = await new AuthorizationCodeMvcApp(this, new AppFlowMetadata()).
AuthorizeAsync(cancellationToken);
if (result.Credential != null)
{
var accessToken = result.Credential.Token.AccessToken;
var client = new RestClient("https://mybusiness.googleapis.com/v4/accounts/116326379071192580211/locations/6608127685860731136/reviews?access_token=" + accessToken);
var request = new RestRequest(Method.GET);
IRestResponse response = client.Execute(request);
GoogleReviewsModel googleReviews = Newtonsoft.Json.JsonConvert.DeserializeObject<GoogleReviewsModel>(response.Content);
return View("Index", googleReviews);
}
else
{
return new RedirectResult(result.RedirectUri);
}
}
public class AuthCallbackController : Google.Apis.Auth.OAuth2.Mvc.Controllers.AuthCallbackController
{
protected override Google.Apis.Auth.OAuth2.Mvc.FlowMetadata FlowData
{
get { return new AppFlowMetadata(); }
}
}
The reviews are from our own company so we don't to logins from different users. What I want to achieve is to login the first time with our company logins and then automatically refresh the access token with the refresh token so the reviews are always visible in the website,
Thanks a lot!
EDIT:
After 1 hour the response I get from the following code is this:
var accessToken = result.Credential.Token.AccessToken;
var client = new RestClient("https://mybusiness.googleapis.com/v4/accounts/116326379071192580211/locations/6608127685860731136/reviews?access_token=" + accessToken);
var request = new RestRequest(Method.GET);
IRestResponse response = client.Execute(request);
""message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.\",\n "status": "UNAUTHENTICATED"
result.Credential contains both the access token and the refresh token so it seems to read the file in app_data. But the access code seems to be expired and is not being refreshed at this point and is not asking to login again neither:
var result = await new AuthorizationCodeMvcApp(this, new AppFlowMetadata()).
AuthorizeAsync(cancellationToken);
You are not actually "telling the library" to refresh the access token, you are directly using the token that had been stored, and is now expired. Your code that looks like this:
var accessToken = result.Credential.Token.AccessToken;
should look like this:
var accessToekn = await result.Credential.GetAccessTokenForRequestAsync();
The GetAccessTokenForRequestAsync method will check if the access token needs refreshing and do so when needed.
Related
I have been working on a program that scans an exchange inbox for specific emails from a specified address. Currently the program reads the inbox, downloads the attachment, and moves the email to another folder. However, after about 15 pulls from the EWS server, the connection starts giving a 401 Unauthorized error until I restart the program. The program is setup to login via OAuth as basic auth is disabled by the system administrator. Below is the code that I am using to obtain the exchange connection and read the emails from the inbox.
Exchange Connection Code:
public static async Task<ExchangeService> GetExchangeConnection()
{
var pcaOptions = new PublicClientApplicationOptions
{
ClientId = AppID,
TenantId = TenantID,
};
var pca = PublicClientApplicationBuilder.CreateWithApplicationOptions(pcaOptions).Build();
var ewsScopes = new string[] { "https://outlook.office365.com/EWS.AccessAsUser.All" };
var securePassword = new SecureString();
foreach (char c in Pasword)
securePassword.AppendChar(c);
try
{
var authResult = await pca.AcquireTokenByUsernamePassword(ewsScopes, Username, securePassword).ExecuteAsync();
ExchangeService exchangeService = new ExchangeService()
{
Credentials = new OAuthCredentials(authResult.AccessToken),
Url = new Uri("https://outlook.office365.com/ews/exchange.asmx"),
};
return exchangeService;
}
catch
{
return null;
}
}
Email Retriever
public static List<Email> RetreiveEmails()
{
ExchangeService exchangeConnection = GetExchangeConnection().Result;
try
{
List<Email> Emails = new List<Email>();
TimeSpan ts = new TimeSpan(0, -5, 0, 0);
DateTime date = DateTime.Now.Add(ts);
SearchFilter.IsGreaterThanOrEqualTo EmailTimeFilter = new SearchFilter.IsGreaterThanOrEqualTo(ItemSchema.DateTimeReceived, date);
if (exchangeConnection != null)
{
FindItemsResults<Item> findResults = exchangeConnection.FindItems(WellKnownFolderName.Inbox, EmailTimeFilter, new ItemView(10));
foreach (Item item in findResults)
{
if (item.Subject != null)
{
EmailMessage message = EmailMessage.Bind(exchangeConnection, item.Id);
message.Load(new PropertySet(BasePropertySet.FirstClassProperties, ItemSchema.TextBody));
Emails.Add(new Email(message.DateTimeReceived, message.From.Name.ToString(), message.Subject, message.TextBody.ToString(), (message.HasAttachments) ? "Yes" : "No", message.Id.ToString()));
}
}
}
exchangeConnection = null;
return Emails;
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
return null;
}
}
The error occurs when the email retriever tries to either create the exchange connection or when requesting the emails from the folder. In either case the code will error out and give me 401 unauthorized while using credentials that work for the first dozen times and then fails after so many attempts. I have tried it with multiple different accounts and the issue persists with all of them and I have made sure that the application is authorized to access the exchange inbox. Any suggestions or help is much appreciated.
After doing further tracing regarding the 401 error it resulted in an issue with the token reaching the end of it's 1 hour lifespan. This is due to the original OAuth token having an initial life of 1 hour. This however was able to be fixed by setting up code to automatically refresh the token when needed. Here is the code to address this issue for anyone else who comes across this problem.
Authentication Manager:
class AuthenticationManager
{
protected IPublicClientApplication App { get; set; }
public AuthenticationManager(IPublicClientApplication app)
{
App = app;
}
public async Task<AuthenticationResult> AcquireATokenFromCacheOrUsernamePasswordAsync(IEnumerable<String> scopes, string username, SecureString password)
{
AuthenticationResult result = null;
var accounts = await App.GetAccountsAsync();
if (accounts.Any())
{
try
{
result = await (App as PublicClientApplication).AcquireTokenSilent(scopes, accounts.FirstOrDefault()).ExecuteAsync();
}
catch (MsalUiRequiredException)
{ }
}
if (result == null)
{
result = await (App as PublicClientApplication).AcquireTokenByUsernamePassword(scopes, username, password).ExecuteAsync();
}
return result;
}
}
I am using direct username and password authentication but the line of code can be switched to getting the user authentication via interactive methods as well. The code essentially creates a new instance of the authentication manager with a PublicClientApplication used to initialize it which houses the appID and tenantID. After initializing, you can call the AquireATokenFromCacheOrUsernamePasswordAsync which will attempt to see if there is an account present to get a token against. Next it will attempt to retrieve the previously cached token or refresh the token if it expires in less than 5 minutes. If there is a token available it will return that to the main application. If there isn't a token available, it will acquire a new token using the username and password supplied. Implementation of this code looks something like this,
class ExchangeServices
{
AuthenticationManager Manager = null;
public ExchangeServices(String AppId, String TenantID)
{
var pcaOptions = new PublicClientApplicationOptions
{
ClientId = AppID,
TenantId = TenantID,
};
var pca = PublicClientApplicationBuilder.CreateWithApplicationOptions(pcaOptions).Build();
Manager = new AuthenticationManager(pca);
}
public static async Task<ExchangeService> GetExchangeService()
{
var ewsScopes = new string[] { "https://outlook.office365.com/EWS.AccessAsUser.All" }
var securePassword = new SecureString();
foreach(char c in Password)
securePassword.AppendChar(c);
var authResult = await Manager.AquireATokenFromCacheOrUsernamePasswordAsync(ewsScopes, Username, securePassword);
ExchangeService exchangeService = new ExchangeService()
{
Credentials = new OAuthCredentials(authResult.AccessToken),
Url = new Uri("https://outlook.office365.com/ews/exchange.asmx");
};
return exchangeService;
}
}
The code above is everything laid out that is needed to create a new authentication manager and use it to get and update new tokens while using EWS services through OAuth. This is the solution that I found to fix the issue described above.
There are two type of application in my solution
1)Web api application
2)MMC c# application
Here I created web api application which has facility of token authentication.
In this Application username and password validating from SQL server database.
i.e If any user request for web api token that user detail must be present in database table.(In user Table Id,Username,Password column are there with data).
So my web api application connected to database server.
Now I created MVC c# application which consume web api and access the data.
what I do here that when user put credential to mvc app login screen and that credential goes to api and validate them.
Api will give response of data If user credential are correct.
Here I got JSON response from web api and data like "access_token","Expire_time","refresh_token" etc
I stored all these detail in Session object.
So whenever I request for Getdata() from mvc app I passing 'access_token' to api and retuned result data.
I set web api token timeout 2 minutes.(token get deleted after 2 minutes)
So problem goes here that how I can maintain user login session in web api using refresh_token.I do not want to user again get login screen and come back to that screen.
Because every 2 minutes he will get login screen which is not correct solution.
I want some function when api get timeout access_token and mvc application again call refresh_token and continue data transaction.
Whenever your accesstoken is expired you can pass refresh token and can update the access token like this. Hope this will help you.
[AllowAnonymous]
[HttpPost]
public IHttpActionResult GetAccessToken(RefreshTokenModel getRefreshToken)
{
ApiResponse apiResponse = new ApiResponse();
apiResponse.Message = "Your session has expired. Kindly login again.";
try
{
var getHashToken = GenerateHash.GetHash(getRefreshToken.RefreshToken);
var getRefreshTokenDetails = tokenDetailBl.GetRefreshTokenDetail(getHashToken);
if (getRefreshTokenDetails != null && getRefreshTokenDetails.ExpiresUtc > DateTime.UtcNow && !string.IsNullOrEmpty(getRefreshTokenDetails.ProtectedTicket))
{
if (getRefreshTokenDetails.DeviceType == getRefreshToken.DeviceType)
{
var currentTime = DateTime.UtcNow;
var refreshTokenLifeTime = Convert.ToDouble(ConfigurationManager.AppSettings["RefreshTokenExpireTime"]);
var tokenExpiration = Convert.ToDouble(ConfigurationManager.AppSettings["AccessTokenExpireTime"]);
ApiIdentityManager apiIdentityManager = new ApiIdentityManager();
var tokenData = JsonConvert.SerializeObject(new { Ticket = getRefreshTokenDetails.ProtectedTicket, DeviceId = getRefreshTokenDetails.DeviceId });
var getIdentityToken = apiIdentityManager.GetRefreshToken(tokenData);
// Delete Old Tokens
tokenDetailBl.DeleteAccessTokenByDevice(getRefreshTokenDetails.DeviceId);
var refreshToken = new RefreshToken()
{
RefreshTokenId = GenerateHash.GetHash(getIdentityToken.RefreshToken),
DeviceId = getRefreshTokenDetails.DeviceId,
DeviceType = getRefreshToken.DeviceType,
UserId = getRefreshTokenDetails.UserId,
IssuedUtc = currentTime,
ExpiresUtc = currentTime.AddMinutes(Convert.ToDouble(refreshTokenLifeTime)),
ProtectedTicket = getIdentityToken.Ticket
};
//Save new tokens
tokenDetailBl.SaveAccessToken(new TokenDetail
{
AccessToken = getIdentityToken.AccessToken,
CreatedOn = DateTime.UtcNow,
UserId = getRefreshTokenDetails.UserId,
DeviceId = getRefreshTokenDetails.DeviceId,
DeviceType = getRefreshToken.DeviceType
});
tokenDetailBl.SaveRefreshToken(refreshToken);
//Get token cache.
CachedData cachedData = new CachedData(tokenDetailBl);
var getAllToken = cachedData.GetAccessTokens();
cachedData.UpdateTokenCache(getIdentityToken.AccessToken, getRefreshTokenDetails.UserId + ":" + DateTime.UtcNow.AddMinutes(tokenExpiration).ToFormateDateTimeString());
var getUserDetails = userBl.GetUserDetails(getRefreshToken.UserId);
getUserDetails.DeviceId = getRefreshTokenDetails.DeviceId;
getUserDetails.DeviceType = getRefreshTokenDetails.DeviceType;
getUserDetails.AccessToken = getIdentityToken.AccessToken;
getUserDetails.TokenType = "bearer";
getUserDetails.ExpiresIn = getIdentityToken.ExpiresIn;
getUserDetails.Issued = getIdentityToken.Issued;
getUserDetails.Expires = DateTime.UtcNow.Add(TimeSpan.FromMinutes(tokenExpiration)).ToString("R");
getUserDetails.RefreshToken = getIdentityToken.RefreshToken;
//Dictionary<string, string> tokenResponse = new Dictionary<string, string>();
//tokenResponse.Add("access_token", getIdentityToken.AccessToken);
//tokenResponse.Add("token_type", "bearer");
//tokenResponse.Add("expires_in", getIdentityToken.ExpiresIn);
//tokenResponse.Add("issued", getIdentityToken.Issued);
//tokenResponse.Add("expires", DateTime.UtcNow.Add(TimeSpan.FromMinutes(tokenExpiration)).ToString("R"));
//tokenResponse.Add("refresh_token", getIdentityToken.RefreshToken);
return ResponseMessage(Request.CreateResponse(HttpStatusCode.OK, getUserDetails));
}
else
{
apiResponse.Message = "Your session has expired. Kindly login again.";
}
}
}
catch (Exception ex)
{
Logger.Error(ex);
}
return ResponseMessage(Request.CreateResponse(HttpStatusCode.Gone, apiResponse));
}
You can use MVC filters to check that your access token is expired or not something like this.
[CacheAuthorize]
[HttpPost]
public IHttpActionResult GetUserList(SearchRequest searchRequest)
and after that code to check validation of access token
public class CacheAuthorizeAttribute : AuthorizeAttribute
{
public CacheAuthorizeAttribute(params string[] roles)
: base()
{
Roles = string.Join(",", roles);
}
public override void OnAuthorization(HttpActionContext actionContext)
{
Dictionary<HttpStatusCode, string> response;
if (SkipAuthorization(actionContext))
{
return;
}
var userSessionManager = new UserCacheManager();
if (userSessionManager.ReValidateSession(out response))
{
base.OnAuthorization(actionContext);
}
else
{
ApiResponse apiResponse = new ApiResponse(response.Values.FirstOrDefault());
actionContext.Response = actionContext.ControllerContext.Request.CreateResponse(response.Keys.FirstOrDefault(), apiResponse);
}
}
/// <summary>
/// Re-validates the user session. Usually called at each authorization request.
/// If the session is not expired, extends it lifetime and returns true.
/// If the session is expired or does not exist, return false.
/// </summary>
/// <returns>true if the session is valid</returns>
public bool ReValidateSession(out Dictionary<HttpStatusCode, string> errorResponse)
{
errorResponse = new Dictionary<HttpStatusCode, string>();
string authToken = this.GetCurrentBearerAuthrorizationToken();
ITokenDetailRepository tokenDetailRepository = new TokenDetailRepository();
ITokenDetailBL tokenDetailBl = new TokenDetailBL(tokenDetailRepository);
CachedData cachedData = new CachedData(tokenDetailBl);
if (!string.IsNullOrEmpty(authToken))
{
var currentUserId = this.GetCurrentUserId();
var getUserTokens = cachedData.GetAccessTokens();
if (!getUserTokens.ContainsKey(authToken))
{
//Get Data from DB
cachedData.GetAccessToken(authToken);
getUserTokens = cachedData.GetAccessTokens();
}
return CheckAccessToken(getUserTokens, authToken, out errorResponse);
}
else
{
errorResponse.Add(HttpStatusCode.Gone, "Access token not found.");
}
return false;
}
private bool CheckAccessToken(Dictionary<string, string> accessTokenDictionary, string authToken, out Dictionary<HttpStatusCode, string> errorResponse)
{
errorResponse = new Dictionary<HttpStatusCode, string>();
var hasToken = accessTokenDictionary.ContainsKey(authToken);
if (hasToken)
{
var getTokenValue = accessTokenDictionary[authToken];
var enCulture = new CultureInfo("en-US");
DateTime tokenAddedDate;
var isCorrectDate = DateTime.TryParseExact(getTokenValue.Split(new char[] { ':' }, 2)[1], "dd-MMM-yyyy,hh:mm tt", enCulture, DateTimeStyles.None, out tokenAddedDate);
if (isCorrectDate)
{
if (tokenAddedDate >= DateTime.UtcNow)
{
return true;
}
else
{
//Check Refresh token expired or not
errorResponse.Add(HttpStatusCode.Unauthorized, "Access token expired.");
}
}
else
{
errorResponse.Add(HttpStatusCode.Gone, "Invalid access token.");
}
}
else
{
errorResponse.Add(HttpStatusCode.Gone, "Invalid access token.");
}
return false;
}
I have two projects. One is an Identity server 4 who handle users and authentication. The second need to use the first to login and ask for a token to access an API.
When I need to refresh the token I don't now how to handle the new access token. How I can set to the authentification asp dot net core the new token. All the refresh process are made on a AuthorizationHandler.
I tried to modify the claims on identity but doesnt work. I tried to stock access token and refresh token inside my own cookie but I have trouble because when I refresh the token I can only use them at the next request (I didn't achieve to modify the request.cookies only the response.cookies.
public static async Task SetToken(TokenKind token,string value, HttpContext context)
{
context.Response.Cookies.Append(GetTokenCookieName(token), value);
}
public static async Task<string> GetRefreshTokenAsync(HttpContext context)
{
return await SearchToken(TokenKind.Refresh,context);
}
private static async Task<string> SearchToken(TokenKind token, HttpContext context)
{
var tokenName = GetTokenName(token);
var test = context.Request.Cookies;
var apiToken = context.Request.Cookies.FirstOrDefault(x => x.Key == GetTokenCookieName(token)).Value;
if (apiToken == null)
{
// Save token into cookie
var tokenValue = await context.GetTokenAsync(GetTokenName(TokenKind.Access));
await SetToken(TokenKind.Access, tokenValue, context);
var refreshTokenValue = await context.GetTokenAsync(GetTokenName(TokenKind.Refresh));
await SetToken(TokenKind.Refresh, refreshTokenValue, context);
switch (token)
{
case TokenKind.Access:
return tokenValue;
case TokenKind.Refresh:
return refreshTokenValue;
default:
return null;
break;
}
}
else
{
return apiToken;
}
}
private async Task<bool> RefreshToken(AuthorizationFilterContext mvcContext, HttpClient client)
{
var refreshToken = await TokenUtils.GetRefreshTokenAsync(mvcContext.HttpContext);
//await mvcContext.HttpContext.GetTokenAsync("refresh_token");
var variables = new Dictionary<string, string>
{
{ "grant_type", "refresh_token" },
{ "client_id", _configuration["ApplicationOptions:ClientId"] },
{ "client_secret", _configuration["ApplicationOptions:ClientSecret"] },
{ "refresh_token", refreshToken }
};
var content = new FormUrlEncodedContent(variables);
var url = _configuration["ApplicationOptions:AuthorizeServer"] + "/connect/token";
var response = await client.PostAsync(url, content);
if (response.IsSuccessStatusCode == false)
{
var errorString = await response.Content.ReadAsStringAsync();
var errorData = JsonConvert.DeserializeObject<dynamic>(errorString);
return false;
}
var contentAsString = await response.Content.ReadAsStringAsync();
var responseData = JsonConvert.DeserializeObject<dynamic>(contentAsString);
var newAccessToken = (string)responseData.access_token;
var newRefreshToken = (string)responseData.refresh_token;
await TokenUtils.SetAccessToken(newAccessToken, mvcContext.HttpContext);
await TokenUtils.SetRefreshToken(newRefreshToken, mvcContext.HttpContext);
var result = await mvcContext.HttpContext.AuthenticateAsync();
if (result.Succeeded)
{
result.Properties.StoreTokens(new List<AuthenticationToken>
{
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.AccessToken,
Value = newAccessToken
},
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.RefreshToken,
Value = newRefreshToken
}
});
return true;
}
else
{
return false;
}
}
I would like to know what is the good pratice to store (or replace the actual access token) with the .net core authentification. I'm sure I'm not doing it the right way. At the end I want to handle correctly my token and my refresh token for not ask the user to login again. Now with my solution my project will denied the access when it need to refresh the token and next request will be granted (because the cookie need to be resend from the user).
Thank to Ruard van Elburg I found the solution (here's the complete answer)
And that's what I used to replace my tokens:
// Save the information in the cookie
var info = await mvcContext.HttpContext.AuthenticateAsync("Cookies");
info.Properties.UpdateTokenValue("refresh_token", newRefreshToken);
info.Properties.UpdateTokenValue("access_token", newAccessToken);
info.Properties.UpdateTokenValue("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture));
await mvcContext.HttpContext.SignInAsync("Cookies", info.Principal, info.Properties);
I'm trying to use Google Calendar API v3, but i have problems while running the codes, it always gives me that error :
An exception of type 'System.AggregateException' occurred in mscorlib.ni.dll but was not handled in user code
Additional information: One or more errors occurred.
I don't know why it does, also It should work as well. Here is a screenshot for it :
Also my codes are :
UserCredential credential;
credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
new Uri("ms-appx:///Assets/client_secrets.json"),
Scopes,
"user",
CancellationToken.None).Result;
// Create Google Calendar API service.
var service = new CalendarService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = ApplicationName,
});
var calendarService = new CalendarService(new BaseClientService.Initializer
{
HttpClientInitializer = credential,
ApplicationName = "Windows 10 Calendar sample"
});
var calendarListResource = await calendarService.CalendarList.List().ExecuteAsync();
If you can at least help with calling it through REST API, that would be great too, but you must consider that it's UWP, so it has another way to get it work as well.
As i already tried through REST API, but i always get "Request error code 400".
Thanks for your attention.
The Google API Client Library for .NET does not support UWP by now. So we can't use Google.Apis.Calendar.v3 Client Library in UWP apps now. For more info, please see the similar question: Universal Windows Platform App with google calendar.
To use Google Calendar API in UWP, we can call it through REST API. To use the REST API, we need to authorize requests first. For how to authorize requests, please see Authorizing Requests to the Google Calendar API and Using OAuth 2.0 for Mobile and Desktop Applications.
After we have the access token, we can call Calendar API like following:
var clientId = "{Your Client Id}";
var redirectURI = "pw.oauth2:/oauth2redirect";
var scope = "https://www.googleapis.com/auth/calendar.readonly";
var SpotifyUrl = $"https://accounts.google.com/o/oauth2/auth?client_id={clientId}&redirect_uri={Uri.EscapeDataString(redirectURI)}&response_type=code&scope={Uri.EscapeDataString(scope)}";
var StartUri = new Uri(SpotifyUrl);
var EndUri = new Uri(redirectURI);
// Get Authorization code
WebAuthenticationResult WebAuthenticationResult = await WebAuthenticationBroker.AuthenticateAsync(WebAuthenticationOptions.None, StartUri, EndUri);
if (WebAuthenticationResult.ResponseStatus == WebAuthenticationStatus.Success)
{
var decoder = new WwwFormUrlDecoder(new Uri(WebAuthenticationResult.ResponseData).Query);
if (decoder[0].Name != "code")
{
System.Diagnostics.Debug.WriteLine($"OAuth authorization error: {decoder.GetFirstValueByName("error")}.");
return;
}
var autorizationCode = decoder.GetFirstValueByName("code");
//Get Access Token
var pairs = new Dictionary<string, string>();
pairs.Add("code", autorizationCode);
pairs.Add("client_id", clientId);
pairs.Add("redirect_uri", redirectURI);
pairs.Add("grant_type", "authorization_code");
var formContent = new Windows.Web.Http.HttpFormUrlEncodedContent(pairs);
var client = new Windows.Web.Http.HttpClient();
var httpResponseMessage = await client.PostAsync(new Uri("https://www.googleapis.com/oauth2/v4/token"), formContent);
if (!httpResponseMessage.IsSuccessStatusCode)
{
System.Diagnostics.Debug.WriteLine($"OAuth authorization error: {httpResponseMessage.StatusCode}.");
return;
}
string jsonString = await httpResponseMessage.Content.ReadAsStringAsync();
var jsonObject = Windows.Data.Json.JsonObject.Parse(jsonString);
var accessToken = jsonObject["access_token"].GetString();
//Call Google Calendar API
using (var httpRequest = new Windows.Web.Http.HttpRequestMessage())
{
string calendarAPI = "https://www.googleapis.com/calendar/v3/users/me/calendarList";
httpRequest.Method = Windows.Web.Http.HttpMethod.Get;
httpRequest.RequestUri = new Uri(calendarAPI);
httpRequest.Headers.Authorization = new Windows.Web.Http.Headers.HttpCredentialsHeaderValue("Bearer", accessToken);
var response = await client.SendRequestAsync(httpRequest);
if (response.IsSuccessStatusCode)
{
var listString = await response.Content.ReadAsStringAsync();
//TODO
}
}
}
I have the Google .NET Client working in my UWP app. The trick is that you have to put it in a .NET Standard 2.0 Class Library, expose the API services you need, and then reference that library from your UWP app.
Also, you have to handle the getting the auth token yourself. It's not that much work and the Drive APIs and Calendar APIs work just fine (the only ones I've tried). You can see that I pass in a simple class that contains the auth token and other auth details to a method called Initialize.
Here is the single class I used in the .NET Standard 2.0 class library:
namespace GoogleProxy
{
public class GoogleService
{
public CalendarService calendarService { get; private set; }
public DriveService driveService { get; private set; }
public GoogleService()
{
}
public void Initialize(AuthResult authResult)
{
var credential = GetCredentialForApi(authResult);
var baseInitializer = new BaseClientService.Initializer { HttpClientInitializer = credential, ApplicationName = "{your app name here}" };
calendarService = new Google.Apis.Calendar.v3.CalendarService(baseInitializer);
driveService = new Google.Apis.Drive.v3.DriveService(baseInitializer);
}
private UserCredential GetCredentialForApi(AuthResult authResult)
{
var initializer = new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = "{your app client id here}",
ClientSecret = "",
},
Scopes = new string[] { "openid", "email", "profile", "https://www.googleapis.com/auth/calendar.readonly", "https://www.googleapis.com/auth/calendar.events.readonly", "https://www.googleapis.com/auth/drive.readonly" },
};
var flow = new GoogleAuthorizationCodeFlow(initializer);
var token = new TokenResponse()
{
AccessToken = authResult.AccessToken,
RefreshToken = authResult.RefreshToken,
ExpiresInSeconds = authResult.ExpirationInSeconds,
IdToken = authResult.IdToken,
IssuedUtc = authResult.IssueDateTime,
Scope = "openid email profile https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/calendar.events.readonly https://www.googleapis.com/auth/drive.readonly",
TokenType = "bearer" };
return new UserCredential(flow, authResult.Id, token);
}
}
}
In order to get the Auth token from google, you have to use custom schemes. Register your app as an 'iOS' app on the google services console and put in a URI scheme (something unique). Then add this scheme to your UWP manifest under Declarations->Protocol. Handle it in your App.xaml.cs:
protected override void OnActivated(IActivatedEventArgs args)
{
base.OnActivated(args);
if (args.Kind == ActivationKind.Protocol)
{
ProtocolActivatedEventArgs protocolArgs = (ProtocolActivatedEventArgs)args;
Uri uri = protocolArgs.Uri;
Debug.WriteLine("Authorization Response: " + uri.AbsoluteUri);
locator.AccountsService.GoogleExternalAuthWait.Set(uri.Query);
}
}
That GoogleExternalAuthWait comes from some magical code I found about how to create an asynchronous ManualResetEvent. https://blogs.msdn.microsoft.com/pfxteam/2012/02/11/building-async-coordination-primitives-part-1-asyncmanualresetevent/ It looks like this (I only converted it to generic).
public class AsyncManualResetEvent<T>
{
private volatile TaskCompletionSource<T> m_tcs = new TaskCompletionSource<T>();
public Task<T> WaitAsync() { return m_tcs.Task; }
public void Set(T TResult) { m_tcs.TrySetResult(TResult); }
public bool IsReset => !m_tcs.Task.IsCompleted;
public void Reset()
{
while (true)
{
var tcs = m_tcs;
if (!tcs.Task.IsCompleted ||
Interlocked.CompareExchange(ref m_tcs, new TaskCompletionSource<T>(), tcs) == tcs)
return;
}
}
}
This is how you start the Google Authorization. What happens is it launches an external browser to begin the google signing process and then wait (that's what the AsyncManualResetEvent does). When you're done, Google will launch a URI using your custom scheme. You should get a message dialog saying the browser is trying to open an app... click ok and the AsyncManualResetEvent continues and finishes the auth process. You'll need to make a class that contains all the auth info to pass to your class library.
private async Task<AuthResult> AuthenticateGoogleAsync()
{
try
{
var stateGuid = Guid.NewGuid().ToString();
var expiration = DateTimeOffset.Now;
var url = $"{GoogleAuthorizationEndpoint}?client_id={WebUtility.UrlEncode(GoogleAccountClientId)}&redirect_uri={WebUtility.UrlEncode(GoogleRedirectURI)}&state={stateGuid}&scope={WebUtility.UrlEncode(GoogleScopes)}&display=popup&response_type=code";
var success = Windows.System.Launcher.LaunchUriAsync(new Uri(url));
GoogleExternalAuthWait = new AsyncManualResetEvent<string>();
var query = await GoogleExternalAuthWait.WaitAsync();
var dictionary = query.Substring(1).Split('&').ToDictionary(x => x.Split('=')[0], x => Uri.UnescapeDataString(x.Split('=')[1]));
if (dictionary.ContainsKey("error"))
{
return null;
}
if (!dictionary.ContainsKey("code") || !dictionary.ContainsKey("state"))
{
return null;
}
if (dictionary["state"] != stateGuid)
return null;
string tokenRequestBody = $"code={dictionary["code"]}&redirect_uri={Uri.EscapeDataString(GoogleRedirectURI)}&client_id={GoogleAccountClientId}&access_type=offline&scope=&grant_type=authorization_code";
StringContent content = new StringContent(tokenRequestBody, Encoding.UTF8, "application/x-www-form-urlencoded");
// Performs the authorization code exchange.
using (HttpClientHandler handler = new HttpClientHandler())
{
handler.AllowAutoRedirect = true;
using (HttpClient client = new HttpClient(handler))
{
HttpResponseMessage response = await client.PostAsync(GoogleTokenEndpoint, content);
if (response.IsSuccessStatusCode)
{
var stringResponse = await response.Content.ReadAsStringAsync();
var json = JObject.Parse(stringResponse);
var id = DecodeIdFromJWT((string)json["id_token"]);
var oauthToken = new AuthResult()
{
Provider = AccountType.Google,
AccessToken = (string)json["access_token"],
Expiration = DateTimeOffset.Now + TimeSpan.FromSeconds(int.Parse((string)json["expires_in"])),
Id = id,
IdToken = (string)json["id_token"],
ExpirationInSeconds = long.Parse((string)json["expires_in"]),
IssueDateTime = DateTime.Now,
RefreshToken = (string)json["refresh_token"]
};
return oauthToken;
}
else
{
return null;
}
}
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
return null;
}
}
Fundamentally, I'm not understanding something about OAuth, Google, and the Google Apis. I'm working the v3 library in Asp.Net Core 1.0. I use Google Authentication:
app.UseGoogleAuthentication(new GoogleOptions
{
ClientId = Configuration["Google:ClientId"],
ClientSecret = Configuration["Google:ClientSecret"],
Scope = {
"email",
"https://www.googleapis.com/auth/userinfo.email",
"profile",
"https://www.googleapis.com/auth/userinfo.profile",
"openid",
"https://www.googleapis.com/auth/calendar",
},
AccessType = "offline",
SaveTokens = true,
});
Simple enough. Then I want to use Google Calendar. However, I cannot consistently get back the Access/Refresh tokens (honestly at this point I'm not sure I understand what these are). I use this code to get back a Calendar Service from google:
private AuthorizationCodeFlow CreateFlow(IEnumerable<string> scopes)
{
return new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer()
{
ClientSecrets = new Google.Apis.Auth.OAuth2.ClientSecrets
{
ClientId = _ClientId,
ClientSecret = _ClientSecret
},
DataStore = new MemoryDataStore(),
Scopes = scopes
});
}
private async Task<UserCredential> CreateUserCredential(HttpContext context, string providerKey, IEnumerable<string> scopes)
{
var flow = CreateFlow(scopes);
var token = await context.GetGoogleTokenResponse();
UserCredential credential = new UserCredential(flow, providerKey, token);
return credential;
}
public async Task<CalendarService> CreateCalendarServiceAsync(HttpContext context, string providerKey)
{
if (string.IsNullOrWhiteSpace(providerKey)) return null;
if (context == null) return null;
var credential = await CreateUserCredential(context, providerKey, new[] { "https://www.googleapis.com/auth/calendar" });
CalendarService service = new CalendarService(new BaseClientService.Initializer
{
HttpClientInitializer = credential,
ApplicationName = _ApplicationName,
});
return service;
}
public static async Task<TokenResponse> GetGoogleTokenResponse(this HttpContext context)
{
var info = await context.Authentication.GetAuthenticateInfoAsync("Google");
if (info == null) return null;
var token = new TokenResponse
{
AccessToken = info.Properties.Items[".Token.access_token"],
RefreshToken = info.Properties.Items[".Token.refresh_token"],
TokenType = info.Properties.Items[".Token.token_type"],
Issued = DateTime.Parse(info.Properties.Items[".issued"]),
};
return token;
}
This is the only way I know how to get back the current access/refresh tokens. What am I missing. Sometimes they exist, others times not. Documentation for the .Net Core 1.0 version is slim at best. Any help on a better way to access Google Apis via Asp.Net core?
Since you already set SaveTokens = true, you can get access_token like this: await context.Authentication.GetTokenAsync("access_token");
So changing your method like below probably solves your problem:
private async Task<UserCredential> CreateUserCredential(HttpContext context, string providerKey, IEnumerable<string> scopes)
{
var flow = CreateFlow(scopes);
var token = await context.Authentication.GetTokenAsync("access_token");
UserCredential credential = new UserCredential(flow, providerKey, token);
return credential;
}