1
votes

I created a simple ASP.NET Core Web application using OAuth authentication from Google. I have this running on my local machine fine. Yet after deploying this as an AppService to Azure the OAuth redirects seem to get messed up.

The app itself can be found here:
https://gcalworkshiftui20180322114905.azurewebsites.net/

Here's an url that actually returns a result and shows that the app is running:
https://gcalworkshiftui20180322114905.azurewebsites.net/Account/Login?ReturnUrl=%2F

Sometimes the app responds fine but once I try to login using Google it keeps loading forever and eventually comes back with the following message:

The specified CGI application encountered an error and the server terminated the process.

Behind the scenes, the authentication callback that seems to be failing with a 502.3 error:

502.3 Bad Gateway “The operation timed out”

The error trace can be found here: https://gcalworkshiftui20180322114905.azurewebsites.net/errorlog.xml

The documentation from Microsoft hasn't really helped yet.
https://docs.microsoft.com/en-us/azure/app-service/app-service-authentication-overview

Further investigation leads me to believe that this has to do with the following code:

public GCalService(string clientId, string secret)
{
    string credPath = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);
    credPath = Path.Combine(credPath, ".credentials/calendar-dotnet-quickstart.json");

    var credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
        new ClientSecrets
        {
            ClientId = clientId,
            ClientSecret = secret
        },
        new[] {CalendarService.Scope.Calendar},
        "user",
        CancellationToken.None,
        new FileDataStore(credPath, true)).Result;

    // Create Google Calendar API service.
    _service = new CalendarService(new BaseClientService.Initializer()
    {
        HttpClientInitializer = credential,
        ApplicationName = "gcalworkshift"
    });
}

As I can imagine Azure not supporting personal folders? Googling about this doesn't tell me much.

1

1 Answers

1
votes

I followed Facebook, Google, and external provider authentication in ASP.NET Core and Google external login setup in ASP.NET Core to create a ASP.NET Core Web Application with Google authentication to check this issue.

I also followed .NET console application to access the Google Calendar API and Calendar.ASP.NET.MVC5 to build my sample project. Here is the core code, you could refer to them:

Startup.cs

    public class Startup
    {
        public readonly IDataStore dataStore = new FileDataStore(GoogleWebAuthorizationBroker.Folder); //C:\Users\{username}\AppData\Roaming\Google.Apis.Auth
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

            services.AddIdentity<ApplicationUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

            services.AddAuthentication().AddGoogle(googleOptions =>
            {
                googleOptions.ClientId = "{ClientId}";
                googleOptions.ClientSecret = "{ClientSecret}";
                googleOptions.Scope.Add(CalendarService.Scope.CalendarReadonly); //"https://www.googleapis.com/auth/calendar.readonly"
                googleOptions.AccessType = "offline"; //request a refresh_token
                googleOptions.Events = new OAuthEvents()
                {
                    OnCreatingTicket = async (context) =>
                    {
                        var userEmail = context.Identity.FindFirst(ClaimTypes.Email).Value;

                        var tokenResponse = new TokenResponse()
                        {
                            AccessToken = context.AccessToken,
                            RefreshToken = context.RefreshToken,
                            ExpiresInSeconds = (long)context.ExpiresIn.Value.TotalSeconds,
                            IssuedUtc = DateTime.UtcNow
                        };

                        await dataStore.StoreAsync(userEmail, tokenResponse);
                    }
                };
            });

            services.AddMvc();
        }

    }
}

CalendarController.cs

    [Authorize]
    public class CalendarController : Controller
    {

        private readonly IDataStore dataStore = new FileDataStore(GoogleWebAuthorizationBroker.Folder);

        private async Task<UserCredential> GetCredentialForApiAsync()
        {
            var initializer = new GoogleAuthorizationCodeFlow.Initializer
            {
                ClientSecrets = new ClientSecrets
                {
                    ClientId = "{ClientId}",
                    ClientSecret = "{ClientSecret}",
                },
                Scopes = new[] {
                    "openid",
                    "email",
                    CalendarService.Scope.CalendarReadonly
                }
            };
            var flow = new GoogleAuthorizationCodeFlow(initializer);

            string userEmail = ((ClaimsIdentity)HttpContext.User.Identity).FindFirst(ClaimTypes.Name).Value;

            var token = await dataStore.GetAsync<TokenResponse>(userEmail);
            return new UserCredential(flow, userEmail, token);
        }

        // GET: /Calendar/ListCalendars
        public async Task<ActionResult> ListCalendars()
        {
            const int MaxEventsPerCalendar = 20;
            const int MaxEventsOverall = 50;

            var credential = await GetCredentialForApiAsync();

            var initializer = new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = "ASP.NET Core Google Calendar Sample",
            };
            var service = new CalendarService(initializer);

            // Fetch the list of calendars.
            var calendars = await service.CalendarList.List().ExecuteAsync();

            return Json(calendars.Items);
        }
    }    

Before deploying to Azure web app, I changed the folder parameter for constructing the FileDataStore to D:\home, but got the following error:

UnauthorizedAccessException: Access to the path 'D:\home\Google.Apis.Auth.OAuth2.Responses.TokenResponse-{user-identifier}' is denied.

Then, I tried to set the parameter folder to D:\home\site and redeploy my web application and found it could work as expected and the logged user crendentials would be saved under the D:\home\site of your azure web app server.

enter image description here

Azure Web Apps run in a secure environment called the sandbox which has some limitations, details you could follow Azure Web App sandbox.

Additionally, you mentioned about the App Service Authentication which provides build-in authentication without adding any code in your code. Since you have wrote the code in your web application for authentication, you do not need to set up the App Service Authentication.

For using App Service Authentication, you could follow here for configuration, then your NetCore backend can obtain additional user details (access_token,refresh_token,etc.) through an HTTP GET on the /.auth/me endpoint, details you could follow this similar issue. After retrieved the token response for the logged user, you could manually construct the UserCredential, then build the CalendarService.