Adding Google API Offline access to a .NET Core app - c#

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);
}
}
}

Related

How to completely log out of ASP.NET Core server application when using .NET Maui mobile application and Web Authenticator

I'm working on .NET Maui mobile client with ASP.NET Core backend, and am trying to integrate Google authorization via the Web Authenticator API built into Maui. I was able to get the login to function by basically following the guide on the Web Authenticator page linked above, but haven't been able to logout from the account I originally logged in with. I've created a logout endpoint for my server, and after I hit this endpoint (where I call HttpContext.SignOutAsync() ) and compare the value of ClaimsPrinciple, the data from Google is gone (see attached screenshots).
before logout
after logout
However, the next time I try to log in I do not need to go through Google authentication, it automatically logs me in again. I've seen similar issues with Web Authenticator (linked here and here and in some other issues linked to these). I'm new at this framework and mobile dev in general and I'm still unclear from these resources what the best way to handle this is - most of these issues are also relating to Xamarins forms rather than Maui, so not sure if theres any more updated solution.
These are the implementations of login and logout and the corresponding requests
public class MobileAuthController : ControllerBase
{
const string callbackScheme = "myapp";
[HttpGet("{scheme}")]
public async Task Get([FromRoute] string scheme)
{
var auth = await Request.HttpContext.AuthenticateAsync(scheme);
if (!auth.Succeeded
|| auth?.Principal == null
|| !auth.Principal.Identities.Any(id => id.IsAuthenticated)
|| string.IsNullOrEmpty(auth.Properties.GetTokenValue("access_token")))
{
//Not authenticated, challenge
await Request.HttpContext.ChallengeAsync(scheme);
}
else
{
var claims = auth.Principal.Identities
.FirstOrDefault().Claims.Select(claim => new
{
claim.Issuer,
claim.OriginalIssuer,
claim.Type,
claim.Value
});
//Build the result url
var user = this.User; //CHECK VALUE OF USER
var url = callbackScheme + "://#";
//Redirect to final url
Request.HttpContext.Response.Redirect(url);
}
}
[HttpPost("logout")]
public async Task Logout()
{
try
{
await HttpContext.SignOutAsync();
var url = callbackScheme + "://#";
var user = this.User; //CHECK VALUE OF USER
}
catch (Exception ex)
{
Debug.WriteLine($"Unable to sign out user {ex.Message}");
}
}
var url = "http://localhost:5000/mobileauth/logout";
HttpContent content = null;
var response = await httpClient.PostAsync(url, content);
WebAuthenticatorResult authResult = await WebAuthenticator.Default.AuthenticateAsync(
new Uri("https://localhost:5001/mobileauth/Google"),
new Uri("myapp://"));
Edit - I'm primarily confused on how i need to be handling Cookies and access/refresh tokens and the built in claims stuff - like I said, I'm new at this

Refresh token missing in Google Oauth response file

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.

Implementing External Authentication for Mobile App in ASP.NET WebApi 2

I'm trying to build an API (using ASP.NET WebApi) that will be consumed by a native mobile app for a school project. (I'm not concerned about/developing the mobile app, this responsibility falls on a different member)
I'm at a point where I need to implement a token based Facebook login. There are a lot of tutorials available for how to implement this feature for browser based apps (this is pretty straight forward and most of it comes inbuilt), but I don't think I follow how this would work with native apps. What I don't understand is how the redirects would work?
According to this link, nothing needs to be handled specifically by my server. And I don't think I understand how this would work? How would the tokens from Facebook be handled?
Also, what part of token handling should I implement, I couldn't really find good documentation for WebApi external login authentication.
Anyway, if someone could point me to the exact flow of token exchanges that happen and what is implemented by default by ASP.NET, that would be super helpful.
Also, the biggest point of confusion for me is I don't understand how the token returned by Facebook will be handled.
I assume the token will be returned to the client (mobile app), how do I get access to it on my server?
How do I create a local token from facebook's token?
Is this all done internally/auto-magically by ASP.NET?
I'm sorry if this is something I should've been able to figure out. I did do quite a bit of research and I found myself drowning in (related & unrelated) information. I don't think I even know how to search for the information I need.
Some links I've read:
Claims And Token Based Authentication (ASP.NET Web API)
Token Based Authentication using ASP.NET Web API 2, Owin, and Identity
ASP.NET Web API 2 external logins with Facebook and Google in AngularJS app
I had to do pretty much the same thing for an application I was working on. I also had a lot of trouble finding information about it. It seemed like everything I found was close to what I needed, but not exactly the solution. I ended up taking bits and pieces from a bunch of different blog posts, articles, etc. and putting them all together to get it to work.
I remember two of the links you posted "Claims and Token Based Authentication" and "ASP.NET Web API 2 external logins with Facebook and Google in AngularJS app" as being ones that had useful information.
I can't give you a comprehensive answer since I don't remember everything I had to do, nor did I even understand everything I was doing at the time, but I can give you the general idea. You are on the right track.
Essentially I ended up using the token granted by Facebook to confirm that they were logged into their Facebook account, created a user based on their Facebook user ID, and granted them my own bearer token that they could use to access my API.
The flow looks something like this:
Client authenticates with Facebook via whatever method (we used oauth.io)
Facebook returns them a token
Client sends token information to the registration endpoint of my WebApi controller
The token is validated using Facebook's Graph API, which returns user info
A user is created in the database via ASP.NET Identity with their Facebook user ID as the key
Client sends token information to the authentication endpoint of my WebApi controller
The token is validated using Facebook's Graph API, which returns user info
The user info is used to look up the user in the database, confirm they have previously registered
ASP.NET Identity is used to generate a new token for that user
That token is returned to the client
Client includes an Authorization header in all future HTTP requests with the new token granted by my service (ex. "Authorization: Bearer TOKEN")
If the WebApi endpoint has the [Authorize] attribute, ASP.NET Identity will automatically validate the bearer token and refuse access if it is not valid
There ended up being a lot of custom code for implementing the OAuth stuff with ASP.NET Identity, and those links you included show you some of that. Hopefully this information will help you a little bit, sorry I couldn't help more.
I followed this article. The flow is basically this
The server has the facebook keys just like with web login
The app asks for available social logins and displays buttons (you can hardcode this I guess)
When a button is pressed the app opens a browser and sets the URL to the one related to the specified social login. The ASP.NET then redirects the browser to facebook/google/whatever with the appropriate Challenge
The user might be logged in or not and might have given permission to your app or not. After he gives the permissions facebook redirects back to the provided callback URL
At that point you can get the external login info from the SignInManager and check if the user already exists and if you should create a new account
Finally a token is generated and the browser is redirected to a URL in which the token is placed. The app gets the token from the URL and closes the browser. Uses the token to proceed with API requests.
Honestly I have no idea if this approach is legit...
The code of the action buttons should redirect to:
public async Task<IEnumerable<ExternalLoginDto>> GetExternalLogins(string returnUrl, bool generateState = false)
{
IEnumerable<AuthenticationScheme> loginProviders = await SignInManager.GetExternalAuthenticationSchemesAsync();
var logins = new List<ExternalLoginDto>();
string state;
if (generateState)
{
const int strengthInBits = 256;
state = RandomOAuthStateGenerator.Generate(strengthInBits);
}
else
{
state = null;
}
foreach (AuthenticationScheme authenticationScheme in loginProviders)
{
var routeValues = new
{
provider = authenticationScheme.Name,
response_type = "token",
client_id = Configuration["Jwt:Issuer"],
redirect_uri = $"{Request.Scheme}//{Request.Host}{returnUrl}",
state = state
};
var login = new ExternalLoginDto
{
Name = authenticationScheme.DisplayName,
Url = Url.RouteUrl("ExternalLogin", routeValues),
State = state
};
logins.Add(login);
}
return logins;
}
The code for the callback action:
[Authorize(AuthenticationSchemes = "Identity.External")]
[Route("ExternalLogin", Name = "ExternalLogin")]
public async Task<IActionResult> GetExternalLogin(string provider, string state = null, string client_id = null, string error = null)
{
if (error != null)
{
ThrowBadRequest(error);
}
if (!User.Identity.IsAuthenticated)
{
return new ChallengeResult(provider);
}
string providerKey = User.FindFirstValue(ClaimTypes.NameIdentifier);
var externalLoginInfo = new ExternalLoginInfo(User, User.Identity.AuthenticationType, providerKey, User.Identity.AuthenticationType);
if (externalLoginInfo.LoginProvider != provider)
{
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
return new ChallengeResult(provider);
}
var userLoginInfo = new UserLoginInfo(externalLoginInfo.LoginProvider, externalLoginInfo.ProviderKey, externalLoginInfo.ProviderDisplayName);
User user = await UserManager.FindByLoginAsync(externalLoginInfo.LoginProvider, externalLoginInfo.ProviderKey);
if (client_id != Configuration["Jwt:Issuer"])
{
return Redirect($"/#error=invalid_client_id_{client_id}");
}
if (user != null)
{
return await LoginWithLocalUser(user, state);
}
else
{
string email = null;
string firstName = null;
string lastName = null;
IEnumerable<Claim> claims = externalLoginInfo.Principal.Claims;
if (externalLoginInfo.LoginProvider == "Google")
{
email = claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value;
firstName = claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.Value;
lastName = claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname)?.Value;
}
else if (externalLoginInfo.LoginProvider == "Facebook")
{
email = claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value;
string[] nameParts = claims.First(c => c.Type == ClaimTypes.Name)?.Value.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
firstName = nameParts?.First();
lastName = nameParts?.Last();
}
//some fallback just in case
firstName ??= externalLoginInfo.Principal.Identity.Name;
lastName ??= externalLoginInfo.Principal.Identity.Name;
user = new User
{
UserName = email,
Email = email,
FirstName = firstName,
LastName = lastName,
EmailConfirmed = true //if the user logs in with Facebook consider the e-mail confirmed
};
IdentityResult userCreationResult = await UserManager.CreateAsync(user);
if (userCreationResult.Succeeded)
{
userCreationResult = await UserManager.AddLoginAsync(user, userLoginInfo);
if (userCreationResult.Succeeded)
{
return await LoginWithLocalUser(user, state);
}
}
string identityErrrors = String.Join(" ", userCreationResult.Errors.Select(ie => ie.Description));
Logger.LogWarning($"Error registering user with external login. Email:{email}, Errors:" + Environment.NewLine + identityErrrors);
return Redirect($"/#error={identityErrrors}");
}
}
private async Task<RedirectResult> LoginWithLocalUser(User user, string state)
{
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
DateTime expirationDate = DateTime.UtcNow.AddDays(365);
string token = user.GenerateJwtToken(Configuration["Jwt:Key"], Configuration["Jwt:Issuer"], expirationDate);
return Redirect($"/#access_token={token}&token_type=bearer&expires_in={(int)(expirationDate - DateTime.UtcNow).TotalSeconds}&state={state}");
}

Token-based Authorization in Existing ASP.NET MVC App

I have inherited an existing application. This application uses ASP.NET MVC 3. It has some APIs. Those APIs look like the following:
[AcceptVerbs(HttpVerbs.Post)]
[Endpoint]
public ActionResult AuthenticatePlayer(string username, string password)
{
// Ensure that the user entered valid credentials
if (Membership.ValidateUser(username, password) == false)
return Json(new { statusCode = StatusCodes.INVALID_CREDENTIALS, message = "You entered an invalid username or password. Please try again." });
// Get the profile of the person that just logged in.
ProfileCommon userProfile = (ProfileCommon)(ProfileCommon.Create(username));
if (userProfile != null)
{
string name = username;
if (String.IsNullOrEmpty(userProfile.FirstName) == false)
name = userProfile.FirstName;
return Json(new {
statusCode = StatusCodes.SUCCESS,
payload = name,
username = username.ToLower(),
});
}
}
[AcceptVerbs(HttpVerbs.Get)]
[Endpoint]
public ActionResult SomeUserAction(string q)
{
// TODO: Ensure the user is authorized to perform this action via a token
// Do something
return Json(new { original = q, response = DateTime.UtcNow.Millisecond }, JsonRequestBehavior.AllowGet);
}
I'm trying to figure out how to integrate a token-based authorization schema into this process. From my understanding, a token-based system would return a short-lived token and a refresh token to a user if they successfully login. Then, each method can check to see if a user is authorized to perform the action by looking at the token. I'm trying to learn if this is built-in to ASP.NET MVC or if there is a library I can use. I need to figure out the shortest way to get this done.
Thank you so much!
I've built a WebAPI Token Authentication library a year ago, providing Token based authentication:
WebAPI Token Auth Bootstrap is out of the box Token based User Auth for WebAPI applications, Provides ready to use 'TokenAuthorize'
Attribute and 'TokenAuthApiController' Controller.
Among its features - Token Based User Authentication User Property inside the
TokenAuthApiController (Id, Username, Role, LastAccess).
Token Based User Authorization TokenAuthorizeAttribute with Access
Level - Public, User, Admin or Anonymous.
Built-in Functionality Login(), Logoff(), Error(), Unauthorized()
Responses with various overloads.
You can read more about here and in its own wiki in GitHub.
Nowadays I am working on a Node.js application and I am using Json Web Tokens (JWT) using Node.js library and it is very easy and straightforward.. its Node.js after all ;)
I saw there is a .NET implementation of JWT explained on this article which I recommend you to look at.
You can use Owin ... i.e. Microsoft.owin.security
I haven't tried this implementation but this is just to give you an idea:
var identity = new ClaimsIdentity(Startup.OAuthBearerOptions.AuthenticationType);
var currentUtc = new SystemClock().UtcNow;
ticket.Properties.IssuedUtc = currentUtc;
ticket.Properties.ExpiresUtc = currentUtc.Add(TimeSpan.FromMinutes(30));
DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
return Json(new {
statusCode = StatusCodes.SUCCESS,
payload = name,
username = username.ToLower(),
accessToken = Startup.OAuthBearerOptions.AccessTokenFormat.Protect(ticket)
});

Getting OAuth2 refresh token

I'm trying to use Google's Calendar API to demo out an OAuth2 integration that we'll need to do with another third party. I'm using the DotNetOpenAuth library, and I've been able to get the initial redirect to Google for the Allow / Deny prompt and get the authorization code back.
I now need to get the access token and refresh token, but I only seem to get an access token back, refresh token is null.
This is my controller action method where Google redirects back to after the user Accepts or Denies:
public ActionResult ProcessResponse(string state, string code, string error)
{
var oAuthClient =
new WebServerClient(
new AuthorizationServerDescription
{
TokenEndpoint = new Uri("https://accounts.google.com/o/oauth2/token"),
AuthorizationEndpoint = new Uri("https://accounts.google.com/o/oauth2/auth"),
ProtocolVersion = ProtocolVersion.V20
},
_applicationId,
_secret)
{
AuthorizationTracker = new TokenManager()
};
var authState = oAuthClient.ProcessUserAuthorization();
var accessToken = authState.AccessToken;
var refreshToken = authState.RefreshToken;
return View(new[] { accessToken, refreshToken });
}
Any ideas?
EDIT:
To get the authorization code, I setup the oAuthClient identically to what I did above, and use this method:
oAuthClient.RequestUserAuthorization(new[] { "https://www.googleapis.com/auth/calendar" }, returnUrl);
I had a similar problem, and solved mine by hand-coding the HttpRequest and HttpResponse handling. See code at: https://stackoverflow.com/a/11361759/29156

Categories

Resources