I'm implementing Google OAuth in ASP.Net MVC application using Google's OAuth .Net library. Below is my code.
IAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow(
new GoogleAuthorizationCodeFlow.Initializer {
ClientSecrets = new ClientSecrets {
** ClientId ** , ** ClientSecret **
},
DataStore = new FileDataStore( ** responsepath ** , true),
Scopes = new [] {
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/gmail.send"
},
Prompt = "select_account"
});
var userId = "user";
var uri = Request.Url.ToString();
var code = Request["code"];
if (code != null) {
var token = flow.ExchangeCodeForTokenAsync(userId, code, uri.Substring(0, uri.IndexOf("?")), CancellationToken.None).Result;
var oauthState = AuthWebUtility.ExtracRedirectFromState(flow.DataStore, userId, Request["state"]).Result;
Response.Redirect(oauthState);
} else {
var result = new AuthorizationCodeWebApp(flow, uri, uri).AuthorizeAsync(userId, CancellationToken.None).Result;
if (result.RedirectUri != null) {
Response.Redirect(result.RedirectUri);
}
}
When user click's Google sign-in button, my page is redirected to Google authentication page. After successful authentication, my page is displayed again. When I check the responsepath, below file is created which contains access token, expiry time, etc.
Google.Apis.Auth.OAuth2.Responses.TokenResponse-user
When I run the above code locally in my visual studio debugging environment (IIS express), the above response file has "refresh_token" in it. When the same code is deployed in production environment (IIS), the "refresh_token" is missing is response file. I would like to know the reason behind it since I need refresh token for further processing.
Note: In both the cases, I revoked the application's access from my Google account before trying. So, this is not about "refresh_token" will be sent only for the first time.
Adding prompt=consent parameter while sending request to Google gives refresh token every time without fail.
Related
I’m working on a project where I need access to a users mailbox (similar to how the MS Flow mailbox connector works), this is fine for when the user is on the site as I can access their mailbox from the graph and the correct permissions request. The problem I have is I need a web job to continually monitor that users mail folder after they’ve given permission. I know that I can use an Application request rather than a delegate request but I doubt my company will sign this off. Is there a way to persistently hold an azure token to access the user information after a user has left the site.. e.g. in a webjob?
Edit
Maybe I've misjudged this, the user authenticates in a web application against an Azure Application for the requested scope
let mailApp : PublicClientApplication = new PublicClientApplication(msalAppConfig);
let mailUser = mailApp.getAllAccounts()[0];
let accessTokenRequest = {
scopes : [ "User.Read", "MailboxSettings.Read", "Mail.ReadWrite", "offline_access" ],
account : mailUser,
}
mailApp.acquireTokenPopup(accessTokenRequest).then(accessTokenResponse => {
.....
}
This returns the correct response as authenticated.
I then want to use this users authentication in a Console App / Web Job, which I try to do with
var app = ConfidentialClientApplicationBuilder.Create(ClientId)
.WithClientSecret(Secret)
.WithAuthority(Authority, true)
.WithTenantId(Tenant)
.Build();
System.Threading.Tasks.Task.Run(async () =>
{
IAccount test = await app.GetAccountAsync(AccountId);
}).Wait();
But the GetAccountAsync allways comes back as null?
#juunas was correct that the tokens are refreshed as needed and to use the AcquireTokenOnBehalfOf function. He should be credited with the answer if possible?
With my code, the idToken returned can be used anywhere else to access the resources. Since my backend WebJob is continuous, I can use the the stored token to access the resource and refresh the token on regular intervals before it expires.
Angalar App:
let mailApp : PublicClientApplication = new PublicClientApplication(msalAppConfig);
let mailUser = mailApp.getAllAccounts()[0];
let accessTokenRequest = {
scopes : [ "User.Read", "MailboxSettings.Read", "Mail.ReadWrite", "offline_access" ],
account : mailUser,
}
mailApp.acquireTokenPopup(accessTokenRequest).then(accessTokenResponse => {
let token : string = accessTokenResponse.idToken;
}
On the backend, either in an API, webJob or Console:
var app = ConfidentialClientApplicationBuilder.Create(ClientId)
.WithClientSecret(Secret)
.WithAuthority(Authority, true)
.WithTenantId(Tenant)
.Build();
var authProvider = new DelegateAuthenticationProvider(async (request) => {
// Use Microsoft.Identity.Client to retrieve token
List<string> scopes = new List<string>() { "Mail.ReadWrite", "MailboxSettings.Read", "offline_access", "User.Read" };
var assertion = new UserAssertion(YourPreviouslyStoredToken);
var result = await app.AcquireTokenOnBehalfOf(scopes, assertion).ExecuteAsync();
request.Headers.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", result.AccessToken);
});
var graphClient = new GraphServiceClient(authProvider);
var users = graphClient.Me.MailFolders.Request().GetAsync().GetAwaiter().GetResult();
In the end I had to abandon using the ConfidentialClientApplicationBuilder, I still use PublicClientApplicationBuilder on the front end to get the users consent but then I handle everything else with the oauth2/v2.0/token rest services which returns and accepts refresh tokens.
That way I can ask the user for mailbox consent using PublicClientApplicationBuilder
Access the user mailbox at any time using oauth2/v2.0/token
I've written an ASP.NET Core webapp that uses Auth0 as its primary authorization mechanism for users, which middlemans a whole bunch of external auth endpoints like Google and Facebook. That works fine and I have no issues there.
At its core the webapp makes use of Google Analytics to perform its own analytics and business logic. The Google Analytics account that is being analysed by my webapp could and is likely different from the users' own Google account. To be clear what I mean is that it is likely the user will login with whatever login provider they wish, and then they'll attach a specific Google business account with access to their businesses Google Analytics system.
The webapp performs analytics both whilst the user is logged in, and whilst the user is offline.
So I've always kept the user auth (Auth0) step seperate from the auth of the Analytics account step. The general process is as follows:
User logs in via Auth0 using whatever provider (Google, Facebook, email/pass) and accesses the private dashboard.
User sets up a "Company" and clicks on a button to authorize our webapp access to a specific Google account with Analytics on it.
User is redirected back to the private dashboard and the refresh token of the Google account is stored for future use.
Previously I had been pushing the Analytics auth through Auth0 as well, and I used a cached Auth0 refresh token to do work offline. However it expires after some days and Auth0 don't appear to provide long-term offline access.
So I figure the easiest thing to do would be to simply not use auth0 for the Analytics auth step, auth directly with the Google API and store the Google refresh token long-term. However I cannot find any concrete examples of how to achieve this!
Official Google API .NET Example - This appears to be very old and not really supported by ASPNET Core. I can't see a clear way to mould this into anything usable and searching SO finds clear issues with it.
SO answer to a similar question - It's a great answer, but the implementation is for user auth and I don't believe would work in my scenario.
I finally cracked it! I ended up throwing away all the libraries and found that it was simplest to use the plain old REST API. Code example below for those curious:
The users' browser GETs the following and is redirected to Google for an auth token:
public IActionResult OnGet([FromQuery]int id, [FromQuery]string returnAction)
{
var org = context.Organizations.Include(o => o.UserOrgs).First(o => o.Id == id);
var user = GetUser();
if (!IsUserMemberOfOrg(user, org)) return BadRequest("User is not a member of this organization!");
var redirectUri = Uri.EscapeUriString(GetBaseUri()+"dash/auth/google?handler=ReturnCode");
var uri = $"https://accounts.google.com/o/oauth2/v2/auth?"+
$"scope={Uri.EscapeUriString("https://www.googleapis.com/auth/analytics.readonly")}"+
$"&prompt=consent"+
$"&access_type=offline"+
//$"&include_granted_scopes=true"+
$"&state={Uri.EscapeUriString(JsonConvert.SerializeObject(new AuthState() { OrgId = id, ReturnAction = returnAction }))}"+
$"&redirect_uri={redirectUri}"+
$"&response_type=code"+
$"&client_id={_configuration["Authentication:Google:ClientId"]}";
return Redirect(uri);
}
Google redirects back to the following, and which point I perform a POST from the webserver to a Google API to exchange the auth token for a refresh token and store it for later:
public async Task<IActionResult> OnGetReturnCode([FromQuery]string state, [FromQuery]string code, [FromQuery]string scope)
{
var authState = JsonConvert.DeserializeObject<AuthState>(state);
var id = authState.OrgId;
var returnAction = authState.ReturnAction;
var org = await context.Organizations.Include(o => o.UserOrgs).SingleOrDefaultAsync(o => o.Id == id);
if (org == null) return BadRequest("This Org doesn't exist!");
using (var httpClient = new HttpClient())
{
var redirectUri = Uri.EscapeUriString(GetBaseUri()+"dash/auth/google?handler=ReturnCode");
var dict = new Dictionary<string, string>
{
{ "code", code },
{ "client_id", _configuration["Authentication:Google:ClientId"] },
{ "client_secret", _configuration["Authentication:Google:ClientSecret"] },
{ "redirect_uri", redirectUri },
{ "grant_type", "authorization_code" }
};
var content = new FormUrlEncodedContent(dict);
var response = await httpClient.PostAsync("https://www.googleapis.com/oauth2/v4/token", content);
var resultContent = JsonConvert.DeserializeObject<GoogleRefreshTokenPostResponse>(await response.Content.ReadAsStringAsync());
org.GoogleAuthRefreshToken = resultContent.refresh_token;
await context.SaveChangesAsync();
return Redirect($"{authState.ReturnAction}/{authState.OrgId}");
}
}
Finally, we can get a new access token with the refresh token later on without user intervention:
public async Task<string> GetGoogleAccessToken(Organization org)
{
if(string.IsNullOrEmpty(org.GoogleAuthRefreshToken))
{
throw new Exception("No refresh token found. " +
"Please visit the organization settings page" +
" to setup your Google account.");
}
using (var httpClient = new HttpClient())
{
var dict = new Dictionary<string, string>
{
{ "client_id", _configuration["Authentication:Google:ClientId"] },
{ "client_secret", _configuration["Authentication:Google:ClientSecret"] },
{ "refresh_token", org.GoogleAuthRefreshToken },
{ "grant_type", "refresh_token" }
};
var resp = await httpClient.PostAsync("https://www.googleapis.com/oauth2/v4/token",
new FormUrlEncodedContent(dict));
if (resp.IsSuccessStatusCode)
{
dynamic returnContent = JObject.Parse(await resp.Content.ReadAsStringAsync());
return returnContent.access_token;
} else
{
throw new Exception(resp.ReasonPhrase);
}
}
}
I'm using the Facebook C# SDK to authorize my app login and post on the user's timeline. So far the login feature works and permissions are authorized, but when my code gets to the PostTaskAsync method, my app crashes with an error stating:
The program '[1020] LC Points.WindowsPhone.exe' has exited with code 0 (0x0).
I've been following this example on SO but their is no Api method available in the SDK I'm using.
Does anyone know how to debug or resolve this posting issue?
I've submitted the publish_actions for review on the Facebook developer console, but don't think that's the reason the app is crashing.
This is the ShareApp Task I'm using to call the login and posting code:
private async Task ShareApp()
{
//Facebook app id
var clientId = "573586446116744";
//Facebook permissions
var scope = "public_profile, email, publish_actions";
var redirectUri = WebAuthenticationBroker.GetCurrentApplicationCallbackUri().ToString();
var fb = new FacebookClient();
var app = new FacebookClient(fb.AccessToken);
var loginUrl = fb.GetLoginUrl(new
{
client_id = clientId,
redirect_uri = redirectUri,
response_type = "token",
scope = scope
});
Uri startUri = loginUrl;
Uri endUri = new Uri(redirectUri, UriKind.Absolute);
WebAuthenticationBroker.AuthenticateAndContinue(startUri, endUri, null, WebAuthenticationOptions.None);
//Code to post app on the user's timeline after button click..crashes when program execution comes to this code:
var postArgs = new Dictionary<string, object>();
postArgs["link"] = "http://allaboutwindowsphone.com/software/developer/Brian-Varley.php";
postArgs["name"] = "More from BV Apps..";
postArgs["message"] = "I'm using LC Points to calculate my Leaving Cert Points!";
await app.PostTaskAsync("/me/feed", postArgs);
//app.Api("/me/feed", postArgs, HttpMethod.Post); //no Api method available which is why I've used PostTaskAsync instead.
}
This is the link to the repo on my GitHub for reference
I'm trying to use the Google Drive and Spreadsheets APIs from a C# console app. I'd like to authorize both services using user credentials with a FileDataStore so that I don't have to reauth my app every single time it runs. Below is how I'm authorizing my Drive service object:
var userCredential = GoogleWebAuthorizationBroker.AuthorizeAsync
(
new ClientSecrets
{
ClientId = "[clientID]",
ClientSecret = "[clientSecret]"
},
new []
{
"https://www.googleapis.com/auth/drive",
"https://spreadsheets.google.com/feeds"
},
"[userName]",
CancellationToken.None,
new FileDataStore("MyApp.GoogleDrive.Auth.Store")
).Result;
var driveService = new DriveService
(
new BaseClientService.Initializer
{
HttpClientInitializer = userCredential,
ApplicationName = "MyApp",
}
);
For the Spreadsheets service, I'm authorizing as prescribed by this guide, but every time I run my app, I have to open a browser to the given auth URL and manually copy in the access token to get it to work.
Is there a way to auth once, obtain the user credentials as above, and use them with both services? Note, I'm authorizing with both the Drive and the Spreadsheets scope, so I don't think there's a problem with that.
I've tried to make it work like this, but I keep getting 400 Bad Request errors when I attempt to insert rows into my spreadsheet:
var auth = new OAuth2Parameters
{
ClientId = "[clientID]",
ClientSecret = "[clientSecret]",
RedirectUri = "[redirectUri]",
Scope = "https://www.googleapis.com/auth/drive https://spreadsheets.google.com/feeds" ,
AccessToken = userCredential.Token.AccessToken,
RefreshToken = userCredential.Token.RefreshToken,
TokenType = userCredential.Token.TokenType,
};
var requestFactory = new GOAuth2RequestFactory(null, "MyApp", auth);
var spreadsheetsService = new SpreadsheetsService("MyApp")
{
Credentials = new GDataCredentials(userCredential.Token.TokenType + " " + userCredential.Token.AccessToken),
RequestFactory = requestFactory,
};
Is there a way to auth once, obtain the user credentials as above, and use them with both services?
Yes. Provided you have included all scopes and have requested offline access, then you'll get a refresh token which you can store and reuse to get access tokens as needed. Obv you need to consider the security implications.
A 400 bad request doesn't sound like an OAuth issue. I think you have two questions/issues here and it might be worth starting a new thread. Include the http request/response for the 400 in your question.
I am attempting to download metric data from Google Analytics using C# and am performing user authentication with OAuth 2.0. I'm using the Installed Application authorisation flow, which requires logging into Google and copy-and-pasting a code into the application. I'm following the code taken from google-api-dotnet-client:
private void DownloadData()
{
Service = new AnalyticsService(new BaseClientService.Initializer() {
Authenticator = CreateAuthenticator(),
});
var request = service.Data.Ga.Get(AccountID, StartDate, EndDate, Metrics);
request.Dimensions = Dimensions;
request.StartIndex = 1;
request.MaxResults = 10000;
var response = request.Execute(); // throws Google.GoogleApiException
}
private IAuthenticator CreateAuthenticator()
{
var provider = new NativeApplicationClient(GoogleAuthenticationServer.Description) {
ClientIdentifier = "123456789012.apps.googleusercontent.com",
ClientSecret = "xxxxxxxxxxxxxxxxxxxxxxxx",
};
return new OAuth2Authenticator<NativeApplicationClient>(provider, Login);
}
private static IAuthorizationState Login(NativeApplicationClient arg)
{
// Generate the authorization URL.
IAuthorizationState state = new AuthorizationState(new[] { AnalyticsService.Scopes.AnalyticsReadonly.GetStringValue() });
state.Callback = new Uri(NativeApplicationClient.OutOfBandCallbackUrl);
Uri authUri = arg.RequestUserAuthorization(state);
// Request authorization from the user by opening a browser window.
Process.Start(authUri.ToString());
Console.Write("Google Authorization Code: ");
string authCode = Console.ReadLine();
// Retrieve the access token by using the authorization code.
state = arg.ProcessUserAuthorization(authCode, state);
return state;
}
The Google account xxxxxx#gmail.com registered the Client ID and secret. The same account has full administration rights in Google Analytics. When I try to pull data from Google Analytics, it goes through the authorisation process, which appears to work properly. Then it fails with:
Google.GoogleApiException
Google.Apis.Requests.RequestError
User does not have sufficient permissions for this profile. [403]
Errors [
Message[User does not have sufficient permissions for this profile.] Location[ - ] Reason [insufficientPermissions] Domain[global]
]
I've been struggling with this for a few hours. I've double checked that the correct user is being used, and is authorised on Google Analytics. I'm at a loss as to what is misconfigured. Any ideas as to what requires configuring or changing?
If auth seems to be working working then my suggestion is that you make sure you're providing the correct ID because based on your code snippet:
var request = service.Data.Ga.Get(AccountID, StartDate, EndDate, Metrics);
one can only assume that you're using the Account ID. If so, that is incorrect and you'd receive the error you've encountered. You need to query with the Profile ID.
If you login to Google Analytics using the web interface you'll see the following pattern in URL of the browser's address bar:
/a12345w654321p9876543/
The number following the p is the profile ID, so 9876543 in the example above. Make sure you're using that and actually you should be using the table id which would be ga:9876543.
If it isn't an ID issue then instead query the Management API to list accounts and see what you have access to and to verify auth is working correctly.
This can help : https://developers.google.com/analytics/devguides/reporting/core/v3/coreErrors, look error 403.
//Thanks for this post. The required profile id can be read from the account summaries.
Dictionary profiles = new Dictionary();
var accounts = service.Management.AccountSummaries.List().Execute();
foreach (var account in accounts.Items)
{
var profileId = account.WebProperties[0].Profiles[0].Id;
profiles.Add("ga:" + profileId, account.Name);
}