5
\$\begingroup\$

I am using OAuth2 for users to sign in with their Google Account and authenticate Google API requests.

I have not used the built-in ASP.NET Identity provider because this requires an app database of local user accounts (passwords, etc). I just want Google to tell us who the user is, keep hold of their auth/refresh tokens, and set an auth cookie to remember them.

Helper class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Auth.OAuth2.Flows;
using Google.Apis.Auth.OAuth2.Responses;
using Google.Apis.Auth.OAuth2.Web;
using Google.Apis.Oauth2.v2;
using Google.Apis.Requests;
using Google.Apis.Services;
using Google.Apis.Util.Store;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc;

public static class GoogleOAuth2
{
  private static string AppName { get; set; }
  private static GoogleAuthorizationCodeFlow Flow { get; set; }
  private static GoogleAuthorizationCodeFlow FlowPromptConsent { get; set; }

  public static void Configure(string appName, IEnumerable<string> scopes, string clientId, string clientSecret, IDataStore dataStore)
  {
    AppName = appName;
    Flow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer {
      DataStore = dataStore,
      ClientSecrets = new ClientSecrets {
        ClientId = clientId,
        ClientSecret = clientSecret
      },
      Scopes = scopes
    });
    FlowPromptConsent = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer {
      DataStore = dataStore,
      ClientSecrets = new ClientSecrets {
        ClientId = clientId,
        ClientSecret = clientSecret
      },
      Scopes = scopes,
      UserDefinedQueryParams = new[] { KeyValuePair.Create("prompt", "consent") }
    });
  }

  public static string GetEmail(this ClaimsPrincipal user)
  {
    var claims = (ClaimsIdentity)user.Identity;
    return claims.FindFirst(ClaimTypes.Email)?.Value;
  }

  public static async Task<BaseClientService.Initializer> GetInitializerAsync(string email)
  {
    var token = await Flow.DataStore.GetAsync<TokenResponse>(email) ?? throw new KeyNotFoundException("User token not found in the data store.");
    return new BaseClientService.Initializer {
      ApplicationName = AppName,
      HttpClientInitializer = new UserCredential(Flow, email, token)
    };
  }

  public static async Task<ActionResult> LoginWithGoogleAsync(this Controller controller)
  {
    var userId = controller.HttpContext.Session.Id;
    var uri = controller.Request.GetDisplayUrl();
    var redirectUri = uri.Substring(0, uri.Length - controller.Request.QueryString.Value.Length); // Current URI without query
    var code = controller.Request.Query["code"].ToString();
    string appReturnUrl = null;

    UserCredential credential = null;

    if (code == string.Empty) {
      // First pass: 'code' is empty
      if (controller.Request.Query.ContainsKey("ReturnUrl")) {
        controller.TempData["ReturnUrl"] = controller.Request.Query["ReturnUrl"].ToString();
      }
      var auth = await new AuthorizationCodeWebApp(Flow, redirectUri, redirectUri).AuthorizeAsync(userId, CancellationToken.None);
      if (auth.RedirectUri != null) {
        return controller.Redirect(auth.RedirectUri); // To Google login page
      }
      credential = auth.Credential;
    } else {
      // On return from the Google login page:
      var token = await Flow.ExchangeCodeForTokenAsync(userId, code, redirectUri, CancellationToken.None);
      credential = new UserCredential(Flow, userId, token);
    }

    var oauthService = new Oauth2Service(new BaseClientService.Initializer { HttpClientInitializer = credential });
    var userInfo = await oauthService.Userinfo.Get().ExecuteAsync();

    // Check if we already have a token for this email address

    var store = Flow.DataStore;
    var oldToken = await store.GetAsync<TokenResponse>(userInfo.Email);
    var newToken = await store.GetAsync<TokenResponse>(userId);
    await store.DeleteAsync<TokenResponse>(userId); // Clean up token store
    await store.DeleteAsync<string>("oauth_" + userId); // Clean up token store

    if (newToken.RefreshToken == null) {

      // If Google didn't give us a refresh token, take the previously saved one if
      // it exists, or redirect back to Google and force a consent screen

      if (oldToken?.RefreshToken != null) {
        newToken.RefreshToken = oldToken.RefreshToken;
      } else {
        controller.TempData.Keep("ReturnUrl");
        var auth = await new AuthorizationCodeWebApp(FlowPromptConsent, redirectUri, redirectUri).AuthorizeAsync(userId, CancellationToken.None);
        return controller.Redirect(auth.RedirectUri);
      }

    }
    await store.StoreAsync(userInfo.Email, newToken); // Save new token, using user's email as key

    var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
    identity.AddClaim(new Claim(ClaimTypes.Name, $"{userInfo.GivenName} {userInfo.FamilyName}"));
    identity.AddClaim(new Claim(ClaimTypes.Email, userInfo.Email));
    await controller.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity), new AuthenticationProperties { IsPersistent = true });

    appReturnUrl = controller.TempData["ReturnUrl"]?.ToString() ?? "/";
    return controller.Redirect(appReturnUrl);
  }
}

Note that session state is used to hold a temporary User Id while communicating with Google, but once we've got an email address for the user this is used as the permanent key in the token store.

Login:

[AllowAnonymous]
[Route("/login")]
public async Task<IActionResult> Login()
{
  if (User.Identity.IsAuthenticated) return Redirect("/");
  return await this.LoginWithGoogleAsync();
}

Google API calls:

var service = new CalendarService(await GoogleOAuth2.GetInitializerAsync(User.GetEmail()));
var calendars = await service.CalendarList.List().ExecuteAsync();
\$\endgroup\$
  • 1
    \$\begingroup\$ What did you end up doing? \$\endgroup\$ – Simon_Weaver Dec 7 '17 at 7:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.